diff --git a/CHANGELOG.md b/CHANGELOG.md index f38d4dc..170d464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,48 @@ ## [Unreleased] +### Refactored - Eliminate duplicated field execution logic + +**Single source of truth for CLI field dispatch** + +Three independent copies of the `match field.field_type` dispatch existed across +`form_parser/executor.rs`, `backends/cli.rs`, and `nickel/roundtrip.rs`. Each had +drifted in behavior: `executor.rs` lacked `RepeatingGroup`, `cli.rs` lacked +`options_from` filtering, `roundtrip.rs` had both but also its own `filter_options_from`. + +- Consolidated into `InquireBackend::execute_field_sync` (`backends/cli.rs`) as the + single canonical implementation +- Added `previous_results: &HashMap` parameter to `execute_field_sync` + (now `pub(crate)`) — unlocks `options_from` filtering for dependent `Select` fields +- Moved `filter_options_from` to `backends/cli.rs` as `pub(crate)`, removed the two + duplicate copies from `executor.rs` and `nickel/roundtrip.rs` +- `FormBackend::execute_field` trait impl now passes `context.results` directly to + `execute_field_sync` instead of ignoring the accumulated results +- Legacy sync path (`execute_with_base_dir`, `execute`, `load_and_execute_from_file`) + gated on `#[cfg(feature = "cli")]` — semantically correct, previously they compiled + against non-CLI feature combinations without the Inquire dependency being present +- Net: −346 lines across 4 files, 0 behavior regressions + +**Fixed - Doctest compilation failures** + +Five doctests in `typedialog-core` failed to compile against the current API: + +- `lib.rs` / `prelude.rs`: `FormDefinition { title, display_items }` → fields renamed + to `name` / `elements` in a prior refactor; replaced with `form_parser::parse_toml()` +- `lib.rs` / `prompt_api.rs`: `confirm(prompt, default)` → takes 3 args + (`prompt, default, formatter`); added missing `None` +- `advanced.rs`: example implemented `render_text`, `render_confirm`, etc. — methods + that no longer exist in `FormBackend`; replaced with the 7 real trait methods +- `config/cli_loader.rs`: `ignore` annotation with fictional `MyBackendConfig` type; + replaced with a concrete inline struct and changed to `no_run` + +**Added - Architecture documentation** + +- New `docs/architecture.md`: technical positioning (TypeDialog vs Inquire/Ratatui/Axum), + `BackendFactory` mechanics, three-phase execution model, `CLI Field Execution` section + documenting `execute_field_sync` + `filter_options_from` contract, legacy sync path, + and remaining known technical debt + ### Added - Web Backend Browser Auto-Launch **`--open` Flag for typedialog-web** diff --git a/crates/typedialog-core/src/advanced.rs b/crates/typedialog-core/src/advanced.rs index 629fb23..a585279 100644 --- a/crates/typedialog-core/src/advanced.rs +++ b/crates/typedialog-core/src/advanced.rs @@ -9,33 +9,36 @@ //! # Examples //! //! ```no_run -//! use typedialog_core::advanced::{FormBackend, RenderContext}; +//! use typedialog_core::advanced::{FormBackend, RenderContext, DisplayItem}; //! use typedialog_core::error::Result; +//! use typedialog_core::form_parser::FieldDefinition; +//! use serde_json::Value; //! -//! // Implement a custom backend //! struct MyCustomBackend; //! //! #[async_trait::async_trait] //! impl FormBackend for MyCustomBackend { -//! async fn render_text( -//! &mut self, -//! prompt: &str, -//! default: Option<&str>, -//! placeholder: Option<&str>, -//! _context: &RenderContext, -//! ) -> Result { -//! // Custom implementation -//! Ok(format!("{}: custom", prompt)) +//! async fn initialize(&mut self) -> Result<()> { +//! Ok(()) //! } //! -//! // ... implement other required methods -//! # async fn render_confirm(&mut self, _: &str, _: Option, _: &RenderContext) -> Result { Ok(true) } -//! # async fn render_select(&mut self, _: &str, _: &[String], _: Option, _: Option, _: bool, _: &RenderContext) -> Result { Ok("".into()) } -//! # async fn render_multiselect(&mut self, _: &str, _: &[String], _: &[String], _: Option, _: bool, _: &RenderContext) -> Result> { Ok(vec![]) } -//! # async fn render_password(&mut self, _: &str, _: &RenderContext) -> Result { Ok("".into()) } -//! # async fn render_custom(&mut self, _: &str, _: &str, _: &RenderContext) -> Result { Ok("".into()) } -//! # async fn render_editor(&mut self, _: &str, _: Option<&str>, _: Option<&str>, _: &RenderContext) -> Result { Ok("".into()) } -//! # async fn render_date(&mut self, _: &str, _: Option, _: Option, _: Option, _: chrono::Weekday, _: &RenderContext) -> Result { Ok(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()) } +//! async fn render_display_item(&self, _item: &DisplayItem, _ctx: &RenderContext) -> Result<()> { +//! Ok(()) +//! } +//! +//! async fn execute_field(&self, field: &FieldDefinition, _ctx: &RenderContext) -> Result { +//! Ok(Value::String(format!("response for {}", field.name))) +//! } +//! +//! async fn shutdown(&mut self) -> Result<()> { +//! Ok(()) +//! } +//! +//! fn as_any(&self) -> &dyn std::any::Any { self } +//! +//! fn is_available() -> bool { true } +//! +//! fn name(&self) -> &str { "my-custom-backend" } //! } //! ``` diff --git a/crates/typedialog-core/src/backends/cli.rs b/crates/typedialog-core/src/backends/cli.rs index 5241865..cce5ca3 100644 --- a/crates/typedialog-core/src/backends/cli.rs +++ b/crates/typedialog-core/src/backends/cli.rs @@ -5,12 +5,38 @@ use super::{FormBackend, RenderContext}; use crate::error::Result; -use crate::form_parser::{DisplayItem, FieldDefinition, FieldType}; +use crate::form_parser::{DisplayItem, FieldDefinition, FieldType, SelectOption}; use crate::prompts; use async_trait::async_trait; use serde_json::Value; use std::collections::HashMap; +pub(crate) fn filter_options_from( + field: &FieldDefinition, + previous_results: &HashMap, +) -> Vec { + let Some(ref source_field) = field.options_from else { + return field.options.clone(); + }; + let Some(source_value) = previous_results.get(source_field) else { + return field.options.clone(); + }; + let selected_values: Vec = match source_value { + Value::Array(arr) => arr + .iter() + .filter_map(|v| v.as_str().map(str::to_owned)) + .collect(), + Value::String(s) => s.split(',').map(|item| item.trim().to_owned()).collect(), + _ => return field.options.clone(), + }; + field + .options + .iter() + .filter(|opt| selected_values.contains(&opt.value)) + .cloned() + .collect() +} + /// CLI Backend implementation using inquire pub struct InquireBackend; @@ -20,8 +46,11 @@ impl InquireBackend { InquireBackend } - #[allow(clippy::only_used_in_recursion)] - fn execute_field_sync(&self, field: &FieldDefinition) -> Result { + pub(crate) fn execute_field_sync( + &self, + field: &FieldDefinition, + previous_results: &HashMap, + ) -> Result { let is_required = field.required.unwrap_or(false); let required_marker = if is_required { " *" } else { " (optional)" }; @@ -36,7 +65,7 @@ impl InquireBackend { if is_required && result.is_empty() { eprintln!("⚠ This field is required. Please enter a value."); - return self.execute_field_sync(field); + return self.execute_field_sync(field, previous_results); } Ok(serde_json::json!(result)) } @@ -63,7 +92,7 @@ impl InquireBackend { if is_required && result.is_empty() { eprintln!("⚠ This field is required. Please enter a value."); - return self.execute_field_sync(field); + return self.execute_field_sync(field, previous_results); } Ok(serde_json::json!(result)) } @@ -75,8 +104,14 @@ impl InquireBackend { )); } let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let options = field - .options + let filtered = filter_options_from(field, previous_results); + if filtered.is_empty() { + return Err(crate::ErrorWrapper::form_parse_failed(format!( + "No options available for field '{}'. Check options_from reference.", + field.name + ))); + } + let options = filtered .iter() .map(|opt| opt.as_string()) .collect::>(); @@ -110,7 +145,7 @@ impl InquireBackend { if is_required && results.is_empty() { eprintln!("⚠ This field is required. Please select at least one option."); - return self.execute_field_sync(field); + return self.execute_field_sync(field, previous_results); } Ok(serde_json::json!(results)) } @@ -125,7 +160,7 @@ impl InquireBackend { if is_required && result.is_empty() { eprintln!("⚠ This field is required. Please enter a value."); - return self.execute_field_sync(field); + return self.execute_field_sync(field, previous_results); } Ok(serde_json::json!(result)) } @@ -153,7 +188,7 @@ impl InquireBackend { if is_required && result.is_empty() { eprintln!("⚠ This field is required. Please enter a value."); - return self.execute_field_sync(field); + return self.execute_field_sync(field, previous_results); } Ok(serde_json::json!(result)) } @@ -311,7 +346,7 @@ impl InquireBackend { for element in &fragment_form.elements { if let crate::form_parser::FormElement::Field(field_def) = element { - let value = backend.execute_field_sync(field_def)?; + let value = backend.execute_field_sync(field_def, &results)?; results.insert(field_def.name.clone(), value); } } @@ -435,10 +470,9 @@ impl FormBackend for InquireBackend { async fn execute_field( &self, field: &FieldDefinition, - _context: &RenderContext, + context: &RenderContext, ) -> Result { - // Wrap synchronous field execution in async context - self.execute_field_sync(field) + self.execute_field_sync(field, &context.results) } fn as_any(&self) -> &dyn std::any::Any { diff --git a/crates/typedialog-core/src/config/cli_loader.rs b/crates/typedialog-core/src/config/cli_loader.rs index 8e172bd..b3de4bf 100644 --- a/crates/typedialog-core/src/config/cli_loader.rs +++ b/crates/typedialog-core/src/config/cli_loader.rs @@ -21,15 +21,26 @@ use std::path::Path; /// /// # Example /// -/// ```ignore +/// ```no_run /// use typedialog_core::config::load_backend_config; +/// use serde::{Deserialize, Serialize}; /// use std::path::PathBuf; /// -/// let config = load_backend_config::( -/// "cli", -/// Some(PathBuf::from("custom.toml").as_path()), -/// MyBackendConfig::default() -/// )?; +/// #[derive(Serialize, Deserialize, Default)] +/// struct CliConfig { +/// timeout: u32, +/// verbose: bool, +/// } +/// +/// # fn main() -> typedialog_core::error::Result<()> { +/// // Without explicit path: searches ~/.config/typedialog/cli/ +/// let config = load_backend_config::("cli", None, CliConfig::default())?; +/// +/// // With explicit path +/// let path = PathBuf::from("custom.toml"); +/// let config = load_backend_config::("cli", Some(path.as_path()), CliConfig::default())?; +/// # Ok(()) +/// # } /// ``` pub fn load_backend_config( backend_name: &str, diff --git a/crates/typedialog-core/src/form_parser/executor.rs b/crates/typedialog-core/src/form_parser/executor.rs index 213b1d4..f09a615 100644 --- a/crates/typedialog-core/src/form_parser/executor.rs +++ b/crates/typedialog-core/src/form_parser/executor.rs @@ -3,16 +3,13 @@ //! Handles form execution with various backends and execution modes. use crate::error::Result; -use crate::prompts; use std::collections::{BTreeMap, HashMap}; use std::path::Path; use super::conditions::evaluate_condition; use super::parser::load_elements_from_file; use super::translation::{translate_display_item, translate_field_definition}; -use super::types::{ - DisplayItem, DisplayMode, FieldDefinition, FieldType, FormDefinition, FormElement, -}; +use super::types::{DisplayItem, DisplayMode, FieldDefinition, FormDefinition, FormElement}; #[cfg(feature = "i18n")] use crate::i18n::I18nBundle; @@ -81,6 +78,7 @@ pub fn render_display_item(item: &DisplayItem, results: &HashMap Result> { execute_with_base_dir(form, Path::new(".")) } /// Load form from TOML file and execute with proper path resolution +#[cfg(feature = "cli")] pub fn load_and_execute_from_file( path: impl AsRef, ) -> Result> { @@ -174,200 +175,6 @@ pub fn load_and_execute_from_file( execute_with_base_dir(form, base_dir) } -/// Filter field options based on options_from reference -fn filter_options_from( - field: &FieldDefinition, - previous_results: &HashMap, -) -> Vec { - // If no options_from specified, return all options - let Some(ref source_field) = field.options_from else { - return field.options.clone(); - }; - - // Get the source field's value - let Some(source_value) = previous_results.get(source_field) else { - // Source field not found, return all options - return field.options.clone(); - }; - - // Extract selected values from source field (could be array or comma-separated string) - let selected_values: Vec = match source_value { - serde_json::Value::Array(arr) => arr - .iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect(), - serde_json::Value::String(s) => s.split(',').map(|item| item.trim().to_string()).collect(), - _ => return field.options.clone(), // Unsupported type, return all - }; - - // Filter options to only include those in selected_values - field - .options - .iter() - .filter(|opt| selected_values.contains(&opt.value)) - .cloned() - .collect() -} - -/// Execute a single field -fn execute_field( - field: &FieldDefinition, - _previous_results: &HashMap, -) -> Result { - let is_required = field.required.unwrap_or(false); - let required_marker = if is_required { " *" } else { " (optional)" }; - - match field.field_type { - FieldType::Text => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let result = prompts::text( - &prompt_with_marker, - field.default.as_deref(), - field.placeholder.as_deref(), - )?; - - if is_required && result.is_empty() { - eprintln!("⚠ This field is required. Please enter a value."); - return execute_field(field, _previous_results); // Retry - } - Ok(serde_json::json!(result)) - } - - FieldType::Confirm => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let default_bool = - field - .default - .as_deref() - .and_then(|s| match s.to_lowercase().as_str() { - "true" | "yes" => Some(true), - "false" | "no" => Some(false), - _ => None, - }); - let result = prompts::confirm(&prompt_with_marker, default_bool, None)?; - Ok(serde_json::json!(result)) - } - - FieldType::Password => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let with_toggle = field.placeholder.as_deref() == Some("toggle"); - let result = prompts::password(&prompt_with_marker, with_toggle)?; - - if is_required && result.is_empty() { - eprintln!("⚠ This field is required. Please enter a value."); - return execute_field(field, _previous_results); // Retry - } - Ok(serde_json::json!(result)) - } - - FieldType::Select => { - if field.options.is_empty() { - return Err(crate::ErrorWrapper::form_parse_failed( - "Select field requires 'options'", - )); - } - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - - // Filter options based on options_from if specified - let filtered_options = filter_options_from(field, _previous_results); - - if filtered_options.is_empty() { - return Err(crate::ErrorWrapper::form_parse_failed(format!( - "No options available for field '{}'. Check options_from reference.", - field.name - ))); - } - - let options = filtered_options - .iter() - .map(|opt| opt.as_string()) - .collect::>(); - let result = prompts::select( - &prompt_with_marker, - options, - field.page_size, - field.vim_mode.unwrap_or(false), - )?; - Ok(serde_json::json!(result)) - } - - FieldType::MultiSelect => { - if field.options.is_empty() { - return Err(crate::ErrorWrapper::form_parse_failed( - "MultiSelect field requires 'options'", - )); - } - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let options = field - .options - .iter() - .map(|opt| opt.as_string()) - .collect::>(); - let results = prompts::multi_select( - &prompt_with_marker, - options, - field.page_size, - field.vim_mode.unwrap_or(false), - )?; - - if is_required && results.is_empty() { - eprintln!("⚠ This field is required. Please select at least one option."); - return execute_field(field, _previous_results); // Retry - } - Ok(serde_json::json!(results)) - } - - FieldType::Editor => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let result = prompts::editor( - &prompt_with_marker, - field.file_extension.as_deref(), - field.prefix_text.as_deref(), - )?; - - if is_required && result.is_empty() { - eprintln!("⚠ This field is required. Please enter a value."); - return execute_field(field, _previous_results); // Retry - } - Ok(serde_json::json!(result)) - } - - FieldType::Date => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let week_start = field.week_start.as_deref().unwrap_or("Mon"); - let result = prompts::date( - &prompt_with_marker, - field.default.as_deref(), - field.min_date.as_deref(), - field.max_date.as_deref(), - week_start, - )?; - Ok(serde_json::json!(result)) - } - - FieldType::Custom => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let type_name = field.custom_type.as_ref().ok_or_else(|| { - crate::ErrorWrapper::form_parse_failed("Custom field requires 'custom_type'") - })?; - let result = prompts::custom(&prompt_with_marker, type_name, field.default.as_deref())?; - - if is_required && result.is_empty() { - eprintln!("⚠ This field is required. Please enter a value."); - return execute_field(field, _previous_results); // Retry - } - Ok(serde_json::json!(result)) - } - - FieldType::RepeatingGroup => { - // Temporary stub - will be implemented in FASE 4 - Err(crate::ErrorWrapper::form_parse_failed( - "RepeatingGroup not yet implemented - use CLI backend (FASE 4)", - )) - } - } -} - /// Build element list from form definition with lazy loading of fragments fn build_element_list( form: &FormDefinition, diff --git a/crates/typedialog-core/src/form_parser/mod.rs b/crates/typedialog-core/src/form_parser/mod.rs index 1b367df..bcde2e9 100644 --- a/crates/typedialog-core/src/form_parser/mod.rs +++ b/crates/typedialog-core/src/form_parser/mod.rs @@ -25,15 +25,18 @@ pub use types::{ // Re-export public functions - parser pub use parser::{load_from_file, parse_toml}; -// Re-export public functions - executor +// Re-export public functions - executor (async backend paths, always available) pub use executor::{ - execute, execute_with_backend, execute_with_backend_complete, execute_with_backend_from_dir, + execute_with_backend, execute_with_backend_complete, execute_with_backend_from_dir, execute_with_backend_i18n, execute_with_backend_i18n_with_defaults, execute_with_backend_two_phase, execute_with_backend_two_phase_with_defaults, - execute_with_base_dir, load_and_execute_from_file, recompute_visible_elements, - render_display_item, + recompute_visible_elements, render_display_item, }; +// Re-export legacy sync paths (CLI-only: use InquireBackend directly) +#[cfg(feature = "cli")] +pub use executor::{execute, execute_with_base_dir, load_and_execute_from_file}; + // Re-export public functions - conditions pub use conditions::{ evaluate_condition, identify_selector_fields, should_load_fragment, value_to_string, diff --git a/crates/typedialog-core/src/lib.rs b/crates/typedialog-core/src/lib.rs index b35fa31..0bafcf3 100644 --- a/crates/typedialog-core/src/lib.rs +++ b/crates/typedialog-core/src/lib.rs @@ -22,17 +22,11 @@ //! //! ```no_run //! use typedialog_core::prelude::*; +//! use typedialog_core::form_parser; //! //! # async fn example() -> Result<()> { -//! // Create a backend and execute forms //! let mut backend = BackendFactory::create(BackendType::Cli)?; -//! let form = FormDefinition { -//! title: Some("User Registration".to_string()), -//! description: None, -//! locale: None, -//! fields: vec![], -//! display_items: vec![], -//! }; +//! let form = form_parser::parse_toml(r#"name = "User Registration""#)?; //! # Ok(()) //! # } //! ``` @@ -44,7 +38,7 @@ //! //! fn example() -> Result<()> { //! let name = prompt_api::text("Enter your name", None, None)?; -//! let confirmed = prompt_api::confirm("Continue?", Some(true))?; +//! let confirmed = prompt_api::confirm("Continue?", Some(true), None)?; //! println!("Hello, {}!", name); //! Ok(()) //! } diff --git a/crates/typedialog-core/src/nickel/roundtrip.rs b/crates/typedialog-core/src/nickel/roundtrip.rs index 742744b..f1cd48b 100644 --- a/crates/typedialog-core/src/nickel/roundtrip.rs +++ b/crates/typedialog-core/src/nickel/roundtrip.rs @@ -481,8 +481,8 @@ impl RoundtripConfig { } } - // Execute field using CLI prompts (from execute_with_base_dir logic) - let value = Self::execute_field_cli(field, &results)?; + let value = crate::backends::cli::InquireBackend::new() + .execute_field_sync(field, &results)?; results.insert(field.name.clone(), value.clone()); } } @@ -491,202 +491,6 @@ impl RoundtripConfig { Ok(results) } - /// Execute a single field using CLI prompts (extracted from executor.rs) - fn execute_field_cli( - field: &form_parser::FieldDefinition, - previous_results: &HashMap, - ) -> Result { - use crate::prompts; - - let is_required = field.required.unwrap_or(false); - let required_marker = if is_required { " *" } else { " (optional)" }; - - match field.field_type { - form_parser::FieldType::Text => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let result = prompts::text( - &prompt_with_marker, - field.default.as_deref(), - field.placeholder.as_deref(), - )?; - - if is_required && result.is_empty() { - eprintln!("⚠ This field is required. Please enter a value."); - return Self::execute_field_cli(field, previous_results); // Retry - } - Ok(serde_json::json!(result)) - } - - form_parser::FieldType::Confirm => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let default_bool = - field - .default - .as_deref() - .and_then(|s| match s.to_lowercase().as_str() { - "true" | "yes" => Some(true), - "false" | "no" => Some(false), - _ => None, - }); - let result = prompts::confirm(&prompt_with_marker, default_bool, None)?; - Ok(serde_json::json!(result)) - } - - form_parser::FieldType::Password => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let with_toggle = field.placeholder.as_deref() == Some("toggle"); - let result = prompts::password(&prompt_with_marker, with_toggle)?; - - if is_required && result.is_empty() { - eprintln!("⚠ This field is required. Please enter a value."); - return Self::execute_field_cli(field, previous_results); // Retry - } - Ok(serde_json::json!(result)) - } - - form_parser::FieldType::Select => { - if field.options.is_empty() { - return Err(crate::error::ErrorWrapper::new( - "Select field requires 'options'".to_string(), - )); - } - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - - // Filter options based on options_from if specified - let filtered_options = Self::filter_options_from(field, previous_results); - - if filtered_options.is_empty() { - return Err(crate::error::ErrorWrapper::new(format!( - "No options available for field '{}'. Check options_from reference.", - field.name - ))); - } - - let options = filtered_options - .iter() - .map(|opt| opt.as_string()) - .collect::>(); - let result = prompts::select( - &prompt_with_marker, - options, - field.page_size, - field.vim_mode.unwrap_or(false), - )?; - Ok(serde_json::json!(result)) - } - - form_parser::FieldType::MultiSelect => { - if field.options.is_empty() { - return Err(crate::error::ErrorWrapper::new( - "MultiSelect field requires 'options'".to_string(), - )); - } - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let options = field - .options - .iter() - .map(|opt| opt.as_string()) - .collect::>(); - let results = prompts::multi_select( - &prompt_with_marker, - options, - field.page_size, - field.vim_mode.unwrap_or(false), - )?; - - if is_required && results.is_empty() { - eprintln!("⚠ This field is required. Please select at least one option."); - return Self::execute_field_cli(field, previous_results); // Retry - } - Ok(serde_json::json!(results)) - } - - form_parser::FieldType::Editor => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let result = prompts::editor( - &prompt_with_marker, - field.file_extension.as_deref(), - field.prefix_text.as_deref(), - )?; - - if is_required && result.is_empty() { - eprintln!("⚠ This field is required. Please enter a value."); - return Self::execute_field_cli(field, previous_results); // Retry - } - Ok(serde_json::json!(result)) - } - - form_parser::FieldType::Date => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let week_start = field.week_start.as_deref().unwrap_or("Mon"); - let result = prompts::date( - &prompt_with_marker, - field.default.as_deref(), - field.min_date.as_deref(), - field.max_date.as_deref(), - week_start, - )?; - Ok(serde_json::json!(result)) - } - - form_parser::FieldType::Custom => { - let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let type_name = field.custom_type.as_ref().ok_or_else(|| { - crate::error::ErrorWrapper::new( - "Custom field requires 'custom_type'".to_string(), - ) - })?; - let result = - prompts::custom(&prompt_with_marker, type_name, field.default.as_deref())?; - - if is_required && result.is_empty() { - eprintln!("⚠ This field is required. Please enter a value."); - return Self::execute_field_cli(field, previous_results); // Retry - } - Ok(serde_json::json!(result)) - } - - form_parser::FieldType::RepeatingGroup => Err(crate::error::ErrorWrapper::new( - "RepeatingGroup not yet implemented".to_string(), - )), - } - } - - /// Filter field options based on options_from reference (extracted from executor.rs) - fn filter_options_from( - field: &form_parser::FieldDefinition, - previous_results: &HashMap, - ) -> Vec { - // If no options_from specified, return all options - let Some(ref source_field) = field.options_from else { - return field.options.clone(); - }; - - // Get the source field's value - let Some(source_value) = previous_results.get(source_field) else { - // Source field not found, return all options - return field.options.clone(); - }; - - // Extract selected values from source field (could be array or comma-separated string) - let selected_values: Vec = match source_value { - Value::Array(arr) => arr - .iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect(), - Value::String(s) => s.split(',').map(|item| item.trim().to_string()).collect(), - _ => return field.options.clone(), // Unsupported type, return all - }; - - // Filter options to only include those in selected_values - field - .options - .iter() - .filter(|opt| selected_values.contains(&opt.value)) - .cloned() - .collect() - } - /// Load defaults from input Nickel file using form field nickel_path fn load_defaults_from_input( input_path: &Path, diff --git a/crates/typedialog-core/src/prelude.rs b/crates/typedialog-core/src/prelude.rs index 350ae3c..638a8e6 100644 --- a/crates/typedialog-core/src/prelude.rs +++ b/crates/typedialog-core/src/prelude.rs @@ -7,16 +7,11 @@ //! //! ```no_run //! use typedialog_core::prelude::*; +//! use typedialog_core::form_parser; //! //! async fn example() -> Result<()> { //! let mut backend = BackendFactory::create(BackendType::Cli)?; -//! let form = FormDefinition { -//! title: Some("Example Form".to_string()), -//! description: None, -//! locale: None, -//! fields: vec![], -//! display_items: vec![], -//! }; +//! let form = form_parser::parse_toml(r#"name = "Example Form""#)?; //! Ok(()) //! } //! ``` diff --git a/crates/typedialog-core/src/prompt_api.rs b/crates/typedialog-core/src/prompt_api.rs index 93006fb..df4af1e 100644 --- a/crates/typedialog-core/src/prompt_api.rs +++ b/crates/typedialog-core/src/prompt_api.rs @@ -10,7 +10,7 @@ //! //! fn example() -> Result<()> { //! let name = prompt_api::text("Enter your name", None, None)?; -//! let confirmed = prompt_api::confirm("Continue?", Some(true))?; +//! let confirmed = prompt_api::confirm("Continue?", Some(true), None)?; //! println!("Hello, {}!", name); //! Ok(()) //! }