1use serde_json::{Map, Value};
2use std::sync::Arc;
3
4#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
9pub enum Identifier {
10 String(Arc<str>),
11 Integer(i64),
12}
13
14impl Identifier {
15 pub fn from_string(id: &str) -> Self {
16 Identifier::String(Arc::from(id))
17 }
18
19 pub fn from_int_str(id: &str) -> Self {
26 Identifier::Integer(id.parse::<i64>().unwrap_or_default())
27 }
28
29 pub fn from_int(id: i64) -> Self {
30 Identifier::Integer(id)
31 }
32
33 pub fn to_value(&self) -> Value {
35 match self {
36 Identifier::String(s) => Value::String(s.to_string()),
37 Identifier::Integer(n) => Value::Number((*n).into()),
38 }
39 }
40
41 pub fn from_json(json: &Value) -> Option<Self> {
45 match json {
46 Value::String(s) => Some(Identifier::String(Arc::from(s.as_str()))),
47 Value::Number(n) => n.as_i64().map(Identifier::Integer),
48 _ => None,
49 }
50 }
51}
52
53impl std::fmt::Display for Identifier {
54 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
55 match self {
56 Identifier::String(s) => write!(f, "{}", s),
57 Identifier::Integer(n) => write!(f, "{}", n),
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq)]
69pub enum KeySegment {
70 Excluded,
72 Simple(Arc<str>),
74 Object(Arc<str>, Arc<KeySegment>),
77 Array(Arc<str>, Identifier, Arc<str>, Arc<KeySegment>),
82 InnerObject(Identifier, Arc<str>, Arc<KeySegment>),
86 PlainArray(Arc<str>),
94}
95
96impl KeySegment {
97 pub fn from_json(json: &Value) -> Option<Self> {
115 match json {
116 Value::String(s) => Some(KeySegment::Simple(Arc::from(s.as_str()))),
117 Value::Object(map) => {
118 let get_str = |field: &str| map.get(field).and_then(|v| v.as_str());
119 let get_next = || map.get("next").and_then(KeySegment::from_json).map(Arc::new);
120 let get_id = || map.get("identifier").and_then(Identifier::from_json);
121 match map.get("type").and_then(|v| v.as_str())? {
122 "excluded" => Some(KeySegment::Excluded),
123 "simple" => get_str("key").map(|s| KeySegment::Simple(Arc::from(s))),
124 "object" => Some(KeySegment::Object(Arc::from(get_str("key")?), get_next()?)),
125 "array" => Some(KeySegment::Array(
126 Arc::from(get_str("container")?),
127 get_id()?,
128 Arc::from(get_str("key_field")?),
129 get_next()?,
130 )),
131 "inner_object" => Some(KeySegment::InnerObject(get_id()?, Arc::from(get_str("field")?), get_next()?)),
132 "plain_array" => Some(KeySegment::PlainArray(Arc::from(get_str("container")?))),
133 _ => None,
134 }
135 }
136 _ => None,
137 }
138 }
139}
140
141impl std::fmt::Display for KeySegment {
142 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
147 match self {
148 KeySegment::Excluded => write!(f, ""),
149 KeySegment::Simple(key) => write!(f, "{}", key),
150 KeySegment::Object(key, _) => write!(f, "{}", key),
151 KeySegment::Array(container, ..) => write!(f, "{}", container),
152 KeySegment::InnerObject(_, field, _) => write!(f, "{}", field),
153 KeySegment::PlainArray(container) => write!(f, "{}", container),
154 }
155 }
156}
157
158pub fn insert_key_segment(current: &mut Map<String, Value>, segment: &KeySegment, value: Value) {
162 match segment {
163 KeySegment::Excluded => {}
164 KeySegment::Simple(key) => {
165 current.insert(key.to_string(), value);
166 }
167 KeySegment::Object(key, next) => {
168 let nested = current
169 .entry(key.to_string())
170 .or_insert_with(|| Value::Object(Map::new()));
171 if let Value::Object(nested_map) = nested {
172 insert_key_segment(nested_map, next, value);
173 }
174 }
175 KeySegment::Array(container, id, key_field, next) => {
176 let signature = matching_signature(key_field, id, next);
177 let arr = current
178 .entry(container.to_string())
179 .or_insert_with(|| Value::Array(vec![]));
180 let Value::Array(items) = arr else { return };
181 let existing_index = items.iter().position(|item| signature_matches(item, &signature));
182 let item_index = match existing_index {
183 Some(idx) => idx,
184 None => {
185 items.push(Value::Object(Map::new()));
186 items.len() - 1
187 }
188 };
189 let Value::Object(item_map) = &mut items[item_index] else {
190 return;
191 };
192 item_map.insert(key_field.to_string(), id.to_value());
195 insert_key_segment(item_map, next, value);
196 }
197 KeySegment::InnerObject(id, field, next) => {
198 current.insert(field.to_string(), id.to_value());
199 insert_key_segment(current, next, value);
200 }
201 KeySegment::PlainArray(container) => {
202 let arr = current
209 .entry(container.to_string())
210 .or_insert_with(|| Value::Array(vec![]));
211 let is_blank = value.is_null() || matches!(&value, Value::String(s) if s.is_empty());
220 if !is_blank {
221 if let Value::Array(items) = arr {
222 items.push(value);
223 }
224 }
225 }
226 }
227}
228
229fn matching_signature(key_field: &Arc<str>, id: &Identifier, next: &KeySegment) -> Vec<(Arc<str>, Identifier)> {
237 let mut signature = vec![(key_field.clone(), id.clone())];
238 let mut cursor = next;
239 while let KeySegment::InnerObject(inner_id, inner_field, inner_next) = cursor {
240 signature.push((inner_field.clone(), inner_id.clone()));
241 cursor = inner_next;
242 }
243 signature
244}
245
246fn signature_matches(item: &Value, signature: &[(Arc<str>, Identifier)]) -> bool {
247 let Value::Object(map) = item else { return false };
248 signature
249 .iter()
250 .all(|(field, id)| map.get(field.as_ref()) == Some(&id.to_value()))
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn run(segments_and_values: &[(KeySegment, Value)]) -> Value {
258 let mut map = Map::new();
259 for (segment, value) in segments_and_values {
260 insert_key_segment(&mut map, segment, value.clone());
261 }
262 Value::Object(map)
263 }
264
265 #[test]
266 fn test_identifier_from_json_reads_strings_and_numbers() {
267 assert_eq!(Identifier::from_json(&serde_json::json!("north")), Some(Identifier::String(Arc::from("north"))));
268 assert_eq!(Identifier::from_json(&serde_json::json!(2015)), Some(Identifier::Integer(2015)));
269 assert_eq!(Identifier::from_json(&serde_json::json!(true)), None);
270 assert_eq!(Identifier::from_json(&serde_json::json!(null)), None);
271 }
272
273 #[test]
274 fn test_key_segment_from_json_plain_string_is_simple_shorthand() {
275 assert!(matches!(KeySegment::from_json(&serde_json::json!("weight")), Some(KeySegment::Simple(k)) if &*k == "weight"));
276 }
277
278 #[test]
279 fn test_key_segment_from_json_excluded() {
280 assert!(matches!(
281 KeySegment::from_json(&serde_json::json!({"type": "excluded"})),
282 Some(KeySegment::Excluded)
283 ));
284 }
285
286 #[test]
287 fn test_key_segment_from_json_object_nests_recursively() {
288 let json = serde_json::json!({
289 "type": "object",
290 "key": "sales",
291 "next": {"type": "object", "key": "north", "next": "value"}
292 });
293 let segment = KeySegment::from_json(&json).expect("should parse");
294 let result = run(&[(segment, Value::from(4500000))]);
296 assert_eq!(result, serde_json::json!({"sales": {"north": {"value": 4500000}}}));
297 }
298
299 #[test]
300 fn test_key_segment_from_json_plain_array() {
301 let json = serde_json::json!({"type": "plain_array", "container": "files"});
302 let segment = KeySegment::from_json(&json).expect("should parse");
303 let result = run(&[
304 (segment.clone(), Value::String("a.pdf".into())),
305 (segment, Value::String("b.pdf".into())),
306 ]);
307 assert_eq!(result, serde_json::json!({"files": ["a.pdf", "b.pdf"]}));
308 }
309
310 #[test]
311 fn test_key_segment_from_json_array_with_inner_object_round_trips_the_longevity_example() {
312 fn column_json(year: i64, gender: &str, class: &str) -> Value {
315 serde_json::json!({
316 "type": "array",
317 "container": "longevity",
318 "identifier": year,
319 "key_field": "year",
320 "next": {
321 "type": "inner_object",
322 "identifier": gender,
323 "field": "gender",
324 "next": {
325 "type": "inner_object",
326 "identifier": class,
327 "field": "class",
328 "next": "value"
329 }
330 }
331 })
332 }
333 let segments = [
334 (KeySegment::from_json(&column_json(2015, "female", "upperclass")).unwrap(), Value::from(87.9)),
335 (KeySegment::from_json(&column_json(2015, "male", "upperclass")).unwrap(), Value::from(83.8)),
336 ];
337 let result = run(&segments);
338 assert_eq!(
339 result,
340 serde_json::json!({"longevity": [
341 {"year": 2015, "gender": "female", "class": "upperclass", "value": 87.9},
342 {"year": 2015, "gender": "male", "class": "upperclass", "value": 83.8}
343 ]})
344 );
345 }
346
347 #[test]
348 fn test_key_segment_from_json_rejects_unknown_type_and_missing_fields() {
349 assert_eq!(KeySegment::from_json(&serde_json::json!({"type": "bogus"})), None);
350 assert_eq!(KeySegment::from_json(&serde_json::json!({"type": "object", "key": "sales"})), None);
352 assert_eq!(KeySegment::from_json(&serde_json::json!({"type": "object", "next": "value"})), None);
353 assert_eq!(KeySegment::from_json(&serde_json::json!({"key": "sales"})), None);
355 }
356
357 #[test]
358 fn test_simple_matches_todays_flat_behavior() {
359 let result = run(&[(KeySegment::Simple(Arc::from("country_code")), Value::String("AFG".into()))]);
360 assert_eq!(result, serde_json::json!({"country_code": "AFG"}));
361 }
362
363 #[test]
364 fn test_excluded_inserts_nothing() {
365 let result = run(&[(KeySegment::Excluded, Value::String("skip me".into()))]);
366 assert_eq!(result, serde_json::json!({}));
367 }
368
369 #[test]
370 fn test_object_nests_and_merges_sibling_columns() {
371 let segments = [
373 (
374 KeySegment::Object(Arc::from("sales"), Arc::new(KeySegment::Simple(Arc::from("west")))),
375 Value::Number(19812.into()),
376 ),
377 (
378 KeySegment::Object(Arc::from("sales"), Arc::new(KeySegment::Simple(Arc::from("east")))),
379 Value::Number(17293.into()),
380 ),
381 ];
382 let result = run(&segments);
383 assert_eq!(result, serde_json::json!({"sales": {"west": 19812, "east": 17293}}));
384 }
385
386 #[test]
387 fn test_array_pushes_positionally_with_no_discriminator() {
388 let segments = [
391 (
392 KeySegment::Array(Arc::from("files"), Identifier::from_int(0), Arc::from("_idx"), Arc::new(KeySegment::Simple(Arc::from("value")))),
393 Value::String("sales-data.xlsx".into()),
394 ),
395 (
396 KeySegment::Array(Arc::from("files"), Identifier::from_int(1), Arc::from("_idx"), Arc::new(KeySegment::Simple(Arc::from("value")))),
397 Value::String("marketing-report.pdf".into()),
398 ),
399 ];
400 let result = run(&segments);
401 assert_eq!(
402 result,
403 serde_json::json!({"files": [
404 {"_idx": 0, "value": "sales-data.xlsx"},
405 {"_idx": 1, "value": "marketing-report.pdf"}
406 ]})
407 );
408 }
409
410 #[test]
411 fn test_plain_array_pushes_bare_scalars_with_no_object_wrapper() {
412 let segments = [
416 (KeySegment::PlainArray(Arc::from("files")), Value::String("file_1.pdf".into())),
417 (KeySegment::PlainArray(Arc::from("files")), Value::String("file_2.pdf".into())),
418 (KeySegment::PlainArray(Arc::from("files")), Value::String("file_3.pdf".into())),
419 ];
420 let result = run(&segments);
421 assert_eq!(result, serde_json::json!({"files": ["file_1.pdf", "file_2.pdf", "file_3.pdf"]}));
422 }
423
424 #[test]
425 fn test_plain_array_drops_null_elements_rather_than_keeping_them_positional() {
426 let segments = [
429 (KeySegment::PlainArray(Arc::from("downloads")), Value::String("file-1.pdf".into())),
430 (KeySegment::PlainArray(Arc::from("downloads")), Value::Null),
431 (KeySegment::PlainArray(Arc::from("downloads")), Value::String("file-3.pdf".into())),
432 ];
433 let result = run(&segments);
434 assert_eq!(result, serde_json::json!({"downloads": ["file-1.pdf", "file-3.pdf"]}));
435 }
436
437 #[test]
438 fn test_plain_array_also_drops_empty_strings_since_csv_blanks_are_never_actually_null() {
439 let segments = [
443 (KeySegment::PlainArray(Arc::from("downloads")), Value::String("file-1.pdf".into())),
444 (KeySegment::PlainArray(Arc::from("downloads")), Value::String("".into())),
445 (KeySegment::PlainArray(Arc::from("downloads")), Value::String("file-3.pdf".into())),
446 ];
447 let result = run(&segments);
448 assert_eq!(result, serde_json::json!({"downloads": ["file-1.pdf", "file-3.pdf"]}));
449 }
450
451 #[test]
452 fn test_plain_array_stays_an_empty_array_not_absent_when_every_matched_cell_is_blank() {
453 let segments = [
457 (KeySegment::PlainArray(Arc::from("downloads")), Value::String("".into())),
458 (KeySegment::PlainArray(Arc::from("downloads")), Value::Null),
459 ];
460 let result = run(&segments);
461 assert_eq!(result, serde_json::json!({"downloads": []}));
462 }
463
464 #[test]
465 fn test_array_inner_object_merges_matching_year_into_one_item() {
466 fn seg(gender: &str, class: &str) -> KeySegment {
469 KeySegment::Array(
470 Arc::from("longevity"),
471 Identifier::from_int_str("2015"),
472 Arc::from("year"),
473 Arc::new(KeySegment::Object(
474 Arc::from(gender),
475 Arc::new(KeySegment::Simple(Arc::from(class))),
476 )),
477 )
478 }
479 let segments = [
480 (seg("female", "upperclass"), Value::from(87.9)),
481 (seg("male", "upperclass"), Value::from(83.8)),
482 (seg("female", "lowerclass"), Value::from(80.4)),
483 (seg("male", "lowerclass"), Value::from(76.9)),
484 ];
485 let result = run(&segments);
486 assert_eq!(
487 result,
488 serde_json::json!({"longevity": [
489 {
490 "year": 2015,
491 "female": {"upperclass": 87.9, "lowerclass": 80.4},
492 "male": {"upperclass": 83.8, "lowerclass": 76.9}
493 }
494 ]})
495 );
496 }
497
498 #[test]
499 fn test_array_with_chained_inner_objects_produces_flat_tidy_items() {
500 fn seg(year: &str, gender: &str, class: &str) -> KeySegment {
505 KeySegment::Array(
506 Arc::from("longevity"),
507 Identifier::from_int_str(year),
508 Arc::from("year"),
509 Arc::new(KeySegment::InnerObject(
510 Identifier::from_string(gender),
511 Arc::from("gender"),
512 Arc::new(KeySegment::InnerObject(
513 Identifier::from_string(class),
514 Arc::from("class"),
515 Arc::new(KeySegment::Simple(Arc::from("value"))),
516 )),
517 )),
518 )
519 }
520 let segments = [
521 (seg("2015", "female", "upperclass"), Value::from(87.9)),
522 (seg("2015", "male", "upperclass"), Value::from(83.8)),
523 (seg("2015", "female", "lowerclass"), Value::from(80.4)),
524 (seg("2015", "male", "lowerclass"), Value::from(76.9)),
525 ];
526 let result = run(&segments);
527 assert_eq!(
528 result,
529 serde_json::json!({"longevity": [
530 {"year": 2015, "gender": "female", "class": "upperclass", "value": 87.9},
531 {"year": 2015, "gender": "male", "class": "upperclass", "value": 83.8},
532 {"year": 2015, "gender": "female", "class": "lowerclass", "value": 80.4},
533 {"year": 2015, "gender": "male", "class": "lowerclass", "value": 76.9}
534 ]})
535 );
536 }
537
538 #[test]
539 fn test_array_with_inner_objects_still_merges_when_full_signature_agrees() {
540 fn seg() -> KeySegment {
544 KeySegment::Array(
545 Arc::from("longevity"),
546 Identifier::from_int_str("2015"),
547 Arc::from("year"),
548 Arc::new(KeySegment::InnerObject(
549 Identifier::from_string("female"),
550 Arc::from("gender"),
551 Arc::new(KeySegment::Simple(Arc::from("value"))),
552 )),
553 )
554 }
555 let result = run(&[(seg(), Value::from(87.9)), (seg(), Value::from(88.0))]);
556 assert_eq!(
557 result,
558 serde_json::json!({"longevity": [
559 {"year": 2015, "gender": "female", "value": 88.0}
560 ]})
561 );
562 }
563}