Something went wrong. Try again.
An opinionated framework for small task-based applications.
Something went wrong. Try again.
4.3 kB · 136 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137//! Adds support for asking for input in the middle of a task.//! See the demo for more.// TODO: more here
use thiserror::Error;use tokio::sync::{RwLock, RwLockReadGuard, oneshot};use wallsocket_core::schema::{CompletedSchema, FieldDesc, Schema};
/// Tracks input prompts, and allows answering them.#[derive(Default)]pub struct InputRegistry { in_flight: RwLock<Vec<PendingInputRequest>>,}
/// An opaque identifier for an input prompt.pub type Id = ulid::Ulid;
/// A pending request for input.#[derive(Debug)]pub struct PendingInputRequest { /// Opaque identifier for this request. pub id: Id,
/// Description of what type of input is needed, and human-readable prompt. pub request: Schema,
sender: oneshot::Sender<CompletedSchema>,}
impl InputRegistry { /// Get the pending requests for input. pub async fn pending(&self) -> RwLockReadGuard<'_, [PendingInputRequest]> { RwLockReadGuard::map(self.in_flight.read().await, Vec::as_slice) }
/// Answer a pending input request. /// # Errors /// If `id` isn't valid. /// If the request corresponding to `id` has already been answered. /// If the type of `completed` doesn't match up with the original request. pub async fn answer(&self, id: Id, completed: CompletedSchema) -> Result<(), InputAnswerError> { let entry = { let mut in_flight = self.in_flight.write().await; let len = in_flight.len(); in_flight.extract_if(0..len, |req| req.id == id).next() };
let Some(entry) = entry else { return Err(InputAnswerError::BadId); };
// ensure that completed request matches type if !completed.matches(&entry.request) { return Err(InputAnswerError::BadAnswer); }
entry .sender .send(completed) .map_err(|_| InputAnswerError::AlreadyAnswered)?;
Ok(()) }
/// Request some input, blocking until we receive an answer or fail to do so. /// # Errors /// If the application is shutting down before we get an answer. pub async fn do_request( &self, req: Schema, ) -> Result<CompletedSchema, oneshot::error::RecvError> { let (send, recv) = oneshot::channel();
self.in_flight.write().await.push(PendingInputRequest { id: Id::new(), request: req, sender: send, });
recv.await }
/// Ask a yes or no question, with the given prompt. /// # Panics /// If the application is shutting down before we get an answer. pub async fn yes_no(&self, prompt: impl AsRef<str>) -> bool { match self .do_request(Schema::YesNo(FieldDesc { short_description: prompt.as_ref().to_string(), })) .await .unwrap() { CompletedSchema::Boolean(s) => s, _ => unreachable!(), } }
/// Ask for a free-form short text response, with the given prompt. /// # Panics /// If the application is shutting down before we get an answer. pub async fn text_box(&self, prompt: impl AsRef<str>) -> String { match self .do_request(Schema::YesNo(FieldDesc { short_description: prompt.as_ref().to_string(), })) .await .unwrap() { CompletedSchema::String(s) => s, _ => unreachable!(), } }}
/// An error encountered when trying to answer a request for input.#[derive(Debug, Clone, Copy, Error)]pub enum InputAnswerError { /// There's no pending input with that id. It may have already been answered, or have timed out. #[error("no pending input request with that id")] BadId,
/// That pending input has already been answered, or hung up. #[error("input request has already been answered, or hung up already")] AlreadyAnswered,
/// The pending input was found, but the answer given isn't in the shape required for it. #[error("response doesn't match the schema that was requested.")] BadAnswer,}
impl std::fmt::Debug for InputRegistry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("InputRegistry").finish_non_exhaustive() }}