1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
8#![cfg_attr(coverage_nightly, allow(unused_features))]
11
12pub mod extfn_def;
13pub mod formula;
14pub mod functions;
15pub mod ids;
16pub mod locale;
17pub mod parser;
18pub mod schedule;
19pub mod value;
20
21pub use extfn_def::{ExternalFn, Language};
22pub use formula::{BinaryOp, DimensionSpec, Expr, Formula, FuncId, UnaryOp};
23pub use ids::{CategoryId, ItemId, MeasureId, Name, ScenarioId, ViewId};
24pub use locale::Locale;
25pub use parser::{
26 parse_definition, parse_expr, parse_formula, Definition, FormulaText, LambdaText, ParseError,
27};
28pub use value::{Value, ValueError, ValueErrorKind, ValueType};
29
30use serde::{Deserialize, Serialize};
31use std::collections::{BTreeMap, BTreeSet, HashMap};
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct Category {
36 pub id: CategoryId,
37 pub name: Name,
38 pub items: Vec<ItemId>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub struct Item {
44 pub id: ItemId,
45 pub category: CategoryId,
46 pub name: Name,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
54pub struct Coordinate {
55 pub dims: BTreeMap<CategoryId, ItemId>,
56}
57
58impl Coordinate {
59 pub fn new() -> Self {
60 Coordinate {
61 dims: BTreeMap::new(),
62 }
63 }
64
65 pub fn from_pairs(pairs: impl IntoIterator<Item = (CategoryId, ItemId)>) -> Self {
66 Coordinate {
67 dims: pairs.into_iter().collect(),
68 }
69 }
70
71 pub fn get(&self, cat: CategoryId) -> Option<ItemId> {
72 self.dims.get(&cat).copied()
73 }
74
75 pub fn with(mut self, cat: CategoryId, item: ItemId) -> Self {
76 self.dims.insert(cat, item);
77 self
78 }
79
80 pub fn categories(&self) -> impl Iterator<Item = CategoryId> + '_ {
82 self.dims.keys().copied()
83 }
84}
85
86impl Default for Coordinate {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94pub enum MeasureKind {
95 Input,
96 Derived(Formula),
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub struct Measure {
102 pub id: MeasureId,
103 pub name: Name,
104 pub value_type: ValueType,
105 pub categories: Vec<CategoryId>,
107 pub kind: MeasureKind,
108 pub description: Option<String>,
109}
110
111impl Measure {
112 pub fn is_input(&self) -> bool {
113 matches!(self.kind, MeasureKind::Input)
114 }
115 pub fn is_derived(&self) -> bool {
116 matches!(self.kind, MeasureKind::Derived(_))
117 }
118}
119
120#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
122pub struct Model {
123 pub categories: HashMap<CategoryId, Category>,
124 pub items: HashMap<ItemId, Item>,
125 pub measures: HashMap<MeasureId, Measure>,
126 #[serde(with = "inputs_as_seq")]
132 pub inputs: HashMap<(MeasureId, Coordinate), Value>,
133 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
140 pub sql_sources: HashMap<MeasureId, SqlSource>,
141 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
145 pub views: HashMap<ViewId, View>,
146 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
151 pub external_fns: HashMap<String, ExternalFn>,
152 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
158 pub external_calls: HashMap<MeasureId, ExternalCall>,
159 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
164 pub scenarios: HashMap<ScenarioId, Scenario>,
165 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
170 pub lambdas: BTreeMap<u32, NamedLambda>,
171}
172
173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180pub struct NamedLambda {
181 pub name: Name,
182 pub params: Vec<String>,
183 pub body: Expr,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum LambdaParam {
190 Value,
192 Reduced,
194 Category,
196}
197
198impl NamedLambda {
199 pub fn param_kinds(&self) -> Result<Vec<LambdaParam>, String> {
202 let mut uses = vec![ParamUse::default(); self.params.len()];
203 param_uses(&self.body, &mut uses);
204 self.params
205 .iter()
206 .zip(uses)
207 .map(|(p, u)| match u {
208 ParamUse {
209 value: true,
210 category: true,
211 ..
212 } => Err(format!(
213 "LAMBDA parameter {p} is used both as a value and as a category"
214 )),
215 ParamUse { category: true, .. } => Ok(LambdaParam::Category),
216 ParamUse { reduced: true, .. } => Ok(LambdaParam::Reduced),
217 _ => Ok(LambdaParam::Value),
218 })
219 .collect()
220 }
221}
222
223#[derive(Debug, Clone, Copy, Default)]
225struct ParamUse {
226 value: bool,
227 reduced: bool,
228 category: bool,
229}
230
231fn param_uses(e: &Expr, uses: &mut [ParamUse]) {
232 let cat = |c: &CategoryId, uses: &mut [ParamUse]| {
233 if let Some(u) = functions::lambda_category_of(*c).and_then(|i| uses.get_mut(i)) {
234 u.category = true;
235 }
236 };
237 match e {
238 Expr::Ref(id, s) => {
239 for c in s.by.iter().chain(&s.over).chain(&s.except) {
240 cat(c, uses);
241 }
242 if let Some(u) = functions::lambda_param_of(*id).and_then(|i| uses.get_mut(i)) {
243 u.value = true;
244 u.reduced |= *s != DimensionSpec::default();
245 }
246 }
247 Expr::Category(c) => cat(c, uses),
248 Expr::UnaryOp(_, a) => param_uses(a, uses),
249 Expr::BinaryOp(_, a, b) => {
250 param_uses(a, uses);
251 param_uses(b, uses);
252 }
253 Expr::Call(_, args) => args.iter().for_each(|a| param_uses(a, uses)),
254 Expr::Literal(_) => {}
255 }
256}
257
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
262pub struct Scenario {
263 pub id: ScenarioId,
264 pub name: Name,
265 #[serde(with = "inputs_as_seq")]
269 pub overrides: HashMap<(MeasureId, Coordinate), Value>,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
275pub struct ExternalCall {
276 pub func: String,
278 pub arg_measures: Vec<MeasureId>,
281 #[serde(default)]
284 pub refresh_policy: RefreshPolicy,
285}
286#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
294pub struct CanvasRect {
295 pub x: f32,
296 pub y: f32,
297 pub w: f32,
298 pub h: f32,
299}
300
301impl Default for CanvasRect {
302 fn default() -> Self {
303 CanvasRect {
306 x: 0.0,
307 y: 0.0,
308 w: 560.0,
309 h: 420.0,
310 }
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
319pub struct MatrixPlacement {
320 pub measure: MeasureId,
321 #[serde(default)]
323 pub axis_order: Vec<CategoryId>,
324 #[serde(default = "one")]
326 pub n_rows: usize,
327 #[serde(default = "one")]
329 pub n_cols: usize,
330 #[serde(default)]
332 pub page_items: Vec<(CategoryId, ItemId)>,
333 #[serde(default)]
335 pub filters: Vec<Filter>,
336 #[serde(default)]
338 pub rect: CanvasRect,
339}
340
341impl MatrixPlacement {
342 pub fn new(measure: MeasureId) -> Self {
344 MatrixPlacement {
345 measure,
346 axis_order: Vec::new(),
347 n_rows: 1,
348 n_cols: 1,
349 page_items: Vec::new(),
350 filters: Vec::new(),
351 rect: CanvasRect::default(),
352 }
353 }
354
355 pub fn allows(&self, category: CategoryId, item: ItemId) -> bool {
358 match self.filters.iter().find(|f| f.category == category) {
359 Some(f) => f.items.contains(&item),
360 None => true,
361 }
362 }
363}
364
365#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
381pub struct View {
382 pub id: ViewId,
383 pub name: Name,
384 pub measure: MeasureId,
386 #[serde(default)]
391 pub axis_order: Vec<CategoryId>,
392 #[serde(default = "one")]
394 pub n_rows: usize,
395 #[serde(default = "one")]
398 pub n_cols: usize,
399 #[serde(default)]
401 pub page_items: Vec<(CategoryId, ItemId)>,
402 #[serde(default)]
405 pub filters: Vec<Filter>,
406 #[serde(default)]
409 pub rect: CanvasRect,
410 #[serde(default)]
414 pub placements: Vec<MatrixPlacement>,
415}
416
417fn one() -> usize {
420 1
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
425pub struct Filter {
426 pub category: CategoryId,
427 pub items: Vec<ItemId>,
430}
431
432impl View {
433 pub fn from_matrices(
436 id: ViewId,
437 name: Name,
438 primary: MatrixPlacement,
439 extras: Vec<MatrixPlacement>,
440 ) -> Self {
441 View {
442 id,
443 name,
444 measure: primary.measure,
445 axis_order: primary.axis_order,
446 n_rows: primary.n_rows,
447 n_cols: primary.n_cols,
448 page_items: primary.page_items,
449 filters: primary.filters,
450 rect: primary.rect,
451 placements: extras,
452 }
453 }
454
455 pub fn primary(&self) -> MatrixPlacement {
458 MatrixPlacement {
459 measure: self.measure,
460 axis_order: self.axis_order.clone(),
461 n_rows: self.n_rows,
462 n_cols: self.n_cols,
463 page_items: self.page_items.clone(),
464 filters: self.filters.clone(),
465 rect: self.rect,
466 }
467 }
468
469 pub fn matrices(&self) -> Vec<MatrixPlacement> {
471 let mut v = Vec::with_capacity(1 + self.placements.len());
472 v.push(self.primary());
473 v.extend(self.placements.iter().cloned());
474 v
475 }
476
477 pub fn allows(&self, category: CategoryId, item: ItemId) -> bool {
485 self.primary().allows(category, item)
486 }
487}
488
489#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
494pub struct SqlSource {
495 pub query: String,
497 pub dimension_columns: Vec<String>,
500 pub value_column: String,
502 #[serde(default)]
507 pub refresh_policy: RefreshPolicy,
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
512#[serde(rename_all = "snake_case")]
513pub enum RefreshPolicy {
514 #[default]
516 Manual,
517 OnLoad,
519 Interval { secs: u64 },
521}
522
523fn substitute_params(body: &Expr, args: &[Expr]) -> Result<Expr, String> {
530 let cat = |c: &CategoryId| match functions::lambda_category_of(*c) {
531 None => Ok(*c),
532 Some(i) => match args.get(i) {
533 Some(Expr::Category(a)) => Ok(*a),
534 _ => Err(format!("argument {} must be a category", i + 1)),
535 },
536 };
537 let cats = |v: &[CategoryId]| v.iter().map(cat).collect::<Result<Vec<_>, String>>();
538 let sub = |e: &Expr| substitute_params(e, args);
539 Ok(match body {
540 Expr::Ref(id, spec) => {
541 let spec = DimensionSpec {
542 by: cats(&spec.by)?,
543 over: cats(&spec.over)?,
544 except: cats(&spec.except)?,
545 };
546 match functions::lambda_param_of(*id).and_then(|i| Some((i, args.get(i)?))) {
547 None => Expr::Ref(*id, spec),
548 Some((_, a)) if spec == DimensionSpec::default() => a.clone(),
549 Some((_, Expr::Ref(m, a))) => {
550 let or = |b: Vec<CategoryId>, a: &[CategoryId]| {
551 if b.is_empty() {
552 a.to_vec()
553 } else {
554 b
555 }
556 };
557 Expr::Ref(
558 *m,
559 DimensionSpec {
560 by: or(spec.by, &a.by),
561 over: or(spec.over, &a.over),
562 except: or(spec.except, &a.except),
563 },
564 )
565 }
566 Some((i, _)) => {
567 return Err(format!(
568 "argument {} is reduced over a category, so it must be a measure",
569 i + 1
570 ))
571 }
572 }
573 }
574 Expr::Category(c) => Expr::Category(cat(c)?),
575 Expr::Call(f, a) => Expr::Call(*f, a.iter().map(sub).collect::<Result<_, _>>()?),
576 Expr::UnaryOp(o, a) => Expr::UnaryOp(*o, Box::new(sub(a)?)),
577 Expr::BinaryOp(o, a, b) => Expr::BinaryOp(*o, Box::new(sub(a)?), Box::new(sub(b)?)),
578 Expr::Literal(_) => body.clone(),
579 })
580}
581
582fn calls_lambda(e: &Expr, ids: &BTreeSet<u32>) -> bool {
584 match e {
585 Expr::Call(f, a) => {
586 functions::named_lambda_of(*f).is_some_and(|i| ids.contains(&i))
587 || a.iter().any(|x| calls_lambda(x, ids))
588 }
589 Expr::UnaryOp(_, a) => calls_lambda(a, ids),
590 Expr::BinaryOp(_, a, b) => calls_lambda(a, ids) || calls_lambda(b, ids),
591 Expr::Literal(_) | Expr::Ref(..) | Expr::Category(_) => false,
592 }
593}
594
595mod inputs_as_seq {
596 use super::{Coordinate, MeasureId, Value};
597 use serde::{Deserialize, Deserializer, Serialize, Serializer};
598 use std::collections::HashMap;
599
600 type Map = HashMap<(MeasureId, Coordinate), Value>;
601
602 pub fn serialize<S: Serializer>(map: &Map, s: S) -> Result<S::Ok, S::Error> {
603 let v: Vec<(&(MeasureId, Coordinate), &Value)> = map.iter().collect();
604 v.serialize(s)
605 }
606
607 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Map, D::Error> {
608 let v: Vec<((MeasureId, Coordinate), Value)> = Vec::deserialize(d)?;
609 Ok(v.into_iter().collect())
610 }
611}
612
613impl Model {
614 pub fn new() -> Self {
615 Model::default()
616 }
617
618 pub fn with_provisional(&self, id: MeasureId, name: &str) -> std::borrow::Cow<'_, Model> {
622 if self.measure_by_name(name).is_some() {
623 return std::borrow::Cow::Borrowed(self);
624 }
625 let mut m = self.clone();
626 m.add_measure(Measure {
627 id,
628 name: Name(name.to_string()),
629 value_type: ValueType::Number,
630 categories: Vec::new(),
631 kind: MeasureKind::Input,
632 description: None,
633 });
634 std::borrow::Cow::Owned(m)
635 }
636
637 pub fn lambda_by_name(&self, name: &str) -> Option<(u32, &NamedLambda)> {
639 self.lambdas
640 .iter()
641 .find(|(_, l)| l.name.0.eq_ignore_ascii_case(name))
642 .map(|(id, l)| (*id, l))
643 }
644
645 pub fn lambda_users(&self, id: u32) -> (Vec<u32>, Vec<MeasureId>) {
648 let mut hit = BTreeSet::from([id]);
649 loop {
650 let before = hit.len();
651 for (lid, l) in &self.lambdas {
652 if !hit.contains(lid) && calls_lambda(&l.body, &hit) {
653 hit.insert(*lid);
654 }
655 }
656 if hit.len() == before {
657 break;
658 }
659 }
660 let measures = self
661 .measures
662 .values()
663 .filter(|m| matches!(&m.kind, MeasureKind::Derived(f) if calls_lambda(&f.expr, &hit)))
664 .map(|m| m.id)
665 .collect::<BTreeSet<_>>();
666 hit.remove(&id);
667 (hit.into_iter().collect(), measures.into_iter().collect())
668 }
669
670 pub fn expand_lambdas(&self, e: &Expr) -> Result<Expr, String> {
674 self.expand_at(e, 0)
675 }
676
677 fn expand_at(&self, e: &Expr, depth: usize) -> Result<Expr, String> {
678 if depth > self.lambdas.len() + 1 {
681 return Err("named LAMBDAs call each other in a cycle".into());
682 }
683 Ok(match e {
684 Expr::Call(f, args) => {
685 let args = args
686 .iter()
687 .map(|a| self.expand_at(a, depth))
688 .collect::<Result<Vec<_>, _>>()?;
689 match functions::named_lambda_of(*f) {
690 None => Expr::Call(*f, args),
691 Some(id) => {
692 let l = self
693 .lambdas
694 .get(&id)
695 .ok_or_else(|| format!("named LAMBDA {id} does not exist"))?;
696 if l.params.len() != args.len() {
697 return Err(format!(
698 "{} takes {} argument(s), got {}",
699 l.name.0,
700 l.params.len(),
701 args.len()
702 ));
703 }
704 let body = substitute_params(&l.body, &args)
705 .map_err(|e| format!("{}: {e}", l.name.0))?;
706 self.expand_at(&body, depth + 1)?
707 }
708 }
709 }
710 Expr::UnaryOp(o, a) => Expr::UnaryOp(*o, Box::new(self.expand_at(a, depth)?)),
711 Expr::BinaryOp(o, a, b) => Expr::BinaryOp(
712 *o,
713 Box::new(self.expand_at(a, depth)?),
714 Box::new(self.expand_at(b, depth)?),
715 ),
716 other => other.clone(),
717 })
718 }
719
720 pub fn load_lambdas(&mut self, rows: &[(u32, String)]) -> Result<(), String> {
726 let mut todo = Vec::new();
727 for (id, text) in rows {
728 match serde_json::from_str::<NamedLambda>(text) {
729 Ok(l) => {
730 self.lambdas.insert(*id, l);
731 }
732 Err(_) => todo.push((*id, text)),
733 }
734 }
735 while !todo.is_empty() {
736 let before = todo.len();
737 let mut err = None;
738 todo.retain(|(id, text)| match parse_definition(self, text) {
739 Ok(Definition::Lambda(lt)) => {
740 self.lambdas.insert(*id, lt.lambda);
741 false
742 }
743 Ok(_) => {
744 err = Some(format!("lambda {id}: {text:?} is not a LAMBDA"));
745 true
746 }
747 Err(e) => {
748 err.get_or_insert(format!("lambda {id}: {e}"));
749 true
750 }
751 });
752 if todo.len() == before {
753 return Err(err.unwrap_or_default());
754 }
755 }
756 Ok(())
757 }
758
759 pub fn add_category(&mut self, id: CategoryId, name: impl Into<String>) {
762 self.categories.insert(
763 id,
764 Category {
765 id,
766 name: Name(name.into()),
767 items: Vec::new(),
768 },
769 );
770 }
771
772 pub fn add_item(&mut self, id: ItemId, category: CategoryId, name: impl Into<String>) {
773 self.items.insert(
774 id,
775 Item {
776 id,
777 category,
778 name: Name(name.into()),
779 },
780 );
781 if let Some(c) = self.categories.get_mut(&category) {
782 if !c.items.contains(&id) {
783 c.items.push(id);
784 }
785 }
786 }
787
788 pub fn add_measure(&mut self, m: Measure) {
789 self.measures.insert(m.id, m);
790 }
791
792 pub fn set_input(&mut self, measure: MeasureId, coord: Coordinate, value: Value) {
793 self.inputs.insert((measure, coord), value);
794 }
795
796 pub fn input(&self, measure: MeasureId, coord: &Coordinate) -> Option<&Value> {
797 self.inputs.get(&(measure, coord.clone()))
798 }
799
800 pub fn measure_dependency_closure(
809 &self,
810 roots: &[MeasureId],
811 ) -> std::collections::HashSet<MeasureId> {
812 let mut seen: std::collections::HashSet<MeasureId> = roots.iter().copied().collect();
813 let mut frontier: Vec<MeasureId> = roots.to_vec();
814 while let Some(m) = frontier.pop() {
815 let mut deps: Vec<MeasureId> = Vec::new();
816 if let Some(measure) = self.measures.get(&m) {
817 if let MeasureKind::Derived(f) = &measure.kind {
818 deps.extend(f.referenced_measures());
819 }
820 }
821 if let Some(call) = self.external_calls.get(&m) {
822 deps.extend(call.arg_measures.iter().copied());
823 }
824 for d in deps {
825 if seen.insert(d) {
826 frontier.push(d);
827 }
828 }
829 }
830 seen
831 }
832
833 pub fn measure_by_name(&self, name: &str) -> Option<&Measure> {
835 self.measures.values().find(|m| m.name.0 == name)
836 }
837
838 pub fn category_by_name(&self, name: &str) -> Option<&Category> {
840 self.categories.values().find(|c| c.name.0 == name)
841 }
842
843 pub fn add_view(&mut self, v: View) {
845 self.views.insert(v.id, v);
846 }
847
848 pub fn view_by_name(&self, name: &str) -> Option<&View> {
850 self.views.values().find(|v| v.name.0 == name)
851 }
852
853 pub fn add_scenario(&mut self, s: Scenario) {
855 self.scenarios.insert(s.id, s);
856 }
857
858 pub fn scenario_by_name(&self, name: &str) -> Option<&Scenario> {
860 self.scenarios.values().find(|s| s.name.0 == name)
861 }
862
863 pub fn with_scenario(&self, id: ScenarioId) -> Model {
871 let mut m = self.clone();
872 m.scenarios.clear();
873 if let Some(s) = self.scenarios.get(&id) {
874 for (key, val) in &s.overrides {
875 m.inputs.insert(key.clone(), val.clone());
876 }
877 }
878 m
879 }
880}
881
882#[cfg(test)]
883#[cfg_attr(coverage_nightly, coverage(off))]
884mod tests {
885 use super::*;
886
887 fn time_product_model() -> Model {
889 let mut m = Model::new();
890 let (time, product) = (CategoryId(1), CategoryId(2));
891 m.add_category(time, "Time");
892 m.add_category(product, "Product");
893 m.add_item(ItemId(10), time, "2025");
894 m.add_item(ItemId(11), time, "2026");
895 m.add_item(ItemId(20), product, "Widget A");
896 m.add_item(ItemId(21), product, "Widget B");
897
898 m.add_measure(Measure {
899 id: MeasureId(100),
900 name: Name("Price".into()),
901 value_type: ValueType::Number,
902 categories: vec![product],
903 kind: MeasureKind::Input,
904 description: Some("Unit price per product".into()),
905 });
906 m.set_input(
907 MeasureId(100),
908 Coordinate::from_pairs([(product, ItemId(20))]),
909 Value::Number(10.0),
910 );
911 m
912 }
913
914 #[test]
915 fn coordinate_is_order_independent() {
916 let a = Coordinate::from_pairs([(CategoryId(1), ItemId(10)), (CategoryId(2), ItemId(20))]);
917 let b = Coordinate::from_pairs([(CategoryId(2), ItemId(20)), (CategoryId(1), ItemId(10))]);
918 assert_eq!(a, b, "BTreeMap key is insertion-order independent");
919 assert_eq!(a.get(CategoryId(1)), Some(ItemId(10)));
920 }
921
922 #[test]
923 fn model_build_and_lookup() {
924 let m = time_product_model();
925 assert_eq!(m.category_by_name("Time").unwrap().items.len(), 2);
926 assert!(m.measure_by_name("Price").unwrap().is_input());
927 let coord = Coordinate::from_pairs([(CategoryId(2), ItemId(20))]);
928 assert_eq!(m.input(MeasureId(100), &coord), Some(&Value::Number(10.0)));
929 }
930
931 #[test]
932 fn model_round_trips_through_json() {
933 let m = time_product_model();
934 let json = serde_json::to_string(&m).expect("serialize");
935 let back: Model = serde_json::from_str(&json).expect("deserialize");
936 assert_eq!(m, back, "model survives a JSON round trip");
937 }
938
939 #[test]
940 fn views_and_filters() {
941 let mut m = time_product_model();
942 let (time, product) = (CategoryId(1), CategoryId(2));
943 m.add_view(View {
945 id: ViewId(1),
946 name: Name("By Product".into()),
947 measure: MeasureId(100),
948 axis_order: vec![product, time],
949 n_rows: 1,
950 n_cols: 1,
951 page_items: vec![],
952 filters: vec![Filter {
953 category: product,
954 items: vec![ItemId(20)], }],
956 rect: Default::default(),
957 placements: vec![],
958 });
959 let v = m.view_by_name("By Product").expect("view");
960 assert_eq!(v.axis_order, vec![product, time]);
961 assert!(v.allows(product, ItemId(20)));
963 assert!(!v.allows(product, ItemId(21)));
964 assert!(v.allows(time, ItemId(10)));
965
966 let json = serde_json::to_string(&m).unwrap();
968 let back: Model = serde_json::from_str(&json).unwrap();
969 assert_eq!(m, back);
970 }
971
972 #[test]
973 fn legacy_flat_view_json_loads_as_a_one_matrix_canvas() {
974 let legacy = r#"{
978 "id": 7,
979 "name": "By Product",
980 "measure": 100,
981 "axis_order": [2, 1],
982 "n_rows": 1,
983 "n_cols": 1,
984 "page_items": [[3, 30]],
985 "filters": [{"category": 2, "items": [20]}]
986 }"#;
987 let v: View = serde_json::from_str(legacy).expect("legacy view deserializes");
988
989 assert_eq!(v.id, ViewId(7));
991 assert_eq!(v.name, Name("By Product".into()));
992 assert_eq!(v.measure, MeasureId(100));
993 assert_eq!(v.axis_order, vec![CategoryId(2), CategoryId(1)]);
994 assert_eq!((v.n_rows, v.n_cols), (1, 1));
995 assert_eq!(v.page_items, vec![(CategoryId(3), ItemId(30))]);
996 assert_eq!(
997 v.filters,
998 vec![Filter {
999 category: CategoryId(2),
1000 items: vec![ItemId(20)]
1001 }]
1002 );
1003
1004 assert!(v.placements.is_empty(), "no extra matrices");
1006 let ms = v.matrices();
1007 assert_eq!(ms.len(), 1);
1008 assert_eq!(ms[0].measure, MeasureId(100));
1009 assert_eq!(ms[0].axis_order, v.axis_order);
1010 assert_eq!((ms[0].n_rows, ms[0].n_cols), (1, 1));
1011 assert_eq!(ms[0].page_items, v.page_items);
1012 assert_eq!(ms[0].filters, v.filters);
1013 assert_eq!(ms[0].rect, CanvasRect::default(), "geometry defaulted");
1014
1015 assert!(v.allows(CategoryId(2), ItemId(20)));
1017 assert!(!v.allows(CategoryId(2), ItemId(21)));
1018 assert!(v.allows(CategoryId(1), ItemId(10))); assert!(!ms[0].allows(CategoryId(2), ItemId(21)));
1020
1021 let prestacking = r#"{"id": 1, "name": "V", "measure": 100, "axis_order": [1, 2]}"#;
1024 let v2: View = serde_json::from_str(prestacking).expect("pre-stacking view");
1025 assert_eq!((v2.n_rows, v2.n_cols), (1, 1));
1026 assert_eq!(v2.matrices().len(), 1);
1027 }
1028
1029 #[test]
1032 fn load_lambdas_from_rows() {
1033 let mut m = time_product_model();
1034 let twice = NamedLambda {
1035 name: Name("Twice".into()),
1036 params: vec!["x".into()],
1037 body: Expr::BinaryOp(
1038 BinaryOp::Mul,
1039 Box::new(Expr::Ref(
1040 functions::lambda_param(0),
1041 DimensionSpec::default(),
1042 )),
1043 Box::new(Expr::Literal(Value::Number(2.0))),
1044 ),
1045 };
1046 m.load_lambdas(&[
1047 (1, "Big = LAMBDA(x, Norm(x, Time) > 1)".into()),
1048 (2, "Norm = LAMBDA(x, c, Twice(x) / MAX(x OVER c))".into()),
1049 (3, serde_json::to_string(&twice).unwrap()),
1050 ])
1051 .unwrap();
1052 assert_eq!(m.lambdas[&3], twice);
1053 assert_eq!(m.lambda_by_name("big").map(|(id, _)| id), Some(1));
1054 let norm = &m.lambdas[&2];
1055 assert_eq!(
1056 norm.param_kinds(),
1057 Ok(vec![LambdaParam::Reduced, LambdaParam::Category])
1058 );
1059 let Expr::BinaryOp(_, call, _) = &m.lambdas[&1].body else {
1060 panic!("{:?}", m.lambdas[&1].body)
1061 };
1062 assert!(matches!(&**call, Expr::Call(f, a)
1063 if *f == functions::named_lambda(2) && a[1] == Expr::Category(CategoryId(1))));
1064 m.add_measure(Measure {
1066 id: MeasureId(200),
1067 name: Name("Flag".into()),
1068 value_type: ValueType::Boolean,
1069 categories: vec![CategoryId(2)],
1070 kind: MeasureKind::Derived(Formula::new(Expr::Call(
1071 functions::named_lambda(1),
1072 vec![Expr::Ref(MeasureId(100), DimensionSpec::default())],
1073 ))),
1074 description: None,
1075 });
1076 assert_eq!(m.lambda_users(3), (vec![1, 2], vec![MeasureId(200)]));
1077 assert_eq!(m.lambda_users(1), (vec![], vec![MeasureId(200)]));
1078 assert_eq!(m.lambda_users(9), (vec![], vec![]));
1079 for (row, why) in [
1080 ("Bad = LAMBDA(x, Nope(x))", "unknown function: Nope"),
1081 ("Bad = Price * 2", "is not a LAMBDA"),
1082 ] {
1083 let e = m.clone().load_lambdas(&[(9, row.into())]).unwrap_err();
1084 assert!(e.starts_with("lambda 9: ") && e.contains(why), "{row}: {e}");
1085 }
1086 }
1087
1088 #[test]
1089 fn multi_placement_view_round_trips_and_filters_per_matrix() {
1090 let (time, product) = (CategoryId(1), CategoryId(2));
1091 let mut second = MatrixPlacement::new(MeasureId(101));
1092 second.axis_order = vec![time, product];
1093 second.n_rows = 2;
1094 second.n_cols = 0;
1095 second.rect = CanvasRect {
1096 x: 520.0,
1097 y: 48.5,
1098 w: 300.0,
1099 h: 200.0,
1100 };
1101 second.filters = vec![Filter {
1102 category: time,
1103 items: vec![ItemId(11)], }];
1105
1106 let mut primary = MatrixPlacement::new(MeasureId(100));
1107 primary.axis_order = vec![product];
1108 primary.rect = CanvasRect {
1109 x: 10.0,
1110 y: 20.0,
1111 w: 400.0,
1112 h: 150.0,
1113 };
1114 primary.filters = vec![Filter {
1115 category: product,
1116 items: vec![ItemId(20)],
1117 }];
1118
1119 let v = View::from_matrices(
1120 ViewId(3),
1121 Name("Canvas".into()),
1122 primary.clone(),
1123 vec![second.clone()],
1124 );
1125 assert_eq!(v.matrices(), vec![primary.clone(), second.clone()]);
1126
1127 assert!(v.allows(product, ItemId(20)));
1129 assert!(!v.allows(product, ItemId(21)));
1130 assert!(v.allows(time, ItemId(10)), "primary has no Time filter");
1131 assert!(!second.allows(time, ItemId(10)), "but matrix 2 does");
1132 assert!(second.allows(time, ItemId(11)));
1133
1134 let json = serde_json::to_string(&v).unwrap();
1136 let back: View = serde_json::from_str(&json).unwrap();
1137 assert_eq!(back, v);
1138 assert_eq!(back.rect.x, 10.0);
1139 assert_eq!(back.placements[0].rect.y, 48.5);
1140 assert_eq!(back.placements[0].rect.w, 300.0);
1141 assert_eq!(
1142 (back.placements[0].n_rows, back.placements[0].n_cols),
1143 (2, 0)
1144 );
1145
1146 let mut m = time_product_model();
1148 m.add_view(v.clone());
1149 let back: Model = serde_json::from_str(&serde_json::to_string(&m).unwrap()).unwrap();
1150 assert_eq!(back, m);
1151 assert_eq!(back.views[&ViewId(3)].matrices().len(), 2);
1152 }
1153
1154 #[test]
1155 fn scenario_overlay_overrides_inputs() {
1156 let mut m = time_product_model();
1157 let product = CategoryId(2);
1158 let a = Coordinate::from_pairs([(product, ItemId(20))]);
1159 assert_eq!(m.input(MeasureId(100), &a), Some(&Value::Number(10.0)));
1161
1162 let mut overrides = std::collections::HashMap::new();
1164 overrides.insert((MeasureId(100), a.clone()), Value::Number(15.0));
1165 m.add_scenario(Scenario {
1166 id: ScenarioId(1),
1167 name: Name("Price hike".into()),
1168 overrides,
1169 });
1170
1171 assert_eq!(m.input(MeasureId(100), &a), Some(&Value::Number(10.0)));
1173 let s = m.scenario_by_name("Price hike").unwrap().id;
1174 let world = m.with_scenario(s);
1175 assert_eq!(world.input(MeasureId(100), &a), Some(&Value::Number(15.0)));
1176 assert!(world.scenarios.is_empty());
1178
1179 let json = serde_json::to_string(&m).unwrap();
1181 let back: Model = serde_json::from_str(&json).unwrap();
1182 assert_eq!(m, back);
1183 }
1184
1185 #[test]
1186 fn measure_dependency_closure_walks_formulas_and_external_calls() {
1187 let mut m = Model::new();
1191 let a = MeasureId(1);
1192 let b = MeasureId(2);
1193 let c = MeasureId(3);
1194 let d = MeasureId(4);
1195 let e = MeasureId(5);
1196 let f = MeasureId(6);
1197 let leaf = |id: MeasureId, kind: MeasureKind| Measure {
1198 id,
1199 name: Name(format!("M{}", id.0)),
1200 value_type: ValueType::Number,
1201 categories: vec![],
1202 kind,
1203 description: None,
1204 };
1205 m.add_measure(leaf(c, MeasureKind::Input));
1206 m.add_measure(leaf(e, MeasureKind::Input));
1207 m.add_measure(leaf(f, MeasureKind::Input));
1208 m.add_measure(leaf(
1209 b,
1210 MeasureKind::Derived(Formula::new(Expr::Ref(c, DimensionSpec::default()))),
1211 ));
1212 m.add_measure(leaf(
1213 a,
1214 MeasureKind::Derived(Formula::new(Expr::Ref(b, DimensionSpec::default()))),
1215 ));
1216 m.add_measure(leaf(d, MeasureKind::Input)); m.external_calls.insert(
1218 d,
1219 ExternalCall {
1220 func: "f".into(),
1221 arg_measures: vec![c, e],
1222 refresh_policy: Default::default(),
1223 },
1224 );
1225
1226 let closure = m.measure_dependency_closure(&[a]);
1227 assert_eq!(closure, [a, b, c].into_iter().collect());
1228
1229 let closure_d = m.measure_dependency_closure(&[d]);
1230 assert_eq!(closure_d, [d, c, e].into_iter().collect());
1231
1232 let closure_both = m.measure_dependency_closure(&[a, d]);
1234 assert_eq!(closure_both, [a, b, c, d, e].into_iter().collect());
1235 assert!(!closure_both.contains(&f));
1236 }
1237}