1use serde_json::{Map, Value};
13use std::collections::HashMap;
14
15pub const MAX_MATRIX_PRODUCT: usize = 10_000;
19
20#[derive(Debug, Clone, PartialEq)]
23pub struct Dim {
24 pub id: String,
26 pub alias: String,
28 pub values: Vec<Value>,
30}
31
32#[derive(Debug, Clone, PartialEq)]
38pub struct CollectedDim {
39 pub id: String,
41 pub alias: String,
43 pub dims: Vec<String>,
46 pub by_tuple: HashMap<String, Vec<Value>>,
48}
49
50pub fn collected_tuple_key(dims: &[String], ctx: &HashMap<String, Value>) -> String {
56 dims.iter()
57 .map(|d| {
58 let v = ctx.get(d).cloned().unwrap_or(Value::Null);
59 format!("{d}={}", serde_json::to_string(&v).unwrap_or_default())
60 })
61 .collect::<Vec<_>>()
62 .join("&")
63}
64
65pub fn inject_collected(ctx: &mut HashMap<String, Value>, collected: &[CollectedDim]) {
71 for cd in collected {
72 let key = collected_tuple_key(&cd.dims, ctx);
73 let list = cd.by_tuple.get(&key).cloned().unwrap_or_default();
74 let mut obj = Map::new();
75 obj.insert(cd.alias.clone(), Value::Array(list));
76 ctx.insert(cd.id.clone(), Value::Object(obj));
77 }
78}
79
80pub fn project_dedup(records: &[Value], select: &str) -> Vec<Value> {
84 let mut seen: Vec<Value> = Vec::new();
85 for rec in records {
86 let Some(v) = project_value(rec, select) else {
87 continue;
88 };
89 if v.is_null() {
90 continue;
91 }
92 if !seen.contains(&v) {
93 seen.push(v);
94 }
95 }
96 seen
97}
98
99fn project_value(record: &Value, select: &str) -> Option<Value> {
103 let path = select
104 .strip_prefix("$.")
105 .or_else(|| select.strip_prefix('$'))
106 .unwrap_or(select);
107 if path.is_empty() {
108 return Some(record.clone());
109 }
110 let mut cur = record;
111 for seg in path.split('.') {
112 cur = cur.get(seg)?;
113 }
114 Some(cur.clone())
115}
116
117pub fn product_size(dims: &[Dim]) -> usize {
121 if dims.is_empty() {
122 return 0;
123 }
124 dims.iter()
125 .map(|d| d.values.len())
126 .try_fold(1usize, |acc, n| acc.checked_mul(n))
127 .unwrap_or(usize::MAX)
128}
129
130pub fn cartesian(dims: &[Dim]) -> Vec<HashMap<String, Value>> {
136 if dims.is_empty() || dims.iter().any(|d| d.values.is_empty()) {
137 return Vec::new();
138 }
139 let mut out: Vec<HashMap<String, Value>> = vec![HashMap::new()];
140 for dim in dims {
141 let mut next = Vec::with_capacity(out.len() * dim.values.len());
142 for base in &out {
143 for v in &dim.values {
144 let mut ctx = base.clone();
145 let mut obj = Map::new();
146 obj.insert(dim.alias.clone(), v.clone());
147 ctx.insert(dim.id.clone(), Value::Object(obj));
148 next.push(ctx);
149 }
150 }
151 out = next;
152 }
153 out
154}
155
156pub fn tuple_state_key_suffix(dims: &[Dim], ctx: &HashMap<String, Value>) -> String {
160 dims.iter()
161 .map(|d| {
162 let v = ctx
163 .get(&d.id)
164 .and_then(|o| o.get(&d.alias))
165 .map(value_brief)
166 .unwrap_or_else(|| "(missing)".to_string());
167 format!("{}={}", d.alias, v)
168 })
169 .collect::<Vec<_>>()
170 .join("&")
171}
172
173fn value_brief(v: &Value) -> String {
175 match v {
176 Value::String(s) => s.clone(),
177 Value::Null => "null".to_string(),
178 other => other.to_string(),
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use serde_json::json;
186
187 #[test]
188 fn project_dedup_dotpath_and_dollar() {
189 let recs = vec![
190 json!({"id": 1, "name": "a"}),
191 json!({"id": 2, "name": "b"}),
192 json!({"id": 1, "name": "c"}), ];
194 assert_eq!(project_dedup(&recs, "$.id"), vec![json!(1), json!(2)]);
195 assert_eq!(project_dedup(&recs, "id"), vec![json!(1), json!(2)]);
196 assert_eq!(project_dedup(&recs, "$").len(), 3);
198 }
199
200 #[test]
201 fn project_dedup_skips_null_and_missing() {
202 let recs = vec![
203 json!({"id": 1}),
204 json!({"id": null}),
205 json!({"other": 9}), json!({"id": 2}),
207 ];
208 assert_eq!(project_dedup(&recs, "$.id"), vec![json!(1), json!(2)]);
209 }
210
211 #[test]
212 fn project_nested_path() {
213 let recs = vec![
214 json!({"meta": {"code": "X"}}),
215 json!({"meta": {"code": "Y"}}),
216 ];
217 assert_eq!(
218 project_dedup(&recs, "$.meta.code"),
219 vec![json!("X"), json!("Y")]
220 );
221 }
222
223 fn dim(id: &str, alias: &str, vals: Vec<Value>) -> Dim {
224 Dim {
225 id: id.into(),
226 alias: alias.into(),
227 values: vals,
228 }
229 }
230
231 #[test]
232 fn cartesian_two_dims_is_product() {
233 let dims = vec![
234 dim("subs", "subsidiary_id", vec![json!(1), json!(2)]),
235 dim(
236 "fields",
237 "field_id",
238 vec![json!("a"), json!("b"), json!("c")],
239 ),
240 ];
241 let ctxs = cartesian(&dims);
242 assert_eq!(ctxs.len(), 6);
243 assert_eq!(ctxs[0]["subs"]["subsidiary_id"], json!(1));
245 assert_eq!(ctxs[0]["fields"]["field_id"], json!("a"));
246 let pairs: Vec<(Value, Value)> = ctxs
248 .iter()
249 .map(|c| {
250 (
251 c["subs"]["subsidiary_id"].clone(),
252 c["fields"]["field_id"].clone(),
253 )
254 })
255 .collect();
256 assert!(pairs.contains(&(json!(2), json!("c"))));
257 }
258
259 #[test]
260 fn cartesian_single_dim() {
261 let dims = vec![dim("d", "v", vec![json!(1), json!(2)])];
262 let ctxs = cartesian(&dims);
263 assert_eq!(ctxs.len(), 2);
264 assert_eq!(ctxs[1]["d"]["v"], json!(2));
265 }
266
267 #[test]
268 fn cartesian_empty_dim_yields_nothing() {
269 let dims = vec![dim("a", "x", vec![json!(1)]), dim("b", "y", vec![])];
270 assert!(cartesian(&dims).is_empty());
271 assert!(cartesian(&[]).is_empty());
272 }
273
274 #[test]
275 fn product_size_math() {
276 assert_eq!(product_size(&[]), 0);
277 assert_eq!(
278 product_size(&[
279 dim("a", "x", vec![json!(1), json!(2)]),
280 dim("b", "y", vec![json!(1)])
281 ]),
282 2
283 );
284 assert_eq!(
285 product_size(&[dim("a", "x", vec![]), dim("b", "y", vec![json!(1)])]),
286 0
287 );
288 }
289
290 #[test]
291 fn collected_tuple_key_symmetric_between_store_and_read() {
292 let dims = vec!["types".to_string()];
295 let store_ctx: HashMap<String, Value> =
296 [("types".to_string(), json!({"name": "deal"}))].into();
297 let read_ctx: HashMap<String, Value> = [
299 ("types".to_string(), json!({"name": "deal"})),
300 ("other".to_string(), json!({"x": 1})),
301 ]
302 .into();
303 assert_eq!(
304 collected_tuple_key(&dims, &store_ctx),
305 collected_tuple_key(&dims, &read_ctx)
306 );
307 }
308
309 #[test]
310 fn inject_collected_puts_the_list_into_ctx() {
311 let cd = CollectedDim {
312 id: "props".into(),
313 alias: "name".into(),
314 dims: vec!["types".into()],
315 by_tuple: [(
316 collected_tuple_key(
317 &["types".into()],
318 &[("types".to_string(), json!({"name": "deal"}))].into(),
319 ),
320 vec![json!("amount"), json!("stage")],
321 )]
322 .into(),
323 };
324 let mut ctx: HashMap<String, Value> =
325 [("types".to_string(), json!({"name": "deal"}))].into();
326 inject_collected(&mut ctx, std::slice::from_ref(&cd));
327 assert_eq!(ctx["props"]["name"], json!(["amount", "stage"]));
328
329 let mut miss: HashMap<String, Value> =
331 [("types".to_string(), json!({"name": "ticket"}))].into();
332 inject_collected(&mut miss, &[cd]);
333 assert_eq!(miss["props"]["name"], json!([]));
334 }
335
336 #[test]
337 fn tuple_state_key_is_stable_and_ordered() {
338 let dims = vec![
339 dim("subs", "subsidiary_id", vec![json!(1)]),
340 dim("fields", "field_id", vec![json!("a")]),
341 ];
342 let ctxs = cartesian(&dims);
343 assert_eq!(
344 tuple_state_key_suffix(&dims, &ctxs[0]),
345 "subsidiary_id=1&field_id=a"
346 );
347 }
348}