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:
@ -163,50 +163,69 @@ pub fn dispatch(model: &mut Model, cmd: &Command) -> CommandResult {
|
||||
path,
|
||||
model_name,
|
||||
array_path,
|
||||
} => import_json_headless(model, path, model_name.as_deref(), array_path.as_deref()),
|
||||
} => import_headless(model, path, model_name.as_deref(), array_path.as_deref()),
|
||||
}
|
||||
}
|
||||
|
||||
fn import_json_headless(
|
||||
fn import_headless(
|
||||
model: &mut Model,
|
||||
path: &str,
|
||||
model_name: Option<&str>,
|
||||
array_path: Option<&str>,
|
||||
) -> CommandResult {
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return CommandResult::err(format!("Cannot read '{path}': {e}")),
|
||||
};
|
||||
let value: serde_json::Value = match serde_json::from_str(&content) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return CommandResult::err(format!("JSON parse error: {e}")),
|
||||
};
|
||||
let is_csv = path.ends_with(".csv");
|
||||
|
||||
let records = if let Some(ap) = array_path.filter(|s| !s.is_empty()) {
|
||||
match extract_array_at_path(&value, ap) {
|
||||
Some(arr) => arr.clone(),
|
||||
None => return CommandResult::err(format!("No array at path '{ap}'")),
|
||||
let records = if is_csv {
|
||||
// Parse CSV file
|
||||
match crate::import::csv_parser::parse_csv(path) {
|
||||
Ok(recs) => recs,
|
||||
Err(e) => return CommandResult::err(e.to_string()),
|
||||
}
|
||||
} else if let Some(arr) = value.as_array() {
|
||||
arr.clone()
|
||||
} else {
|
||||
// Find first array
|
||||
let paths = crate::import::analyzer::find_array_paths(&value);
|
||||
if let Some(first) = paths.first() {
|
||||
match extract_array_at_path(&value, first) {
|
||||
// Parse JSON file
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return CommandResult::err(format!("Cannot read '{path}': {e}")),
|
||||
};
|
||||
let value: serde_json::Value = match serde_json::from_str(&content) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return CommandResult::err(format!("JSON parse error: {e}")),
|
||||
};
|
||||
|
||||
if let Some(ap) = array_path.filter(|s| !s.is_empty()) {
|
||||
match extract_array_at_path(&value, ap) {
|
||||
Some(arr) => arr.clone(),
|
||||
None => return CommandResult::err("Could not extract records array"),
|
||||
None => return CommandResult::err(format!("No array at path '{ap}'")),
|
||||
}
|
||||
} else if let Some(arr) = value.as_array() {
|
||||
arr.clone()
|
||||
} else {
|
||||
return CommandResult::err("No array found in JSON");
|
||||
let paths = crate::import::analyzer::find_array_paths(&value);
|
||||
if let Some(first) = paths.first() {
|
||||
match extract_array_at_path(&value, first) {
|
||||
Some(arr) => arr.clone(),
|
||||
None => return CommandResult::err("Could not extract records array"),
|
||||
}
|
||||
} else {
|
||||
return CommandResult::err("No array found in JSON");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let proposals = analyze_records(&records);
|
||||
|
||||
// Auto-accept all and build via ImportPipeline
|
||||
// Build via ImportPipeline
|
||||
let raw = if is_csv {
|
||||
serde_json::Value::Array(records.clone())
|
||||
} else {
|
||||
// For JSON, we need the original parsed value
|
||||
// Re-read and parse to get it (or pass it up from above)
|
||||
serde_json::from_str(&std::fs::read_to_string(path).unwrap_or_default())
|
||||
.unwrap_or(serde_json::Value::Array(records.clone()))
|
||||
};
|
||||
|
||||
let pipeline = crate::import::wizard::ImportPipeline {
|
||||
raw: value,
|
||||
raw,
|
||||
array_paths: vec![],
|
||||
selected_path: array_path.unwrap_or("").to_string(),
|
||||
records,
|
||||
@ -223,7 +242,7 @@ fn import_json_headless(
|
||||
match pipeline.build_model() {
|
||||
Ok(new_model) => {
|
||||
*model = new_model;
|
||||
CommandResult::ok_msg("JSON imported successfully")
|
||||
CommandResult::ok_msg("Imported successfully")
|
||||
}
|
||||
Err(e) => CommandResult::err(e.to_string()),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user