Skip to main content

spreadsheet_to_json/
key_segment.rs

1use serde_json::{Map, Value};
2use std::sync::Arc;
3
4/// A sortable identifier used to discriminate between array items -- either a plain
5/// string label ("west") or an integer ("2015"). Kept as a small union type (rather than
6/// always coercing to a string) so identifiers extracted from header labels render as
7/// the JSON type they actually are (`2015`, not `"2015"`).
8#[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    /// Parses `id` as an integer identifier. Only ever fed numeric segments already
20    /// extracted from header labels (e.g. "2015" out of "longevity_2015_female"), where
21    /// a parse failure would mean something has gone wrong upstream in the extraction
22    /// itself, not in this value -- confirmed as an acceptable fallback for that
23    /// constrained calling context, unlike the general-purpose sanitize-don't-guess
24    /// rule this crate otherwise follows for arbitrary user input.
25    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    /// The serde_json::Value this identifier renders as when written into a field.
34    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    /// Parses an `Identifier` from JSON -- a plain JSON string becomes `String`, a plain
42    /// JSON number becomes `Integer`, matching `to_value`'s own output shape so a
43    /// round-trip through JSON is lossless for both variants.
44    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/// Describes where a column's cell value lands in the (possibly nested) output row.
63/// Recursive variants wrap their continuation in `Arc` (not `Box`) to keep the type's
64/// size finite -- same reasoning as `Format::Array`'s `Arc<Format>`: `Column` (and
65/// therefore its `key: Option<KeySegment>`) is cloned repeatedly by `resolve_columns()`
66/// during column resolution, so an O(1) refcount bump matters here the same way it does
67/// for `Format`. Nothing in this tree needs `Box`'s unique-ownership guarantee.
68#[derive(Debug, Clone, PartialEq)]
69pub enum KeySegment {
70    /// Column is fully omitted from output.
71    Excluded,
72    /// Flat leaf -- today's only behavior, insert directly under this key.
73    Simple(Arc<str>),
74    /// Descend into (creating if needed) a plain nested object under this key, then
75    /// continue the rest of the path inside it.
76    Object(Arc<str>, Arc<KeySegment>),
77    /// Find-or-create an item in the named array whose `key_field` equals `identifier`,
78    /// then continue the rest of the path *inside* that item. Two columns land in the
79    /// same item only when their *entire* chain of Array/InnerObject identifiers agree,
80    /// not just this one segment -- see `matching_signature` below.
81    Array(Arc<str>, Identifier, Arc<str>, Arc<KeySegment>),
82    /// Inline a literal-valued field on the *current* item (no new nesting level), then
83    /// continue the rest of the path in the same item. Used to flatten multiple
84    /// discriminators onto one array item instead of nesting each one.
85    InnerObject(Identifier, Arc<str>, Arc<KeySegment>),
86    /// Push the column's own resolved value directly into the named array, as a bare
87    /// scalar -- no wrapping object, no discriminator, always appended. Distinct from
88    /// `Array`, whose items are always objects (at least the `key_field` is set) --
89    /// there's no way to get a plain array of raw values through `Array`/`InnerObject`
90    /// alone. Order is whatever order the matched columns were processed in (column
91    /// position in the sheet), the same choice already made for the analogous
92    /// numbered-fields-to-array case at the spread-cli layer.
93    PlainArray(Arc<str>),
94}
95
96impl KeySegment {
97    /// Parses a `KeySegment` from JSON -- the primary way any client crate (a frontend
98    /// UI building a JSON payload, a Web API, etc.) reaches the full tree without writing
99    /// Rust or touching calamine/csv directly. A plain JSON string is shorthand for
100    /// `Simple` (matches `Column.key`'s existing plain-string convention); anything else
101    /// needs a tagged object with a `"type"` field selecting the variant:
102    ///
103    /// - `"excluded"` -- no other fields
104    /// - `"simple"` -- `"key"` (string)
105    /// - `"object"` -- `"key"` (string), `"next"` (nested KeySegment)
106    /// - `"array"` -- `"container"` (string), `"identifier"` (string or number),
107    ///   `"key_field"` (string), `"next"` (nested KeySegment)
108    /// - `"inner_object"` -- `"identifier"` (string or number), `"field"` (string),
109    ///   `"next"` (nested KeySegment)
110    /// - `"plain_array"` -- `"container"` (string)
111    ///
112    /// Returns `None` on anything malformed (unknown type, missing/wrong-typed field) --
113    /// same sanitize-don't-guess stance as the rest of this crate's parsing.
114    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    /// A flat, single-string fallback for contexts that only ever show one name per
143    /// column (header/metadata listings) -- not a serialization of the whole tree. Shows
144    /// the outermost field name at this segment; nested detail is only ever realized by
145    /// `insert_key_segment` when actually building a row.
146    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
158/// Walks `segment`, inserting `value` at the location it describes within `current`.
159/// `current` is a plain JSON object -- the top-level row map for a column with no
160/// nesting, or a nested/array-item object for anything reached via `Object`/`Array`.
161pub 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            // Idempotent whether the item was just created or reused -- always leaves
193            // this segment's own discriminator field set.
194            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            // The array itself always exists once any column maps to it, even if every
203            // matched cell in this row turns out blank -- ["title": "Title A", "downloads":
204            // []], not "downloads" missing entirely. Create the entry unconditionally
205            // *before* deciding whether to push, not after: an early return here would
206            // skip creating it at all when this happens to be the only (blank) column
207            // seen so far for this row.
208            let arr = current
209                .entry(container.to_string())
210                .or_insert_with(|| Value::Array(vec![]));
211            // A null (or, since a blank CSV/text cell comes through as "" rather than a
212            // genuine null -- there's no such thing as a null CSV field -- an empty
213            // string too) means "this sequential slot didn't apply" (e.g. download_2 was
214            // blank while download_1/download_3 had real files), not "a real element
215            // whose value happens to be empty" -- dropped rather than kept as a
216            // positional entry, so ["file-1.pdf", "", "file-3.pdf"] (or the null
217            // equivalent from a native xlsx/ods empty cell) becomes just
218            // ["file-1.pdf", "file-3.pdf"].
219            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
229/// Every (field, identifier) pair that determines whether two columns land in the same
230/// array item -- this Array's own, plus any InnerObjects chained directly after it,
231/// stopping at the first segment that isn't an InnerObject. An InnerObject sets a
232/// sibling field on the very same item rather than starting a new nesting level, so it
233/// has to agree too before two columns are considered "the same item"; anything past an
234/// Object/another Array/a terminal Simple is a genuinely separate substructure and
235/// doesn't need to factor into this Array's own matching decision.
236fn 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        // exercise it through the real row-insertion path, not just check the shape
295        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        // The full tree from the year/gender/class example, built entirely from JSON --
313        // this is the shape a frontend UI would send, not something hand-typed via --keys.
314        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        // "object" requires both "key" and "next"
351        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        // no "type" at all on an object that isn't the plain-string shorthand
354        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        // sales_west, sales_east both descend into the same "sales" object
372        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        // file_1, file_2, file_3 -- Array with an Identifier that never matches an
389        // existing item's field (a fresh Identifier per column) always appends.
390        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        // file_1, file_2, file_3 -- unlike KeySegment::Array (which always wraps items in
413        // an object with at least a key_field set), PlainArray produces a genuinely flat
414        // array of raw values, no discriminator needed at all.
415        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        // download_1, download_2 (blank), download_3 -- the blank slot is dropped
427        // entirely, not kept as a positional null.
428        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        // A blank CSV/text-cell field comes through as Value::String(""), never a
440        // genuine null -- CSV has no native null -- so the null check alone wouldn't
441        // catch the practical case this feature exists for.
442        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        // Regression: an early return on the blank check used to skip creating the
454        // array entry at all, so a row where every download_N column was blank had no
455        // "downloads" key whatsoever, rather than "downloads": [].
456        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        // longevity_2015_female_upperclass and longevity_2015_male_upperclass: same
467        // year -> same item; different gender -> different nested branch.
468        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        // The full-chain-matching case that motivated matching_signature: four columns
501        // all sharing year=2015 must NOT collapse onto one item, because their gender/
502        // class InnerObject fields differ too -- each column's full (year, gender,
503        // class) signature is distinct, so each gets its own item.
504        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        // Two columns resolving to the exact same (year, gender, class) signature land
541        // in the same item -- e.g. a duplicate/aliased source column -- last value wins
542        // on the shared "value" field rather than creating a spurious second item.
543        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}