ibuilder/lib.rs
1//! [](https://github.com/edomora97/ibuilder/actions?query=workflow%3ARust)
2//! [](https://github.com/edomora97/ibuilder/actions?query=workflow%3AAudit)
3//! [](https://crates.io/crates/ibuilder)
4//! [](https://docs.rs/ibuilder)
5//!
6//! Interactive builders for structs.
7//!
8//! This crate provides a way to construct structs interactively, prompting the user multiple
9//! choices and text inputs.
10//!
11//! The builder provides the user with interactive menu-like interfaces, keeping the UI abstract
12//! and rust type-safeness.
13//!
14//! ## Rationale
15//! When building an interactive application (e.g. a Telegram bot or a console application) it can
16//! be pretty cumbersome to come out with a decent interface without writing tons of code for the
17//! logic for handling the parsing and the validation of the input.
18//!
19//! This crates provides a useful abstraction that allows an easy connection between the data and
20//! the user interface. Just by deriving the struct (or enum) that defines your data you can get a
21//! safe interface for building a UI.
22//!
23//! The derive API is inspired by the great [`structopt`](https://docs.rs/structopt) crate.
24//!
25//! ## API Overview
26//!
27//! The API of this crate is very simple:
28//! - Derive a struct (or an enum) from `IBuilder`, including all the structs/enums that it depends
29//! upon;
30//! - Call the `builder()` method (from the `Buildable` trait) to get an instance of `Builder<T>`;
31//! - Call `get_options()` on the builder to get an object that contains a message to show the user,
32//! a list of possible _choices_ (i.e. buttons to press) and eventually the possibility to enter
33//! some text (i.e. a text box);
34//! - Call `to_node()` on the builder to get a tree-like structure with the state of the builder,
35//! highlighting the fields that still need actions;
36//! - You choose how to show to the user the options and when the user made the decision call
37//! `choose(input)` on the builder. This will apply the choice to the state of the structure if
38//! it's valid, or return an error;
39//! - When the state is complete (all the required fields are present) a new option is present in
40//! the list: _Done_. If the user selects it `choose` will return an instance of `T`.
41//!
42//! A list of all the possible options for the `ibuilder` attribute can be found [here](https://docs.rs/ibuilder/*/ibuilder/derive.IBuilder.html).
43//!
44//! ## Supported Features
45//! - Deriving any struct with named fields (or with one unnamed field like `struct Foo(i64)`)
46//! - Enums (also with variants with field, but only one if unnamed)
47//! - Default values for the fields and default variant for enums
48//! - Custom message prompt for fields, structs, enums and variants
49//! - Renaming fields, structs and variants for better looking options
50//! - Hidden fields (that takes the value only from the default)
51//! - Nested structures (i.e. custom types)
52//! - Supported field types: all numeric types from rust, `bool`, `String`, `char`, `Box<T>`,
53//! `Vec<T>` and `Option<T>`
54//! - Any field type that implementes the `NewBuildableValue` trait
55//!
56//! ## Example of Usage
57//!
58//! In this example the data is stored inside a struct named `Person` which has 3 fields, one of
59//! which has a default value. Deriving from `IBuilder` gives access to the `builder()` method that
60//! returns a `Builder<Person>`.
61//!
62//!  |  | 
63//! :-------------------------:|:-------------------------:|:-------------------------:
64//! **Figure 1**: main menu | **Figure 2**: `AgeRange` menu | **Figure 3**: main menu again
65//!
66//! ```
67//! use ibuilder::*;
68//!
69//! #[derive(IBuilder)]
70//! pub struct Person {
71//! #[ibuilder(rename = "full name")]
72//! full_name: String,
73//! age: AgeRange,
74//! #[ibuilder(default = 2, rename = "number of hands")]
75//! num_hands: u64,
76//! }
77//!
78//! #[derive(IBuilder, Debug, Eq, PartialEq)]
79//! #[ibuilder(prompt = "How old are you?")]
80//! pub enum AgeRange {
81//! #[ibuilder(rename = "Less than 13 years old")]
82//! Child,
83//! #[ibuilder(rename = "From 13 to 19 years old")]
84//! Teen,
85//! #[ibuilder(rename = "20 years or more")]
86//! Adult,
87//! #[ibuilder(rename = "I don't want to tell")]
88//! Unknown,
89//! }
90//!
91//! let mut builder = Person::builder();
92//!
93//! // * figure 1 *
94//! let options = builder.get_options(); // main menu: select the field to edit
95//! builder.choose(Input::choice("age")).unwrap(); // select the field
96//!
97//! // * figure 2 *
98//! let options = builder.get_options(); // age menu
99//! builder.choose(Input::choice("Adult")).unwrap(); // insert the value
100//!
101//! let options = builder.get_options(); // back to the main menu
102//! builder.choose(Input::choice("full_name")).unwrap(); // select the field
103//!
104//! let options = builder.get_options(); // full_name menu
105//! assert!(options.text_input); // for inserting the string value
106//! builder.choose(Input::text("edomora97")).unwrap(); // insert the value
107//!
108//! // * figure 3 *
109//! assert!(builder.is_done());
110//! let options = builder.get_options(); // main menu again, but the "Done" option is available
111//! // chose the "Done" option, the return value is Ok(Some(Person))
112//! let value = builder.choose(Input::Choice(FINALIZE_ID.to_string())).unwrap().unwrap();
113//!
114//! assert_eq!(value.full_name, "edomora97");
115//! assert_eq!(value.age, AgeRange::Adult);
116//! assert_eq!(value.num_hands, 2);
117//! ```
118
119#[cfg(feature = "derive")]
120pub use ibuilder_derive::IBuilder;
121
122use std::any::Any;
123use std::marker::PhantomData;
124
125use failure::Fail;
126
127use crate::nodes::Node;
128
129pub mod builders;
130pub mod nodes;
131
132/// The identifier of the "Done" choice.
133pub const FINALIZE_ID: &str = "__finalize";
134/// The identifier of the "Back" choice.
135pub const BACK_ID: &str = "__back";
136
137/// Interactive builder for creating instances of the struct `T` by communicating. To instantiate a
138/// new `Builder` for the type `T`, make `T` derive from `IBuilder` and call `builder()` on it from
139/// the `Buildable` trait.
140///
141/// ## Communication
142/// After having instantiated a new `Builder<T>` you can call the `get_options()` method for
143/// fetching the list of possible actions that can be done to update the builder. Those options are
144/// like menu entries used to move between menus and set the value of the fields.
145///
146/// The `Options` struct contains a list of possible `Choice`s (like buttons to press) and
147/// eventually allow raw text input (like a textbox). For example while editing an integer field
148/// the user can insert the new value of the number _as a text_ or can choose to go back to the
149/// previous menu by pressing on "back".
150///
151/// The user's input is communicated to the `Builder` via the `choose` method. It takes an `Input`,
152/// a container with the choice of the user, which can be either some `Text` (if the `Options`
153/// allowed it), or a `Choice` (whose content is the identifier of the selected option between the
154/// ones in the `Options`).
155///
156/// When the user has filled all the fields of the builder, he can select the "done" options, which
157/// will make the `choose` method return `Ok(Some(T))`, signaling the end of the communication.
158#[derive(Debug)]
159pub struct Builder<T> {
160 builder: Box<dyn BuildableValue>,
161 current_fields: Vec<String>,
162 inner_type: PhantomData<T>,
163}
164
165/// A type that supports being built using a `Builder`. Deriving `IBuilder` an auto-generated
166/// implementation for this trait is provided.
167pub trait Buildable<T> {
168 /// Create a new `Builder<T>` for the current type.
169 fn builder() -> Builder<T>;
170}
171
172impl<T> Buildable<T> for T
173where
174 T: NewBuildableValue + 'static,
175{
176 fn builder() -> Builder<T> {
177 Builder::<T>::from_buildable_value(T::new_buildable_value(Default::default()))
178 }
179}
180
181/// The interactive builder for a base type.
182pub trait BuildableValue: std::fmt::Debug {
183 /// Try to change the inner value using the provided input.
184 fn apply(&mut self, data: Input, current_fields: &[String]) -> Result<(), ChooseError>;
185
186 /// The options to show to the user for setting this value.
187 fn get_options(&self, current_fields: &[String]) -> Options;
188
189 /// Whether this value contains itself other values (i.e. it's a struct).
190 fn get_subfields(&self, current_fields: &[String]) -> Vec<String>;
191
192 /// Create the tree structure of this value.
193 fn to_node(&self) -> Node;
194
195 /// Get the inner value, if present, as an `Any`.
196 ///
197 /// It's **very important** that the returned `Any` internal type matches the type that this
198 /// builder is used for. The `Builder` will downcast this `Any` to the types it's expecting,
199 /// panicking in case of mismatched type.
200 fn get_value_any(&self) -> Option<Box<dyn Any>>;
201}
202
203/// A type that can be built with a `BuildableValue` inside a `Builder`. Keep in mind that the
204/// semantics of the generated builder must be compatible with this type, especially looking at the
205/// `get_value_any` method.
206pub trait NewBuildableValue {
207 /// Construct a new `BuildableValue` using the provided configuration. Note that using this
208 /// constructor instead of the `new` method of the actual builder opaques the inner type.
209 fn new_buildable_value(config: BuildableValueConfig<()>) -> Box<dyn BuildableValue>;
210}
211
212/// The configuration for customizing the aspect of a `BuildableValue` that produces a value of type
213/// `T`.
214pub struct BuildableValueConfig<T> {
215 /// The default value to use, if `None` there is no default value and the field must be
216 /// provided.
217 pub default: Option<T>,
218 /// The prompt message to show to the user, if `None` a default message is shown.
219 pub prompt: Option<String>,
220}
221
222impl<T> Default for BuildableValueConfig<T> {
223 fn default() -> Self {
224 Self {
225 default: None,
226 prompt: None,
227 }
228 }
229}
230
231impl<T: Buildable<T>> Default for Builder<T> {
232 fn default() -> Self {
233 T::builder()
234 }
235}
236
237impl<T: 'static> Builder<T> {
238 /// Create a new builder from a `BuildableValue`. Note that the inner type of the
239 /// `BuildableValue` must match `T`, otherwise a panic is very likely.
240 pub fn from_buildable_value(inner: Box<dyn BuildableValue>) -> Builder<T> {
241 Self {
242 builder: inner,
243 current_fields: vec![],
244 inner_type: Default::default(),
245 }
246 }
247
248 /// Return all the valid options that this builder accepts in the current state.
249 pub fn get_options(&self) -> Options {
250 let mut options = self.builder.get_options(&self.current_fields);
251 // main menu
252 if self.current_fields.is_empty() {
253 if self.is_done() {
254 options.choices.push(Choice {
255 choice_id: FINALIZE_ID.to_string(),
256 text: "Done".to_string(),
257 needs_action: false,
258 });
259 }
260 // field menu
261 } else {
262 options.choices.push(Choice {
263 choice_id: BACK_ID.to_string(),
264 text: "Go back".to_string(),
265 needs_action: false,
266 });
267 }
268 options
269 }
270
271 /// Apply an input to the builder, making it change state. Call again `get_options()` for the
272 /// new options.
273 ///
274 /// Returns `Ok(None)` if the process is not done yet, `Ok(Some(T))` when the user choose to
275 /// finish the builder.
276 pub fn choose(&mut self, input: Input) -> Result<Option<T>, ChooseError> {
277 // main menu
278 if self.current_fields.is_empty() {
279 if let Input::Choice(data) = &input {
280 if data == FINALIZE_ID && self.is_done() {
281 return Ok(Some(self.finalize().expect("Finalize failed")));
282 }
283 }
284
285 // field menu
286 } else {
287 match &input {
288 Input::Choice(data) if data == BACK_ID => {
289 self.current_fields.pop();
290 return Ok(None);
291 }
292 _ => {}
293 }
294 };
295 let subfields = self.builder.get_subfields(&self.current_fields);
296 for subfield in subfields {
297 match &input {
298 Input::Choice(data) => {
299 if subfield == data.as_str() {
300 self.builder.apply(input, &self.current_fields)?;
301 self.current_fields.push(subfield);
302 return Ok(None);
303 }
304 }
305 Input::Text(_) => {}
306 }
307 }
308 self.builder.apply(input, &self.current_fields)?;
309 self.current_fields.pop();
310 Ok(None)
311 }
312
313 /// If the process is done try to finalize the process, even if the user hasn't completed the
314 /// the selection yet.
315 pub fn finalize(&self) -> Result<T, FinalizeError> {
316 self.builder
317 .get_value_any()
318 .ok_or(FinalizeError::MissingField)
319 .map(|r| *r.downcast::<T>().unwrap())
320 }
321
322 /// Check if all the fields have been set and the call to `finalize()` will be successful.
323 pub fn is_done(&self) -> bool {
324 self.builder.get_value_any().is_some()
325 }
326
327 /// Return the tree structure of the `Builder` internal state.
328 pub fn to_node(&self) -> Node {
329 self.builder.to_node()
330 }
331}
332
333/// The options that the user has for the next choice in the `Builder`.
334#[derive(Debug, Eq, PartialEq)]
335pub struct Options {
336 /// A textual message with the query to show to the user.
337 pub query: String,
338 /// Whether the user can insert raw textual inputs (i.e. `Input::Text`).
339 pub text_input: bool,
340 /// The list of all the choices the user can use.
341 pub choices: Vec<Choice>,
342}
343
344/// A single choice that the user can select.
345#[derive(Debug, Eq, PartialEq)]
346pub struct Choice {
347 /// Identifier of the choice, may not be shown to the user. Its value has to be used as the
348 /// value in `Input::Choice`.
349 pub choice_id: String,
350 /// Textual message to show to the user about this choice.
351 pub text: String,
352 /// This choice probably needs to be selected sooner or later because there is a field inside
353 /// that is missing.
354 pub needs_action: bool,
355}
356
357/// An input of the user to the `Builder`.
358#[derive(Debug, Eq, PartialEq)]
359pub enum Input {
360 /// The user inserted some raw textual content. Can be used only if the `text_input` field of
361 /// the last `Options` was set to `true`.
362 Text(String),
363 /// The user selected one of the multiple choices in the `Options`. The value should be one of
364 /// the `choice_id` inside the list of `Choice`s of the last `Options`.
365 Choice(String),
366}
367
368impl Input {
369 /// The user inserted some raw textual content. Can be used only if the `text_input` field of
370 /// the last `Options` was set to `true`.
371 pub fn text<S: Into<String>>(text: S) -> Input {
372 Input::Text(text.into())
373 }
374 /// The user selected one of the multiple choices in the `Options`. The value should be one of
375 /// the `choice_id` inside the list of `Choice`s of the last `Options`.
376 pub fn choice<S: Into<String>>(choice: S) -> Input {
377 Input::Choice(choice.into())
378 }
379}
380
381/// The `Input` provided to `Builder::choose` was is invalid.
382#[derive(Debug, Fail, Eq, PartialEq)]
383pub enum ChooseError {
384 /// The textual input is not valid.
385 #[fail(display = "Invalid input: {}", error)]
386 InvalidText { error: String },
387 /// Provided `Input::Text` even though `Options::text_input` was set to `false`.
388 #[fail(display = "Unexpected text")]
389 UnexpectedText,
390 /// Provided an `Input::Choice` with an invalid id.
391 #[fail(display = "Unexpected choice")]
392 UnexpectedChoice,
393}
394
395/// The finalization of the result failed.
396#[derive(Debug, Fail, Eq, PartialEq)]
397pub enum FinalizeError {
398 /// One or more fields were still missing.
399 #[fail(display = "There is at least a missing field")]
400 MissingField,
401}