chore: improve submit page and end info with backends

This commit is contained in:
Jesús Pérez 2025-12-28 13:29:23 +00:00
parent 070e338c5c
commit 2e75e2106c
Signed by: jesus
GPG Key ID: 9F243E355E0BC939
4 changed files with 317 additions and 57 deletions

View File

@ -55,6 +55,15 @@ pub struct RoundtripResult {
/// Validation result (if enabled)
pub validation_passed: Option<bool>,
/// Original input Nickel code (for diff)
pub input_nickel: String,
/// Path to output file
pub output_path: PathBuf,
/// Initial values loaded from input (for change detection)
pub initial_values: std::collections::HashMap<String, Value>,
}
impl RoundtripConfig {
@ -118,12 +127,26 @@ impl RoundtripConfig {
);
}
// Step 2: Execute form to get results
// Step 2: Load defaults from input .ncl (if fields have nickel_path)
let initial_values =
Self::load_defaults_from_input(&self.input_ncl, &self.form_path, self.verbose)?;
if self.verbose && !initial_values.is_empty() {
eprintln!(
"[roundtrip] Loaded {} default values from input",
initial_values.len()
);
}
// Step 3: Execute form to get results (with defaults pre-populated)
if self.verbose {
eprintln!("[roundtrip] Executing form: {}", self.form_path.display());
}
let form_results = Self::execute_form_with_backend(&self.form_path, backend).await?;
let initial_values_backup = initial_values.clone();
let form_results =
Self::execute_form_with_backend_and_defaults(&self.form_path, backend, initial_values)
.await?;
if self.verbose {
eprintln!(
@ -197,8 +220,11 @@ impl RoundtripConfig {
Ok(RoundtripResult {
input_contracts,
form_results,
output_nickel,
output_nickel: output_nickel.clone(),
validation_passed,
input_nickel: input_source,
output_path: self.output_ncl,
initial_values: initial_values_backup,
})
}
@ -230,12 +256,24 @@ impl RoundtripConfig {
);
}
// Step 2: Execute form to get results
// Step 2: Load defaults from input .ncl (if fields have nickel_path)
let initial_values =
Self::load_defaults_from_input(&self.input_ncl, &self.form_path, self.verbose)?;
if self.verbose && !initial_values.is_empty() {
eprintln!(
"[roundtrip] Loaded {} default values from input",
initial_values.len()
);
}
// Step 3: Execute form to get results (with defaults pre-populated)
if self.verbose {
eprintln!("[roundtrip] Executing form: {}", self.form_path.display());
}
let form_results = Self::execute_form(&self.form_path)?;
let initial_values_backup = initial_values.clone();
let form_results = Self::execute_form_with_defaults(&self.form_path, initial_values)?;
if self.verbose {
eprintln!(
@ -309,15 +347,19 @@ impl RoundtripConfig {
Ok(RoundtripResult {
input_contracts,
form_results,
output_nickel,
output_nickel: output_nickel.clone(),
validation_passed,
input_nickel: input_source,
output_path: self.output_ncl,
initial_values: initial_values_backup,
})
}
/// Execute a form with a specific backend and return results
async fn execute_form_with_backend(
/// Execute a form with a specific backend and return results (with defaults)
async fn execute_form_with_backend_and_defaults(
form_path: &Path,
backend: &mut dyn FormBackend,
initial_values: HashMap<String, Value>,
) -> Result<HashMap<String, Value>> {
// Read form definition
let form_content = fs::read_to_string(form_path).map_err(|e| {
@ -330,24 +372,160 @@ impl RoundtripConfig {
// Migrate to unified elements format if needed
form.migrate_to_elements();
// NOTE: We don't apply defaults here because execute_with_backend_two_phase_with_defaults
// will call build_element_list which reloads fragments from disk, losing any modifications.
// Instead, we pass initial_values to execute_with_backend_two_phase_with_defaults
// which will apply them after fragment expansion.
// Extract base directory for resolving relative paths (includes, fragments)
let base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
// Execute form using provided backend (TUI, Web, or CLI)
form_parser::execute_with_backend_two_phase(form, backend, None, base_dir).await
// Execute form using provided backend (TUI, Web, or CLI) with defaults
form_parser::execute_with_backend_two_phase_with_defaults(
form,
backend,
None,
base_dir,
Some(initial_values),
)
.await
}
/// Execute a form and return results (CLI backend only)
fn execute_form(form_path: &Path) -> Result<HashMap<String, Value>> {
// Load form definition from file (resolves constraint interpolations + includes)
let form = form_parser::load_from_file(form_path)?;
/// Execute a form and return results (CLI backend only, with defaults)
fn execute_form_with_defaults(
form_path: &Path,
initial_values: HashMap<String, Value>,
) -> Result<HashMap<String, Value>> {
// 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))
})?;
// Parse TOML form definition
let mut form = form_parser::parse_toml(&form_content)?;
// Migrate to unified elements format
form.migrate_to_elements();
// Merge initial values into form defaults (unified elements format)
for element in &mut 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
if field.default.is_none() {
field.default = Some(form_parser::value_to_string(value));
}
}
}
}
// Extract base directory for resolving relative paths (includes, fragments)
let base_dir = form_path.parent().unwrap_or_else(|| Path::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)
}
/// Load defaults from input Nickel file using form field nickel_path
fn load_defaults_from_input(
input_path: &Path,
form_path: &Path,
verbose: bool,
) -> Result<HashMap<String, Value>> {
// Export input .ncl to JSON
let json_value = match NickelCli::export(input_path) {
Ok(val) => val,
Err(_) => {
// If export fails (e.g., file is empty or has syntax errors),
// return empty defaults rather than failing the whole roundtrip
if verbose {
eprintln!(
"[roundtrip] Warning: Could not export input .ncl, proceeding without defaults"
);
}
return Ok(HashMap::new());
}
};
// Load form to get field definitions with nickel_path
let form_content = fs::read_to_string(form_path).map_err(|e| {
crate::error::ErrorWrapper::new(format!("Failed to read form file: {}", e))
})?;
let mut form = form_parser::parse_toml(&form_content)?;
form.migrate_to_elements();
// Extract base directory for resolving fragment includes
let base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
// Expand fragments to get ALL fields (including those in conditional groups)
// Uses expand_includes to process group elements with includes
let expanded_form = form_parser::expand_includes(form, base_dir)?;
// Extract field definitions that have nickel_path
let fields_with_paths: Vec<_> = expanded_form
.elements
.iter()
.filter_map(|elem| {
if let form_parser::FormElement::Field(field) = elem {
if field.nickel_path.is_some() {
Some(field.clone())
} else {
None
}
} else {
None
}
})
.collect();
if fields_with_paths.is_empty() {
// No fields with nickel_path, return empty
return Ok(HashMap::new());
}
// Extract values using nickel_path
let mut defaults = HashMap::new();
if let Value::Object(obj) = json_value {
for field in &fields_with_paths {
if let Some(nickel_path) = &field.nickel_path {
if let Some(value) =
Self::extract_value_by_path(&Value::Object(obj.clone()), nickel_path)
{
defaults.insert(field.name.clone(), value);
}
}
}
}
if verbose && !defaults.is_empty() {
eprintln!(
"[roundtrip] Extracted {} default values from {} fields with nickel_path",
defaults.len(),
fields_with_paths.len()
);
}
Ok(defaults)
}
/// Extract a value from nested JSON using a path
fn extract_value_by_path(json: &Value, path: &[String]) -> Option<Value> {
let mut current = json;
for key in path {
match current {
Value::Object(map) => {
current = map.get(key)?;
}
_ => return None,
}
}
Some(current.clone())
}
}
#[cfg(test)]
@ -367,4 +545,39 @@ mod tests {
assert_eq!(config.output_ncl, PathBuf::from("output.ncl"));
assert!(config.validate);
}
#[test]
fn test_extract_value_by_path() {
use serde_json::json;
let json = json!({
"ci": {
"project": {
"name": "test-project"
}
}
});
let path = vec!["ci".to_string(), "project".to_string(), "name".to_string()];
let result = RoundtripConfig::extract_value_by_path(&json, &path);
assert!(result.is_some());
assert_eq!(result.unwrap(), json!("test-project"));
}
#[test]
fn test_extract_value_by_path_missing() {
use serde_json::json;
let json = json!({
"ci": {
"project": {}
}
});
let path = vec!["ci".to_string(), "project".to_string(), "name".to_string()];
let result = RoundtripConfig::extract_value_by_path(&json, &path);
assert!(result.is_none());
}
}

View File

@ -181,23 +181,33 @@ pub async fn nickel_roundtrip(
eprintln!("[roundtrip] Generated {} bytes", result.output_nickel.len());
}
// Print summary
println!("✓ Roundtrip completed successfully (TUI backend)");
println!(" Input fields: {}", result.form_results.len());
println!(
" Imports preserved: {}",
result.input_contracts.imports.len()
);
println!(
" Contracts preserved: {}",
result.input_contracts.field_contracts.len()
// Create and print terminal summary
use typedialog_core::nickel::summary::RoundtripSummary;
let summary = RoundtripSummary::from_values(
&result.initial_values,
&result.form_results,
result.validation_passed,
result.output_path.display().to_string(),
);
// Print terminal summary
print!("{}", summary.render_terminal(verbose));
println!(
"\n✅ Configuration saved to: {}\n",
result.output_path.display()
);
println!("Next steps:");
println!(
" - Review the configuration: cat {}",
result.output_path.display()
);
println!(" - Apply CI tools: (run your CI setup command)");
println!(" - Re-run this script anytime to update your configuration\n");
if let Some(passed) = result.validation_passed {
if passed {
println!(" ✓ Validation: PASSED");
} else {
println!(" ✗ Validation: FAILED");
if !passed {
return Err(Error::validation_failed(
"Nickel typecheck failed on output",
));

View File

@ -589,11 +589,32 @@ async fn nickel_roundtrip_cmd(
let port = 8080;
let mut backend = BackendFactory::create(BackendType::Web { port })?;
// Set roundtrip context for web backend so it can show summary page
if let Some(init_vals) = &initial_values {
use typedialog_core::backends::web::RoundtripContext;
// Downcast to WebBackend to access state
let web_backend = backend
.as_any()
.downcast_ref::<typedialog_core::backends::web::WebBackend>()
.ok_or_else(|| Error::validation_failed("Expected WebBackend"))?;
if let Some(state) = web_backend.get_state() {
let context = RoundtripContext {
initial_values: init_vals.clone(),
output_path: output.clone(),
input_nickel: input_source.clone(),
};
state.set_roundtrip_context(context).await;
}
}
println!("Starting interactive form on http://localhost:{}", port);
println!("Complete the form and submit to continue...\n");
// USE THE SAME EXECUTION PATH AS NORMAL WEB BACKEND
// This respects display_mode and calls execute_form_complete() when display_mode = "complete"
let initial_values_backup = initial_values.clone();
let form_results = form_parser::execute_with_backend_i18n_with_defaults(
form,
backend.as_mut(),
@ -669,24 +690,29 @@ async fn nickel_roundtrip_cmd(
true
};
// Print summary
println!("✓ Roundtrip completed successfully (Web backend - interactive)");
println!(" Input fields: {}", form_results.len());
println!(" Imports preserved: {}", input_contracts.imports.len());
println!(
" Contracts preserved: {}",
input_contracts.field_contracts.len()
// Create and print terminal summary
use typedialog_core::nickel::summary::RoundtripSummary;
let summary = RoundtripSummary::from_values(
&initial_values_backup.unwrap_or_default(),
&form_results,
Some(validation_passed),
output.display().to_string(),
);
if validate {
if validation_passed {
println!(" ✓ Validation: PASSED");
} else {
println!(" ✗ Validation: FAILED");
return Err(Error::validation_failed(
"Nickel typecheck failed on output",
));
}
// Print terminal summary
print!("{}", summary.render_terminal(false));
println!("\n✅ Configuration saved to: {}\n", output.display());
println!("Next steps:");
println!(" - Review the configuration: cat {}", output.display());
println!(" - Apply CI tools: (run your CI setup command)");
println!(" - Re-run this script anytime to update: .typedialog/ci/ci-configure.sh\n");
if !validation_passed {
return Err(Error::validation_failed(
"Nickel typecheck failed on output",
));
}
Ok(())

View File

@ -175,22 +175,33 @@ pub fn nickel_roundtrip(
eprintln!("[roundtrip] Generated {} bytes", result.output_nickel.len());
}
println!("✓ Roundtrip completed successfully");
println!(" Input fields: {}", result.form_results.len());
println!(
" Imports preserved: {}",
result.input_contracts.imports.len()
);
println!(
" Contracts preserved: {}",
result.input_contracts.field_contracts.len()
// Create and print terminal summary
use typedialog_core::nickel::summary::RoundtripSummary;
let summary = RoundtripSummary::from_values(
&result.initial_values,
&result.form_results,
result.validation_passed,
result.output_path.display().to_string(),
);
// Print terminal summary
print!("{}", summary.render_terminal(verbose));
println!(
"\n✅ Configuration saved to: {}\n",
result.output_path.display()
);
println!("Next steps:");
println!(
" - Review the configuration: cat {}",
result.output_path.display()
);
println!(" - Apply CI tools: (run your CI setup command)");
println!(" - Re-run this script anytime to update your configuration\n");
if let Some(passed) = result.validation_passed {
if passed {
println!(" ✓ Validation: PASSED");
} else {
println!(" ✗ Validation: FAILED");
if !passed {
return Err(Error::validation_failed(
"Nickel typecheck failed on output",
));