chore: Stack Overflow bug in nickel-roundtrip

This commit is contained in:
Jesús Pérez 2025-12-28 18:56:17 +00:00
parent 39e5c35a28
commit 30b5b4797e
Signed by: jesus
GPG Key ID: 9F243E355E0BC939

View File

@ -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