chore: improve submit page and end info with backends
This commit is contained in:
parent
070e338c5c
commit
2e75e2106c
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
));
|
||||
|
||||
@ -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(())
|
||||
|
||||
@ -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",
|
||||
));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user