chore: reformat

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Langley
2026-03-31 00:07:22 -07:00
parent 37584670eb
commit 183b2350f7
17 changed files with 1112 additions and 471 deletions

View File

@ -1,5 +1,5 @@
use std::collections::HashSet;
use serde_json::Value;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq)]
pub enum FieldKind {
@ -51,73 +51,76 @@ pub fn analyze_records(records: &[Value]) -> Vec<FieldProposal> {
}
}
fields.into_iter().map(|field| {
let values: Vec<&Value> = records.iter()
.filter_map(|r| r.get(&field))
.collect();
fields
.into_iter()
.map(|field| {
let values: Vec<&Value> = records.iter().filter_map(|r| r.get(&field)).collect();
let all_numeric = values.iter().all(|v| v.is_number());
let all_string = values.iter().all(|v| v.is_string());
let all_numeric = values.iter().all(|v| v.is_number());
let all_string = values.iter().all(|v| v.is_string());
if all_numeric {
return FieldProposal {
field,
kind: FieldKind::Measure,
distinct_values: vec![],
accepted: true,
};
}
if all_string {
let distinct: HashSet<&str> = values.iter()
.filter_map(|v| v.as_str())
.collect();
let distinct_vec: Vec<String> = distinct.into_iter().map(String::from).collect();
let n = distinct_vec.len();
let _total = values.len();
// Check if looks like date
let looks_like_date = distinct_vec.iter().any(|s| {
s.contains('-') && s.len() >= 8
|| s.starts_with("Q") && s.len() == 2
|| ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
.iter().any(|m| s.starts_with(m))
});
if looks_like_date {
if all_numeric {
return FieldProposal {
field,
kind: FieldKind::TimeCategory,
distinct_values: distinct_vec,
kind: FieldKind::Measure,
distinct_values: vec![],
accepted: true,
};
}
if n <= CATEGORY_THRESHOLD {
if all_string {
let distinct: HashSet<&str> = values.iter().filter_map(|v| v.as_str()).collect();
let distinct_vec: Vec<String> = distinct.into_iter().map(String::from).collect();
let n = distinct_vec.len();
let _total = values.len();
// Check if looks like date
let looks_like_date = distinct_vec.iter().any(|s| {
s.contains('-') && s.len() >= 8
|| s.starts_with("Q") && s.len() == 2
|| [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct",
"Nov", "Dec",
]
.iter()
.any(|m| s.starts_with(m))
});
if looks_like_date {
return FieldProposal {
field,
kind: FieldKind::TimeCategory,
distinct_values: distinct_vec,
accepted: true,
};
}
if n <= CATEGORY_THRESHOLD {
return FieldProposal {
field,
kind: FieldKind::Category,
distinct_values: distinct_vec,
accepted: true,
};
}
return FieldProposal {
field,
kind: FieldKind::Category,
kind: FieldKind::Label,
distinct_values: distinct_vec,
accepted: true,
accepted: false,
};
}
return FieldProposal {
// Mixed or other: treat as label
FieldProposal {
field,
kind: FieldKind::Label,
distinct_values: distinct_vec,
distinct_values: vec![],
accepted: false,
};
}
// Mixed or other: treat as label
FieldProposal {
field,
kind: FieldKind::Label,
distinct_values: vec![],
accepted: false,
}
}).collect()
}
})
.collect()
}
/// Extract nested array from JSON by dot-path

View File

@ -1,3 +1,2 @@
pub mod wizard;
pub mod analyzer;
pub mod wizard;

View File

@ -1,9 +1,11 @@
use serde_json::Value;
use anyhow::{anyhow, Result};
use serde_json::Value;
use super::analyzer::{FieldKind, FieldProposal, analyze_records, extract_array_at_path, find_array_paths};
use crate::model::Model;
use super::analyzer::{
analyze_records, extract_array_at_path, find_array_paths, FieldKind, FieldProposal,
};
use crate::model::cell::{CellKey, CellValue};
use crate::model::Model;
// ── Pipeline (no UI state) ────────────────────────────────────────────────────
@ -75,10 +77,16 @@ impl ImportPipeline {
/// Build a Model from the current proposals. Pure — no side effects.
pub fn build_model(&self) -> Result<Model> {
let categories: Vec<&FieldProposal> = self.proposals.iter()
.filter(|p| p.accepted && matches!(p.kind, FieldKind::Category | FieldKind::TimeCategory))
let categories: Vec<&FieldProposal> = self
.proposals
.iter()
.filter(|p| {
p.accepted && matches!(p.kind, FieldKind::Category | FieldKind::TimeCategory)
})
.collect();
let measures: Vec<&FieldProposal> = self.proposals.iter()
let measures: Vec<&FieldProposal> = self
.proposals
.iter()
.filter(|p| p.accepted && p.kind == FieldKind::Measure)
.collect();
@ -112,7 +120,8 @@ impl ImportPipeline {
let mut valid = true;
for cat_proposal in &categories {
let val = map.get(&cat_proposal.field)
let val = map
.get(&cat_proposal.field)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| map.get(&cat_proposal.field).map(|v| v.to_string()));
@ -128,7 +137,9 @@ impl ImportPipeline {
}
}
if !valid { continue; }
if !valid {
continue;
}
for measure in &measures {
if let Some(val) = map.get(&measure.field).and_then(|v| v.as_f64()) {
@ -180,7 +191,12 @@ impl ImportWizard {
WizardStep::ReviewProposals
};
Self { pipeline, step, cursor: 0, message: None }
Self {
pipeline,
step,
cursor: 0,
message: None,
}
}
// ── Step transitions ──────────────────────────────────────────────────────
@ -219,7 +235,9 @@ impl ImportWizard {
WizardStep::ReviewProposals => self.pipeline.proposals.len(),
_ => 0,
};
if len == 0 { return; }
if len == 0 {
return;
}
if delta > 0 {
self.cursor = (self.cursor + 1).min(len - 1);
} else if self.cursor > 0 {
@ -240,18 +258,22 @@ impl ImportWizard {
if self.cursor < self.pipeline.proposals.len() {
let p = &mut self.pipeline.proposals[self.cursor];
p.kind = match p.kind {
FieldKind::Category => FieldKind::Measure,
FieldKind::Measure => FieldKind::TimeCategory,
FieldKind::Category => FieldKind::Measure,
FieldKind::Measure => FieldKind::TimeCategory,
FieldKind::TimeCategory => FieldKind::Label,
FieldKind::Label => FieldKind::Category,
FieldKind::Label => FieldKind::Category,
};
}
}
// ── Model name input ──────────────────────────────────────────────────────
pub fn push_name_char(&mut self, c: char) { self.pipeline.model_name.push(c); }
pub fn pop_name_char(&mut self) { self.pipeline.model_name.pop(); }
pub fn push_name_char(&mut self, c: char) {
self.pipeline.model_name.push(c);
}
pub fn pop_name_char(&mut self) {
self.pipeline.model_name.pop();
}
// ── Delegate build to pipeline ────────────────────────────────────────────
@ -262,9 +284,9 @@ impl ImportWizard {
#[cfg(test)]
mod tests {
use serde_json::json;
use super::ImportPipeline;
use crate::import::analyzer::FieldKind;
use serde_json::json;
#[test]
fn flat_array_auto_selected() {
@ -337,7 +359,9 @@ mod tests {
fn build_model_fails_with_no_accepted_categories() {
let raw = json!([{"revenue": 100.0, "cost": 50.0}]);
let mut p = ImportPipeline::new(raw);
for prop in &mut p.proposals { prop.accepted = false; }
for prop in &mut p.proposals {
prop.accepted = false;
}
assert!(p.build_model().is_err());
}
@ -364,14 +388,20 @@ mod tests {
use crate::model::cell::CellKey;
let k_east = CellKey::new(vec![
("Measure".to_string(), "revenue".to_string()),
("region".to_string(), "East".to_string()),
("region".to_string(), "East".to_string()),
]);
let k_west = CellKey::new(vec![
("Measure".to_string(), "revenue".to_string()),
("region".to_string(), "West".to_string()),
("region".to_string(), "West".to_string()),
]);
assert_eq!(model.get_cell(&k_east).and_then(|v| v.as_f64()), Some(100.0));
assert_eq!(model.get_cell(&k_west).and_then(|v| v.as_f64()), Some(200.0));
assert_eq!(
model.get_cell(&k_east).and_then(|v| v.as_f64()),
Some(100.0)
);
assert_eq!(
model.get_cell(&k_west).and_then(|v| v.as_f64()),
Some(200.0)
);
}
#[test]