252 lines
8.8 KiB
Rust
252 lines
8.8 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use super::types::{CellValueArg, Command, CommandResult};
|
|
use crate::formula::parse_formula;
|
|
use crate::import::analyzer::{analyze_records, extract_array_at_path, FieldKind};
|
|
use crate::model::cell::{CellKey, CellValue};
|
|
use crate::model::Model;
|
|
use crate::persistence;
|
|
|
|
/// Execute a command against the model, returning a result.
|
|
/// This is the single authoritative mutation path used by both the TUI and headless modes.
|
|
pub fn dispatch(model: &mut Model, cmd: &Command) -> CommandResult {
|
|
match cmd {
|
|
Command::AddCategory { name } => match model.add_category(name) {
|
|
Ok(_) => CommandResult::ok_msg(format!("Category '{name}' added")),
|
|
Err(e) => CommandResult::err(e.to_string()),
|
|
},
|
|
|
|
Command::AddItem { category, item } => match model.category_mut(category) {
|
|
Some(cat) => {
|
|
cat.add_item(item);
|
|
CommandResult::ok()
|
|
}
|
|
None => CommandResult::err(format!("Category '{category}' not found")),
|
|
},
|
|
|
|
Command::AddItemInGroup {
|
|
category,
|
|
item,
|
|
group,
|
|
} => match model.category_mut(category) {
|
|
Some(cat) => {
|
|
cat.add_item_in_group(item, group);
|
|
CommandResult::ok()
|
|
}
|
|
None => CommandResult::err(format!("Category '{category}' not found")),
|
|
},
|
|
|
|
Command::SetCell { coords, value } => {
|
|
let kv: Vec<(String, String)> = coords
|
|
.iter()
|
|
.map(|pair| (pair[0].clone(), pair[1].clone()))
|
|
.collect();
|
|
// Validate all categories exist before mutating anything
|
|
for (cat_name, _) in &kv {
|
|
if model.category(cat_name).is_none() {
|
|
return CommandResult::err(format!("Category '{cat_name}' not found"));
|
|
}
|
|
}
|
|
// Ensure items exist within their categories
|
|
for (cat_name, item_name) in &kv {
|
|
model.category_mut(cat_name).unwrap().add_item(item_name);
|
|
}
|
|
let key = CellKey::new(kv);
|
|
let cell_value = match value {
|
|
CellValueArg::Number { number } => CellValue::Number(*number),
|
|
CellValueArg::Text { text } => CellValue::Text(text.clone()),
|
|
};
|
|
model.set_cell(key, cell_value);
|
|
CommandResult::ok()
|
|
}
|
|
|
|
Command::ClearCell { coords } => {
|
|
let kv: Vec<(String, String)> = coords
|
|
.iter()
|
|
.map(|pair| (pair[0].clone(), pair[1].clone()))
|
|
.collect();
|
|
let key = CellKey::new(kv);
|
|
model.clear_cell(&key);
|
|
CommandResult::ok()
|
|
}
|
|
|
|
Command::AddFormula {
|
|
raw,
|
|
target_category,
|
|
} => {
|
|
match parse_formula(raw, target_category) {
|
|
Ok(formula) => {
|
|
// Ensure the target item exists in the target category
|
|
let target = formula.target.clone();
|
|
let cat_name = formula.target_category.clone();
|
|
if let Some(cat) = model.category_mut(&cat_name) {
|
|
cat.add_item(&target);
|
|
}
|
|
model.add_formula(formula);
|
|
CommandResult::ok_msg(format!("Formula '{raw}' added"))
|
|
}
|
|
Err(e) => CommandResult::err(format!("Parse error: {e}")),
|
|
}
|
|
}
|
|
|
|
Command::RemoveFormula {
|
|
target,
|
|
target_category,
|
|
} => {
|
|
model.remove_formula(target, target_category);
|
|
CommandResult::ok()
|
|
}
|
|
|
|
Command::CreateView { name } => {
|
|
model.create_view(name);
|
|
CommandResult::ok()
|
|
}
|
|
|
|
Command::DeleteView { name } => match model.delete_view(name) {
|
|
Ok(_) => CommandResult::ok(),
|
|
Err(e) => CommandResult::err(e.to_string()),
|
|
},
|
|
|
|
Command::SwitchView { name } => match model.switch_view(name) {
|
|
Ok(_) => CommandResult::ok(),
|
|
Err(e) => CommandResult::err(e.to_string()),
|
|
},
|
|
|
|
Command::SetAxis { category, axis } => {
|
|
model.active_view_mut().set_axis(category, *axis);
|
|
CommandResult::ok()
|
|
}
|
|
|
|
Command::SetPageSelection { category, item } => {
|
|
model.active_view_mut().set_page_selection(category, item);
|
|
CommandResult::ok()
|
|
}
|
|
|
|
Command::ToggleGroup { category, group } => {
|
|
model
|
|
.active_view_mut()
|
|
.toggle_group_collapse(category, group);
|
|
CommandResult::ok()
|
|
}
|
|
|
|
Command::HideItem { category, item } => {
|
|
model.active_view_mut().hide_item(category, item);
|
|
CommandResult::ok()
|
|
}
|
|
|
|
Command::ShowItem { category, item } => {
|
|
model.active_view_mut().show_item(category, item);
|
|
CommandResult::ok()
|
|
}
|
|
|
|
Command::Save { path } => match persistence::save(model, std::path::Path::new(path)) {
|
|
Ok(_) => CommandResult::ok_msg(format!("Saved to {path}")),
|
|
Err(e) => CommandResult::err(e.to_string()),
|
|
},
|
|
|
|
Command::Load { path } => match persistence::load(std::path::Path::new(path)) {
|
|
Ok(mut loaded) => {
|
|
loaded.normalize_view_state();
|
|
*model = loaded;
|
|
CommandResult::ok_msg(format!("Loaded from {path}"))
|
|
}
|
|
Err(e) => CommandResult::err(e.to_string()),
|
|
},
|
|
|
|
Command::ExportCsv { path } => {
|
|
let view_name = model.active_view.clone();
|
|
match persistence::export_csv(model, &view_name, std::path::Path::new(path)) {
|
|
Ok(_) => CommandResult::ok_msg(format!("Exported to {path}")),
|
|
Err(e) => CommandResult::err(e.to_string()),
|
|
}
|
|
}
|
|
|
|
Command::ImportJson {
|
|
path,
|
|
model_name,
|
|
array_path,
|
|
} => import_headless(model, path, model_name.as_deref(), array_path.as_deref()),
|
|
}
|
|
}
|
|
|
|
fn import_headless(
|
|
model: &mut Model,
|
|
path: &PathBuf,
|
|
model_name: Option<&str>,
|
|
array_path: Option<&str>,
|
|
) -> CommandResult {
|
|
let is_csv = path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("csv"));
|
|
|
|
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 {
|
|
// Parse JSON file
|
|
let content = match std::fs::read_to_string(path) {
|
|
Ok(c) => c,
|
|
Err(e) => return CommandResult::err(format!("Cannot read '{}': {e}", path.display())),
|
|
};
|
|
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(format!("No array at path '{ap}'")),
|
|
}
|
|
} else if let Some(arr) = value.as_array() {
|
|
arr.clone()
|
|
} else {
|
|
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);
|
|
|
|
// 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,
|
|
array_paths: vec![],
|
|
selected_path: array_path.unwrap_or("").to_string(),
|
|
records,
|
|
proposals: proposals
|
|
.into_iter()
|
|
.map(|mut p| {
|
|
p.accepted = p.kind != FieldKind::Label;
|
|
p
|
|
})
|
|
.collect(),
|
|
model_name: model_name.unwrap_or("Imported Model").to_string(),
|
|
};
|
|
|
|
match pipeline.build_model() {
|
|
Ok(new_model) => {
|
|
*model = new_model;
|
|
CommandResult::ok_msg("Imported successfully")
|
|
}
|
|
Err(e) => CommandResult::err(e.to_string()),
|
|
}
|
|
}
|