Add CSV import functionality

- Use csv crate for robust CSV parsing (handles quoted fields, empty values, \r\n)
- Extend --import command to auto-detect format by file extension (.csv or .json)
- Reuse existing ImportPipeline and analyzer for field type detection
- Categories detected automatically (string fields), measures for numeric fields
- Updated help text and welcome screen to mention CSV support

All 201 tests pass.
This commit is contained in:
Edward Langley
2026-04-01 01:32:19 -07:00
parent 2cf1123bcb
commit 23e26f0e06
6 changed files with 256 additions and 38 deletions

View File

@ -54,26 +54,40 @@ impl Runnable for CmdLineArgs {
// Load or create model
let model = get_initial_model(&self.file_path)?;
// Pre-TUI import: parse JSON and open wizard
let import_json = if let Some(ref path) = self.import_path {
// Pre-TUI import: parse JSON or CSV and open wizard
let import_value = if let Some(ref path) = self.import_path {
match std::fs::read_to_string(path) {
Err(e) => {
eprintln!("Cannot read '{}': {e}", path.display());
return Ok(());
}
Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
Err(e) => {
eprintln!("JSON parse error: {e}");
return Ok(());
Ok(content) => {
if path.to_string_lossy().ends_with(".csv") {
// Parse CSV and wrap as JSON array
match crate::import::csv_parser::parse_csv(&path.to_string_lossy()) {
Ok(records) => Some(serde_json::Value::Array(records)),
Err(e) => {
eprintln!("CSV parse error: {e}");
return Ok(());
}
}
} else {
// Parse JSON
match serde_json::from_str::<serde_json::Value>(&content) {
Err(e) => {
eprintln!("JSON parse error: {e}");
return Ok(());
}
Ok(json) => Some(json),
}
}
Ok(json) => Some(json),
},
}
}
} else {
None
};
run_tui(model, self.file_path, import_json)
run_tui(model, self.file_path, import_value)
}
}
@ -130,7 +144,7 @@ impl Runnable for HelpArgs {
println!("improvise — multi-dimensional data modeling TUI\n");
println!("USAGE:");
println!(" improvise [file.improv] Open or create a model");
println!(" improvise --import data.json Import JSON then open TUI");
println!(" improvise --import data.json Import JSON (or CSV) then open TUI");
println!(" improvise --cmd '{{...}}' Run a JSON command (headless, repeatable)");
println!(" improvise --script cmds.jsonl Run commands from file (headless)");
println!("\nTUI KEYS (vim-style):");
@ -246,13 +260,13 @@ impl<'a> Drop for TuiContext<'a> {
fn run_tui(
model: Model,
file_path: Option<PathBuf>,
import_json: Option<serde_json::Value>,
import_value: Option<serde_json::Value>,
) -> Result<()> {
let mut stdout = io::stdout();
let mut tui_context = TuiContext::enter(&mut stdout)?;
let mut app = App::new(model, file_path);
if let Some(json) = import_json {
if let Some(json) = import_value {
app.start_import_wizard(json);
}
@ -518,7 +532,7 @@ fn draw_welcome(f: &mut Frame, area: Rect) {
),
("", Style::default()),
(
":import <file.json> Import a JSON file",
":import <file> Import JSON or CSV file",
Style::default().fg(Color::Cyan),
),
(