chore: Stack Overflow bug in nickel-roundtrip
This commit is contained in:
parent
39e5c35a28
commit
30b5b4797e
@ -396,6 +396,8 @@ impl RoundtripConfig {
|
||||
form_path: &Path,
|
||||
initial_values: HashMap<String, Value>,
|
||||
) -> Result<HashMap<String, Value>> {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Read form definition
|
||||
let form_content = fs::read_to_string(form_path).map_err(|e| {
|
||||
crate::error::ErrorWrapper::new(format!("Failed to read form file: {}", e))
|
||||
@ -407,8 +409,15 @@ impl RoundtripConfig {
|
||||
// Migrate to unified elements format
|
||||
form.migrate_to_elements();
|
||||
|
||||
// Merge initial values into form defaults (unified elements format)
|
||||
for element in &mut form.elements {
|
||||
// Extract base directory for resolving relative paths (includes, fragments)
|
||||
let base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
|
||||
|
||||
// CRITICAL FIX: Expand fragments BEFORE applying defaults
|
||||
// This ensures defaults are applied to real fields, not to groups with includes
|
||||
let mut expanded_form = form_parser::expand_includes(form, base_dir)?;
|
||||
|
||||
// Now apply initial values as defaults to the EXPANDED fields
|
||||
for element in &mut expanded_form.elements {
|
||||
if let form_parser::FormElement::Field(field) = element {
|
||||
if let Some(value) = initial_values.get(&field.name) {
|
||||
// Set as default if field doesn't already have one
|
||||
@ -419,12 +428,262 @@ impl RoundtripConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract base directory for resolving relative paths (includes, fragments)
|
||||
let base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
|
||||
// Execute expanded form directly (without re-expanding includes)
|
||||
// This is a simplified version of execute_with_base_dir that doesn't call expand_includes again
|
||||
let mut results = HashMap::new();
|
||||
|
||||
// Execute form using CLI backend (interactive prompts)
|
||||
// This will expand includes from groups during execution
|
||||
form_parser::execute_with_base_dir(form, base_dir)
|
||||
// Print form header
|
||||
if let Some(desc) = &expanded_form.description {
|
||||
println!("\n{}\n{}\n", expanded_form.name, desc);
|
||||
} else {
|
||||
println!("\n{}\n", expanded_form.name);
|
||||
}
|
||||
|
||||
// Build ordered element map
|
||||
let mut element_map: BTreeMap<usize, form_parser::FormElement> = BTreeMap::new();
|
||||
let mut order_counter = 0;
|
||||
|
||||
for element in expanded_form.elements {
|
||||
let order = match &element {
|
||||
form_parser::FormElement::Item(item) => {
|
||||
if item.order == 0 {
|
||||
order_counter += 1;
|
||||
order_counter - 1
|
||||
} else {
|
||||
item.order
|
||||
}
|
||||
}
|
||||
form_parser::FormElement::Field(field) => {
|
||||
if field.order == 0 {
|
||||
order_counter += 1;
|
||||
order_counter - 1
|
||||
} else {
|
||||
field.order
|
||||
}
|
||||
}
|
||||
};
|
||||
element_map.insert(order, element);
|
||||
}
|
||||
|
||||
// Process elements in order (using CLI prompts)
|
||||
for (_, element) in element_map.iter() {
|
||||
match element {
|
||||
form_parser::FormElement::Item(item) => {
|
||||
form_parser::render_display_item(item, &results);
|
||||
}
|
||||
form_parser::FormElement::Field(field) => {
|
||||
// Check if field should be shown based on conditional
|
||||
if let Some(condition) = &field.when {
|
||||
if !form_parser::evaluate_condition(condition, &results) {
|
||||
// Field condition not met, skip it
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute field using CLI prompts (from execute_with_base_dir logic)
|
||||
let value = Self::execute_field_cli(field, &results)?;
|
||||
results.insert(field.name.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Execute a single field using CLI prompts (extracted from executor.rs)
|
||||
fn execute_field_cli(
|
||||
field: &form_parser::FieldDefinition,
|
||||
previous_results: &HashMap<String, Value>,
|
||||
) -> Result<Value> {
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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<String, Value>,
|
||||
) -> Vec<form_parser::SelectOption> {
|
||||
// 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<String> = 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user