Skip to main content

ncp_runtime/
mapping.rs

1use std::collections::HashMap;
2
3use crate::result::CborValue;
4
5/// Resolve a dot-path from a CborValue map.
6/// e.g., resolve_path(value, "output.label") traverses into nested maps.
7/// Returns None if any segment is missing or the value is not a map at an intermediate step.
8/// Empty path returns the value itself.
9pub fn resolve_path(value: &CborValue, path: &str) -> Option<CborValue> {
10    if path.is_empty() {
11        return Some(value.clone());
12    }
13
14    let segments: Vec<&str> = path.split('.').collect();
15    let mut current = value.clone();
16
17    for segment in &segments {
18        match current {
19            CborValue::Map(pairs) => {
20                let found = pairs
21                    .into_iter()
22                    .find(|(k, _)| matches!(k, CborValue::Text(s) if s == segment));
23                match found {
24                    Some((_, v)) => current = v,
25                    None => return None,
26                }
27            }
28            _ => return None,
29        }
30    }
31
32    Some(current)
33}
34
35/// Build a nested CborValue map from a dot-path and a value.
36/// e.g., set_path("input.sentiment", val) → Map { "input": Map { "sentiment": val } }
37/// Empty path returns the value itself.
38pub fn set_path(path: &str, value: CborValue) -> CborValue {
39    if path.is_empty() {
40        return value;
41    }
42
43    let segments: Vec<&str> = path.split('.').collect();
44    let mut result = value;
45
46    // Build inside-out: wrap the value in nested single-key maps from right to left
47    for segment in segments.into_iter().rev() {
48        result = CborValue::Map(vec![(CborValue::Text(segment.to_string()), result)]);
49    }
50
51    result
52}
53
54/// Merge two CborValue maps deeply. Keys from `overlay` overwrite keys in `base`.
55/// Preserves stable key order: existing keys stay in place, new keys appended.
56/// Non-map values: overlay wins.
57pub fn merge_maps(base: CborValue, overlay: CborValue) -> CborValue {
58    match (base, overlay) {
59        (CborValue::Map(mut base_pairs), CborValue::Map(overlay_pairs)) => {
60            // Index existing text keys → position
61            let mut idx: HashMap<String, usize> = HashMap::new();
62            for (i, (k, _)) in base_pairs.iter().enumerate() {
63                if let CborValue::Text(s) = k {
64                    idx.insert(s.clone(), i);
65                }
66            }
67
68            for (ok, ov) in overlay_pairs {
69                if let CborValue::Text(ref ks) = ok {
70                    if let Some(&i) = idx.get(ks) {
71                        let old = std::mem::replace(
72                            &mut base_pairs[i],
73                            (CborValue::Null, CborValue::Null),
74                        );
75                        let (bk, bv) = old;
76                        let merged = merge_maps(bv, ov);
77                        base_pairs[i] = (bk, merged);
78                    } else {
79                        idx.insert(ks.clone(), base_pairs.len());
80                        base_pairs.push((ok, ov));
81                    }
82                } else {
83                    // Non-text keys: just append (shouldn't happen for spec-shaped maps)
84                    base_pairs.push((ok, ov));
85                }
86            }
87
88            CborValue::Map(base_pairs)
89        }
90        (_, overlay) => overlay,
91    }
92}
93
94/// Convert a CborValue to a serde_json::Value for final output.
95pub fn cbor_to_json(value: &CborValue) -> serde_json::Value {
96    match value {
97        CborValue::Null => serde_json::Value::Null,
98        CborValue::Bool(b) => serde_json::Value::Bool(*b),
99        CborValue::Integer(n) => serde_json::json!(n),
100        CborValue::Float(f) => serde_json::json!(f),
101        CborValue::Text(s) => serde_json::Value::String(s.clone()),
102        CborValue::Bytes(b) => serde_json::Value::String(hex::encode(b)),
103        CborValue::Array(items) => {
104            serde_json::Value::Array(items.iter().map(cbor_to_json).collect())
105        }
106        CborValue::Map(pairs) => {
107            let mut map = serde_json::Map::new();
108            for (k, v) in pairs {
109                let key = match k {
110                    CborValue::Text(s) => s.clone(),
111                    other => format!("{other:?}"),
112                };
113                map.insert(key, cbor_to_json(v));
114            }
115            serde_json::Value::Object(map)
116        }
117    }
118}
119
120/// Extract output.confidence as `Option<f64>` from a BrickResult's output.
121pub fn extract_confidence(value: &CborValue) -> Option<f64> {
122    match resolve_path(value, "confidence") {
123        Some(CborValue::Float(f)) => Some(f),
124        Some(CborValue::Integer(n)) => Some(n as f64),
125        _ => None,
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    fn text(s: &str) -> CborValue {
134        CborValue::Text(s.to_string())
135    }
136    fn int(n: i64) -> CborValue {
137        CborValue::Integer(n)
138    }
139
140    fn sample_map() -> CborValue {
141        CborValue::Map(vec![
142            (text("label"), text("positive")),
143            (text("confidence"), CborValue::Float(0.95)),
144            (
145                text("nested"),
146                CborValue::Map(vec![(text("deep"), int(42))]),
147            ),
148        ])
149    }
150
151    #[test]
152    fn resolve_top_level() {
153        let map = sample_map();
154        let val = resolve_path(&map, "label").unwrap();
155        assert!(matches!(val, CborValue::Text(s) if s == "positive"));
156    }
157
158    #[test]
159    fn resolve_nested() {
160        let map = sample_map();
161        let val = resolve_path(&map, "nested.deep").unwrap();
162        assert!(matches!(val, CborValue::Integer(42)));
163    }
164
165    #[test]
166    fn resolve_missing() {
167        let map = sample_map();
168        assert!(resolve_path(&map, "nonexistent").is_none());
169        assert!(resolve_path(&map, "nested.missing").is_none());
170    }
171
172    #[test]
173    fn resolve_empty_path() {
174        let map = sample_map();
175        let val = resolve_path(&map, "").unwrap();
176        assert!(matches!(val, CborValue::Map(_)));
177    }
178
179    #[test]
180    fn set_single_segment() {
181        let result = set_path("label", text("positive"));
182        let resolved = resolve_path(&result, "label").unwrap();
183        assert!(matches!(resolved, CborValue::Text(s) if s == "positive"));
184    }
185
186    #[test]
187    fn set_multi_segment() {
188        let result = set_path("input.sentiment", text("positive"));
189        let resolved = resolve_path(&result, "input.sentiment").unwrap();
190        assert!(matches!(resolved, CborValue::Text(s) if s == "positive"));
191    }
192
193    #[test]
194    fn set_empty_path() {
195        let val = text("hello");
196        let result = set_path("", val);
197        assert!(matches!(result, CborValue::Text(s) if s == "hello"));
198    }
199
200    #[test]
201    fn merge_disjoint() {
202        let a = set_path("x", int(1));
203        let b = set_path("y", int(2));
204        let merged = merge_maps(a, b);
205        assert!(matches!(
206            resolve_path(&merged, "x"),
207            Some(CborValue::Integer(1))
208        ));
209        assert!(matches!(
210            resolve_path(&merged, "y"),
211            Some(CborValue::Integer(2))
212        ));
213    }
214
215    #[test]
216    fn merge_deep() {
217        let a = set_path("input.x", int(1));
218        let b = set_path("input.y", int(2));
219        let merged = merge_maps(a, b);
220        assert!(matches!(
221            resolve_path(&merged, "input.x"),
222            Some(CborValue::Integer(1))
223        ));
224        assert!(matches!(
225            resolve_path(&merged, "input.y"),
226            Some(CborValue::Integer(2))
227        ));
228    }
229
230    #[test]
231    fn merge_preserves_order() {
232        let base = CborValue::Map(vec![
233            (text("a"), int(1)),
234            (text("b"), int(2)),
235            (text("c"), int(3)),
236        ]);
237        let overlay = CborValue::Map(vec![(text("b"), int(20)), (text("d"), int(4))]);
238        let merged = merge_maps(base, overlay);
239        if let CborValue::Map(pairs) = &merged {
240            let keys: Vec<&str> = pairs
241                .iter()
242                .map(|(k, _)| {
243                    if let CborValue::Text(s) = k {
244                        s.as_str()
245                    } else {
246                        ""
247                    }
248                })
249                .collect();
250            assert_eq!(keys, vec!["a", "b", "c", "d"]);
251            assert!(matches!(
252                resolve_path(&merged, "b"),
253                Some(CborValue::Integer(20))
254            ));
255        } else {
256            panic!("expected map");
257        }
258    }
259
260    #[test]
261    fn extract_confidence_float() {
262        let map = sample_map();
263        assert_eq!(extract_confidence(&map), Some(0.95));
264    }
265
266    #[test]
267    fn extract_confidence_missing() {
268        let map = CborValue::Map(vec![(text("label"), text("positive"))]);
269        assert_eq!(extract_confidence(&map), None);
270    }
271
272    #[test]
273    fn cbor_to_json_roundtrip() {
274        let cbor = sample_map();
275        let json = cbor_to_json(&cbor);
276        assert_eq!(json["label"], "positive");
277        assert_eq!(json["confidence"], 0.95);
278        assert_eq!(json["nested"]["deep"], 42);
279    }
280}