chore: auto-format detection and cascade file path resolution, defaults multiformat
Some checks failed
CI / Lint (bash) (push) Has been cancelled
CI / Lint (markdown) (push) Has been cancelled
CI / Lint (nickel) (push) Has been cancelled
CI / Lint (nushell) (push) Has been cancelled
CI / Lint (rust) (push) Has been cancelled
CI / Benchmark (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / License Compliance (push) Has been cancelled
CI / Code Coverage (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (macos-latest) (push) Has been cancelled
CI / Build (ubuntu-latest) (push) Has been cancelled
CI / Build (windows-latest) (push) Has been cancelled
Some checks failed
CI / Lint (bash) (push) Has been cancelled
CI / Lint (markdown) (push) Has been cancelled
CI / Lint (nickel) (push) Has been cancelled
CI / Lint (nushell) (push) Has been cancelled
CI / Lint (rust) (push) Has been cancelled
CI / Benchmark (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / License Compliance (push) Has been cancelled
CI / Code Coverage (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (macos-latest) (push) Has been cancelled
CI / Build (ubuntu-latest) (push) Has been cancelled
CI / Build (windows-latest) (push) Has been cancelled
This commit is contained in:
parent
f490760395
commit
e58905c1f1
@ -7,7 +7,7 @@
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use typedialog_core::backends::{BackendFactory, BackendType};
|
||||
use typedialog_core::cli_common;
|
||||
use typedialog_core::config::{load_backend_config, TypeDialogConfig};
|
||||
@ -96,6 +96,42 @@ enum Commands {
|
||||
},
|
||||
}
|
||||
|
||||
/// Resolve a file path with cascading search relative to form directory
|
||||
///
|
||||
/// Search order:
|
||||
/// 1. If absolute path → use directly
|
||||
/// 2. If path exists as-is (relative to cwd) → use it
|
||||
/// 3. Try relative to form's directory → use if exists
|
||||
/// 4. For output files → prefer form's directory
|
||||
/// 5. For input files → return original (will fail with clear error)
|
||||
///
|
||||
/// Returns the resolved PathBuf.
|
||||
fn resolve_file_path(path: &Path, form_base_dir: &Path, is_output: bool) -> PathBuf {
|
||||
// 1. If absolute, use it directly
|
||||
if path.is_absolute() {
|
||||
return path.to_path_buf();
|
||||
}
|
||||
|
||||
// 2. Check if path exists as-is (relative to cwd)
|
||||
if path.exists() {
|
||||
return path.to_path_buf();
|
||||
}
|
||||
|
||||
// 3. Try relative to form's directory
|
||||
let form_relative = form_base_dir.join(path);
|
||||
if form_relative.exists() {
|
||||
return form_relative;
|
||||
}
|
||||
|
||||
// 4. For output files, prefer form directory even if doesn't exist yet
|
||||
if is_output {
|
||||
return form_relative;
|
||||
}
|
||||
|
||||
// 5. Return original path (will fail with clear error for input files)
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
/// Recursively flatten nested JSON objects into a single-level map
|
||||
/// Converts {"a": {"b": {"c": "value"}}} to {"a_b_c": "value"}
|
||||
fn flatten_json_object(
|
||||
@ -227,6 +263,29 @@ async fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Detect output format from filename extension
|
||||
///
|
||||
/// Returns the format string based on file extension:
|
||||
/// - `.toml` → "toml"
|
||||
/// - `.yaml` or `.yml` → "yaml"
|
||||
/// - `.json` → "json"
|
||||
/// - `.txt` → "text"
|
||||
/// - default → provided format
|
||||
fn detect_output_format(output_path: Option<&PathBuf>, default_format: &str) -> String {
|
||||
output_path
|
||||
.and_then(|p| p.extension())
|
||||
.and_then(|ext| ext.to_str())
|
||||
.and_then(|ext| match ext.to_lowercase().as_str() {
|
||||
"toml" => Some("toml"),
|
||||
"yaml" | "yml" => Some("yaml"),
|
||||
"json" => Some("json"),
|
||||
"txt" => Some("text"),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(default_format)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
async fn execute_form(
|
||||
config: PathBuf,
|
||||
defaults: Option<PathBuf>,
|
||||
@ -239,6 +298,9 @@ async fn execute_form(
|
||||
|
||||
let mut form = form_parser::parse_toml(&toml_content)?;
|
||||
|
||||
// Auto-detect format from output filename if not explicitly json
|
||||
let actual_format = detect_output_format(output.as_ref(), format);
|
||||
|
||||
// Web backend uses unified elements array internally, migrate if using legacy format
|
||||
form.migrate_to_elements();
|
||||
|
||||
@ -248,11 +310,15 @@ async fn execute_form(
|
||||
// Expand groups with includes to load fragment files
|
||||
form = form_parser::expand_includes(form, base_dir)?;
|
||||
|
||||
// Load default values from JSON or .ncl file if provided
|
||||
let initial_values = if let Some(defaults_path) = defaults {
|
||||
// Load default values from JSON, TOML, or .ncl file if provided
|
||||
let initial_values = if let Some(defaults_path_input) = defaults {
|
||||
// Resolve defaults path with cascading search
|
||||
let defaults_path = resolve_file_path(&defaults_path_input, base_dir, false);
|
||||
use typedialog_core::nickel::NickelCli;
|
||||
|
||||
let is_ncl = defaults_path.extension().and_then(|s| s.to_str()) == Some("ncl");
|
||||
let extension = defaults_path.extension().and_then(|s| s.to_str());
|
||||
let is_ncl = extension == Some("ncl");
|
||||
let is_toml = extension == Some("toml");
|
||||
|
||||
let defaults_json: std::collections::HashMap<String, serde_json::Value> = if is_ncl {
|
||||
// Convert .ncl to JSON using nickel export
|
||||
@ -286,6 +352,43 @@ async fn execute_form(
|
||||
))
|
||||
}
|
||||
}
|
||||
} else if is_toml {
|
||||
// Read TOML and convert to JSON
|
||||
let defaults_content = fs::read_to_string(&defaults_path).map_err(|e| {
|
||||
Error::validation_failed(format!("Failed to read defaults file: {}", e))
|
||||
})?;
|
||||
let toml_value: toml::Value = toml::from_str(&defaults_content).map_err(|e| {
|
||||
Error::validation_failed(format!("Failed to parse defaults TOML: {}", e))
|
||||
})?;
|
||||
// Convert TOML to JSON
|
||||
let parsed: serde_json::Value = serde_json::to_value(&toml_value).map_err(|e| {
|
||||
Error::validation_failed(format!("Failed to convert TOML to JSON: {}", e))
|
||||
})?;
|
||||
match parsed {
|
||||
serde_json::Value::Object(map) => {
|
||||
// Extract fields from elements (which includes fragments after expand_includes)
|
||||
let form_fields: Vec<form_parser::FieldDefinition> = form
|
||||
.elements
|
||||
.iter()
|
||||
.filter_map(|elem| match elem {
|
||||
form_parser::FormElement::Field(f) => Some(f.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut combined = extract_nickel_defaults(&map, &form_fields);
|
||||
let flattened = flatten_json_object(&map);
|
||||
for (k, v) in flattened {
|
||||
combined.entry(k).or_insert(v);
|
||||
}
|
||||
combined
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::validation_failed(
|
||||
"Defaults TOML must be a table/object".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Read JSON directly - combine extraction and flatten
|
||||
let defaults_content = fs::read_to_string(&defaults_path).map_err(|e| {
|
||||
@ -377,8 +480,10 @@ async fn execute_form(
|
||||
};
|
||||
|
||||
// Save results to output file if specified
|
||||
if let Some(out_path) = output {
|
||||
let output_str = helpers::format_results(&results, format)?;
|
||||
if let Some(output_path_input) = output {
|
||||
// Resolve output path with cascading search (will create if doesn't exist)
|
||||
let out_path = resolve_file_path(&output_path_input, base_dir, true);
|
||||
let output_str = helpers::format_results(&results, &actual_format)?;
|
||||
fs::write(&out_path, output_str)
|
||||
.map_err(|e| Error::validation_failed(format!("Failed to write output file: {}", e)))?;
|
||||
eprintln!("[web] Results saved to: {}", out_path.display());
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use typedialog_core::backends::BackendFactory;
|
||||
use typedialog_core::config::TypeDialogConfig;
|
||||
use typedialog_core::i18n::{I18nBundle, LocaleLoader, LocaleResolver};
|
||||
@ -12,6 +12,63 @@ use unic_langid::LanguageIdentifier;
|
||||
|
||||
use super::helpers::{extract_nickel_defaults, flatten_json_object, print_results};
|
||||
|
||||
/// Detect output format from filename extension
|
||||
///
|
||||
/// Returns the format string based on file extension:
|
||||
/// - `.toml` → "toml"
|
||||
/// - `.yaml` or `.yml` → "yaml"
|
||||
/// - `.json` → "json"
|
||||
/// - `.txt` → "text"
|
||||
/// - default → provided format
|
||||
fn detect_output_format(output_path: Option<&PathBuf>, default_format: &str) -> String {
|
||||
output_path
|
||||
.and_then(|p| p.extension())
|
||||
.and_then(|ext| ext.to_str())
|
||||
.and_then(|ext| match ext.to_lowercase().as_str() {
|
||||
"toml" => Some("toml"),
|
||||
"yaml" | "yml" => Some("yaml"),
|
||||
"json" => Some("json"),
|
||||
"txt" => Some("text"),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(default_format)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Resolve a file path with cascading search relative to form directory
|
||||
///
|
||||
/// Search order:
|
||||
/// 1. If absolute path → use directly
|
||||
/// 2. If path exists as-is (relative to cwd) → use it
|
||||
/// 3. Try relative to form's directory → use if exists
|
||||
/// 4. For output files → prefer form's directory
|
||||
/// 5. For input files → return original (will fail with clear error)
|
||||
fn resolve_file_path(path: &Path, form_base_dir: &Path, is_output: bool) -> PathBuf {
|
||||
// 1. If absolute, use it directly
|
||||
if path.is_absolute() {
|
||||
return path.to_path_buf();
|
||||
}
|
||||
|
||||
// 2. Check if path exists as-is (relative to cwd)
|
||||
if path.exists() {
|
||||
return path.to_path_buf();
|
||||
}
|
||||
|
||||
// 3. Try relative to form's directory
|
||||
let form_relative = form_base_dir.join(path);
|
||||
if form_relative.exists() {
|
||||
return form_relative;
|
||||
}
|
||||
|
||||
// 4. For output files, prefer form directory even if doesn't exist yet
|
||||
if is_output {
|
||||
return form_relative;
|
||||
}
|
||||
|
||||
// 5. Return original path (will fail with clear error for input files)
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute_form(
|
||||
config: PathBuf,
|
||||
@ -32,10 +89,18 @@ pub async fn execute_form(
|
||||
let form = form_parser::parse_toml(&toml_content)?;
|
||||
let base_dir = config.parent().unwrap_or_else(|| std::path::Path::new("."));
|
||||
|
||||
// Load default values from JSON or .ncl file if provided
|
||||
let initial_values = if let Some(defaults_path) = defaults {
|
||||
// Auto-detect format from output filename if not explicitly specified
|
||||
let actual_format = detect_output_format(output_file.as_ref(), format);
|
||||
|
||||
// Load default values from JSON, TOML, or .ncl file if provided
|
||||
let initial_values = if let Some(defaults_path_input) = defaults {
|
||||
// Resolve defaults path with cascading search
|
||||
let defaults_path = resolve_file_path(&defaults_path_input, base_dir, false);
|
||||
NickelCli::verify()?;
|
||||
let is_ncl = defaults_path.extension().and_then(|s| s.to_str()) == Some("ncl");
|
||||
|
||||
let extension = defaults_path.extension().and_then(|s| s.to_str());
|
||||
let is_ncl = extension == Some("ncl");
|
||||
let is_toml = extension == Some("toml");
|
||||
|
||||
let defaults_json: HashMap<String, serde_json::Value> = if is_ncl {
|
||||
let value = NickelCli::export(&defaults_path)?;
|
||||
@ -55,7 +120,36 @@ pub async fn execute_form(
|
||||
))
|
||||
}
|
||||
}
|
||||
} else if is_toml {
|
||||
// Read TOML and convert to JSON
|
||||
let defaults_content = fs::read_to_string(&defaults_path).map_err(|e| {
|
||||
Error::validation_failed(format!("Failed to read defaults file: {}", e))
|
||||
})?;
|
||||
let toml_value: toml::Value = toml::from_str(&defaults_content).map_err(|e| {
|
||||
Error::validation_failed(format!("Failed to parse defaults TOML: {}", e))
|
||||
})?;
|
||||
// Convert TOML to JSON
|
||||
let parsed: serde_json::Value = serde_json::to_value(&toml_value).map_err(|e| {
|
||||
Error::validation_failed(format!("Failed to convert TOML to JSON: {}", e))
|
||||
})?;
|
||||
match parsed {
|
||||
serde_json::Value::Object(map) => {
|
||||
let extracted = extract_nickel_defaults(&map, &form.fields);
|
||||
let flattened = flatten_json_object(&map);
|
||||
let mut combined = extracted;
|
||||
for (k, v) in flattened {
|
||||
combined.entry(k).or_insert(v);
|
||||
}
|
||||
combined
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::validation_failed(
|
||||
"Defaults TOML must be a table/object".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Read JSON directly
|
||||
let defaults_content = fs::read_to_string(&defaults_path).map_err(|e| {
|
||||
Error::validation_failed(format!("Failed to read defaults file: {}", e))
|
||||
})?;
|
||||
@ -144,8 +238,10 @@ pub async fn execute_form(
|
||||
let mut engine = TemplateEngine::new();
|
||||
let nickel_output = engine.render_file(template_path.as_path(), &results, None)?;
|
||||
|
||||
if let Some(path) = output_file {
|
||||
fs::write(path, &nickel_output).map_err(Error::io)?;
|
||||
if let Some(output_path_input) = output_file {
|
||||
// Resolve output path with cascading search
|
||||
let path = resolve_file_path(output_path_input, base_dir, true);
|
||||
fs::write(&path, &nickel_output).map_err(Error::io)?;
|
||||
} else {
|
||||
println!("{}", nickel_output);
|
||||
}
|
||||
@ -174,7 +270,7 @@ pub async fn execute_form(
|
||||
let config = TypeDialogConfig::default();
|
||||
print_results(
|
||||
&results,
|
||||
format,
|
||||
&actual_format,
|
||||
output_file,
|
||||
&form_fields,
|
||||
&encryption_context,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user