Skip to main content

osp_cli/dsl/eval/
resolve.rs

1use std::collections::HashSet;
2
3use crate::core::row::Row;
4use serde_json::Value;
5
6use crate::dsl::{
7    eval::{
8        flatten::{coalesce_flat_row, flatten_row},
9        matchers::match_row_keys,
10    },
11    parse::{
12        key_spec::ExactMode,
13        path::{PathExpression, Selector, parse_path, requires_materialization},
14    },
15};
16
17pub fn resolve_values(row: &Row, token: &str, exact: ExactMode) -> Vec<Value> {
18    let trimmed = token.trim();
19    if trimmed.is_empty() {
20        return Vec::new();
21    }
22
23    if let Ok(path) = parse_path(trimmed) {
24        let nested = Value::Object(row.clone());
25        // Fast path for nested JSON traversal. Absolute paths trust the path
26        // result even when empty; relative paths fall through to flat-key
27        // matching when the nested traversal found nothing.
28        let direct = evaluate_path(&nested, &path);
29        if path.absolute || !direct.is_empty() {
30            return dedup_values(direct);
31        }
32    }
33
34    // Flat-key fallback for dotted keys that already exist as flattened labels.
35    let flattened = flatten_row(row);
36    let matched = match_row_keys(&flattened, trimmed, exact);
37    let values = matched
38        .iter()
39        .filter_map(|key| flattened.get(*key).cloned())
40        .collect::<Vec<_>>();
41
42    dedup_values(values)
43}
44
45pub fn resolve_values_truthy(row: &Row, token: &str, exact: ExactMode) -> bool {
46    resolve_values(row, token, exact).iter().any(is_truthy)
47}
48
49pub fn resolve_first_value(row: &Row, token: &str, exact: ExactMode) -> Option<Value> {
50    let value = resolve_values(row, token, exact).into_iter().next()?;
51    match value {
52        Value::Array(values) => values.into_iter().next(),
53        scalar => Some(scalar),
54    }
55}
56
57pub fn evaluate_path(root: &Value, path: &PathExpression) -> Vec<Value> {
58    let mut current: Vec<Value> = vec![root.clone()];
59
60    for segment in &path.segments {
61        let mut next = Vec::new();
62
63        for node in current {
64            let mut values = Vec::new();
65            if let Some(name) = &segment.name {
66                if let Value::Object(map) = node
67                    && let Some(value) = map.get(name)
68                {
69                    values.push(value.clone());
70                }
71            } else {
72                values.push(node);
73            }
74
75            for selector in &segment.selectors {
76                values = apply_selector(values, selector);
77                if values.is_empty() {
78                    break;
79                }
80            }
81
82            next.extend(values);
83        }
84
85        current = next;
86        if current.is_empty() {
87            break;
88        }
89    }
90
91    current
92}
93
94pub fn enumerate_path_values(root: &Value, path: &PathExpression) -> Vec<(String, Value)> {
95    if path.segments.is_empty() {
96        return Vec::new();
97    }
98
99    let mut out = Vec::new();
100    traverse_path(root, path, 0, String::new(), &mut out);
101    out
102}
103
104pub fn resolve_pairs(flat_row: &Row, token: &str) -> (Vec<(String, Value)>, bool) {
105    let trimmed = token.trim();
106    if trimmed.is_empty() {
107        return (Vec::new(), false);
108    }
109
110    let expr = parse_path(trimmed).ok();
111    if let Some(expr) = expr
112        && !expr.segments.is_empty()
113    {
114        let materialized = requires_materialization(&expr);
115        let nested = Value::Object(coalesce_flat_row(flat_row));
116        let pairs = enumerate_path_values(&nested, &expr);
117        if materialized || !pairs.is_empty() {
118            return (pairs, materialized);
119        }
120    }
121
122    let matched = match_row_keys(flat_row, trimmed, ExactMode::None);
123    if !matched.is_empty() {
124        let pairs = matched
125            .into_iter()
126            .filter_map(|key| {
127                flat_row
128                    .get(key)
129                    .cloned()
130                    .map(|value| (key.to_string(), value))
131            })
132            .collect::<Vec<_>>();
133        return (pairs, false);
134    }
135
136    // Nothing matched at all: materialize the whole flat row so downstream
137    // quick-stage rendering can still project something useful.
138    let pairs = flat_row
139        .iter()
140        .map(|(key, value)| (key.clone(), value.clone()))
141        .collect::<Vec<_>>();
142    (pairs, true)
143}
144
145fn apply_selector(values: Vec<Value>, selector: &Selector) -> Vec<Value> {
146    match selector {
147        Selector::Fanout => values
148            .into_iter()
149            .flat_map(|value| match value {
150                Value::Array(items) => items,
151                _ => Vec::new(),
152            })
153            .collect(),
154        Selector::Index(index) => values
155            .into_iter()
156            .filter_map(|value| match value {
157                Value::Array(items) => {
158                    let len = items.len() as i64;
159                    let idx = if *index < 0 { len + index } else { *index };
160                    if idx >= 0 {
161                        items.get(idx as usize).cloned()
162                    } else {
163                        None
164                    }
165                }
166                _ => None,
167            })
168            .collect(),
169        Selector::Slice { start, stop, step } => values
170            .into_iter()
171            .flat_map(|value| match value {
172                Value::Array(items) => slice_indices(items.len() as i64, *start, *stop, *step)
173                    .into_iter()
174                    .filter_map(|index| items.get(index as usize).cloned())
175                    .collect(),
176                _ => Vec::new(),
177            })
178            .collect(),
179    }
180}
181
182fn dedup_values(values: Vec<Value>) -> Vec<Value> {
183    let mut seen = HashSet::new();
184    let mut out = Vec::new();
185
186    for value in values {
187        let Ok(key) = serde_json::to_string(&value) else {
188            continue;
189        };
190        if seen.insert(key) {
191            out.push(value);
192        }
193    }
194
195    out
196}
197
198fn traverse_path(
199    root: &Value,
200    path: &PathExpression,
201    segment_index: usize,
202    flat_key: String,
203    out: &mut Vec<(String, Value)>,
204) {
205    if segment_index == path.segments.len() {
206        out.push((flat_key, root.clone()));
207        return;
208    }
209
210    let segment = &path.segments[segment_index];
211    let mut current: Vec<(Value, String)> = vec![(root.clone(), flat_key)];
212
213    if let Some(name) = &segment.name {
214        let mut next = Vec::new();
215        for (value, key) in current {
216            if let Value::Object(map) = value
217                && let Some(child) = map.get(name)
218            {
219                next.push((child.clone(), append_name(&key, name)));
220            }
221        }
222        current = next;
223    }
224
225    for selector in &segment.selectors {
226        current = apply_selector_pairs(current, selector);
227        if current.is_empty() {
228            return;
229        }
230    }
231
232    for (value, key) in current {
233        traverse_path(&value, path, segment_index + 1, key, out);
234    }
235}
236
237fn apply_selector_pairs(values: Vec<(Value, String)>, selector: &Selector) -> Vec<(Value, String)> {
238    let mut out = Vec::new();
239    for (value, key) in values {
240        let items = match value {
241            Value::Array(items) => items,
242            _ => Vec::new(),
243        };
244        let len = items.len() as i64;
245        match selector {
246            Selector::Fanout => {
247                for (index, item) in items.into_iter().enumerate() {
248                    out.push((item, append_index(&key, index)));
249                }
250            }
251            Selector::Index(index) => {
252                let mut idx = *index;
253                if idx < 0 {
254                    idx += len;
255                }
256                if idx >= 0
257                    && let Some(item) = items.get(idx as usize)
258                {
259                    out.push((item.clone(), append_index(&key, idx as usize)));
260                }
261            }
262            Selector::Slice { start, stop, step } => {
263                let indices = slice_indices(len, *start, *stop, *step);
264                for idx in indices {
265                    if let Some(item) = items.get(idx as usize) {
266                        out.push((item.clone(), append_index(&key, idx as usize)));
267                    }
268                }
269            }
270        }
271    }
272    out
273}
274
275fn slice_indices(len: i64, start: Option<i64>, stop: Option<i64>, step: Option<i64>) -> Vec<i64> {
276    let step = step.unwrap_or(1);
277    if step == 0 {
278        return Vec::new();
279    }
280
281    let mut out = Vec::new();
282    if step > 0 {
283        let mut index = start.unwrap_or(0);
284        if index < 0 {
285            index += len;
286        }
287        index = index.clamp(0, len);
288
289        let mut stop_index = stop.unwrap_or(len);
290        if stop_index < 0 {
291            stop_index += len;
292        }
293        stop_index = stop_index.clamp(0, len);
294
295        while index < stop_index {
296            if index >= 0 {
297                out.push(index);
298            }
299            index += step;
300        }
301    } else {
302        let mut index = start.unwrap_or(len - 1);
303        if index < 0 {
304            index += len;
305        }
306        index = index.clamp(-1, len - 1);
307
308        let stop_index = match stop {
309            Some(stop_value) => {
310                let mut normalized = stop_value;
311                if normalized < 0 {
312                    normalized += len;
313                }
314                normalized.clamp(-1, len - 1)
315            }
316            None => -1,
317        };
318
319        while index > stop_index {
320            if index >= 0 {
321                out.push(index);
322            }
323            index += step;
324        }
325    }
326
327    out
328}
329
330fn append_name(base: &str, name: &str) -> String {
331    if base.is_empty() {
332        name.to_string()
333    } else {
334        format!("{base}.{name}")
335    }
336}
337
338fn append_index(base: &str, index: usize) -> String {
339    if base.is_empty() {
340        format!("[{index}]")
341    } else {
342        format!("{base}[{index}]")
343    }
344}
345
346pub fn is_truthy(value: &Value) -> bool {
347    match value {
348        Value::Null => false,
349        Value::Bool(flag) => *flag,
350        Value::Number(number) => number.as_f64().is_some_and(|value| value != 0.0),
351        Value::String(text) => !text.is_empty(),
352        Value::Array(values) => !values.is_empty(),
353        Value::Object(map) => !map.is_empty(),
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use serde_json::json;
360
361    use crate::dsl::parse::{key_spec::ExactMode, path::parse_path};
362
363    use super::{
364        enumerate_path_values, evaluate_path, is_truthy, resolve_first_value, resolve_pairs,
365        resolve_values, slice_indices,
366    };
367
368    #[test]
369    fn resolve_values_prefers_direct_path_then_fuzzy_fallback() {
370        let row = json!({"metadata": {"asset": {"id": 42}}, "id": 7})
371            .as_object()
372            .cloned()
373            .expect("object");
374
375        let values = resolve_values(&row, "asset.id", ExactMode::None);
376        assert_eq!(values, vec![json!(42)]);
377
378        let values = resolve_values(&row, ".asset.id", ExactMode::None);
379        assert!(values.is_empty());
380    }
381
382    #[test]
383    fn evaluate_path_handles_fanout_and_slice() {
384        let root = json!({"items": [{"id": 1}, {"id": 2}, {"id": 3}]});
385
386        let path = parse_path("items[].id").expect("path should parse");
387        assert_eq!(
388            evaluate_path(&root, &path),
389            vec![json!(1), json!(2), json!(3)]
390        );
391
392        let path = parse_path("items[:2].id").expect("path should parse");
393        assert_eq!(evaluate_path(&root, &path), vec![json!(1), json!(2)]);
394
395        let path = parse_path("items[::-1].id").expect("path should parse");
396        assert_eq!(
397            evaluate_path(&root, &path),
398            vec![json!(3), json!(2), json!(1)]
399        );
400    }
401
402    #[test]
403    fn slice_indices_handles_forward_and_reverse_ranges_consistently() {
404        assert_eq!(slice_indices(5, Some(1), Some(4), Some(1)), vec![1, 2, 3]);
405        assert_eq!(slice_indices(5, None, None, Some(-1)), vec![4, 3, 2, 1, 0]);
406        assert_eq!(slice_indices(5, Some(-3), None, Some(1)), vec![2, 3, 4]);
407        assert_eq!(slice_indices(0, None, None, Some(-1)), Vec::<i64>::new());
408        assert_eq!(slice_indices(5, None, None, Some(0)), Vec::<i64>::new());
409    }
410
411    #[test]
412    fn truthy_rules_match_dsl_expectations() {
413        assert!(!is_truthy(&json!(null)));
414        assert!(!is_truthy(&json!("")));
415        assert!(!is_truthy(&json!([])));
416        assert!(is_truthy(&json!("x")));
417        assert!(is_truthy(&json!([1])));
418    }
419
420    #[test]
421    fn resolve_first_value_unwraps_arrays_and_dedups_results() {
422        let row = json!({
423            "items": [{"id": 7}, {"id": 7}],
424            "dup": [1, 1]
425        })
426        .as_object()
427        .cloned()
428        .expect("object");
429
430        assert_eq!(
431            resolve_first_value(&row, "items[].id", ExactMode::None),
432            Some(json!(7))
433        );
434        assert_eq!(
435            resolve_values(&row, "dup[]", ExactMode::None),
436            vec![json!(1)]
437        );
438    }
439
440    #[test]
441    fn resolve_pairs_handles_path_flat_fallback_and_full_materialization() {
442        let flat = json!({
443            "items[0].id": 1,
444            "items[1].id": 2,
445            "flat.value": "x"
446        })
447        .as_object()
448        .cloned()
449        .expect("object");
450
451        let (path_pairs, materialized) = resolve_pairs(&flat, "items[].id");
452        assert!(!materialized);
453        assert_eq!(
454            path_pairs,
455            vec![
456                ("items[0].id".to_string(), json!(1)),
457                ("items[1].id".to_string(), json!(2))
458            ]
459        );
460
461        let (materialized_pairs, materialized) = resolve_pairs(&flat, "items[-1].id");
462        assert!(materialized);
463        assert_eq!(
464            materialized_pairs,
465            vec![("items[1].id".to_string(), json!(2))]
466        );
467
468        let (flat_pairs, materialized) = resolve_pairs(&flat, "flat.value");
469        assert!(!materialized);
470        assert_eq!(flat_pairs, vec![("flat.value".to_string(), json!("x"))]);
471
472        let (fallback_pairs, materialized) = resolve_pairs(&flat, "missing");
473        assert!(materialized);
474        assert_eq!(fallback_pairs.len(), 3);
475    }
476
477    #[test]
478    fn enumerate_paths_and_selectors_cover_negative_indexes() {
479        let root = json!({"items": [{"id": 1}, {"id": 2}, {"id": 3}]});
480        let path = parse_path("items[-1].id").expect("path should parse");
481        assert_eq!(evaluate_path(&root, &path), vec![json!(3)]);
482
483        let enumerated = enumerate_path_values(
484            &root,
485            &parse_path("items[:2].id").expect("path should parse"),
486        );
487        assert_eq!(
488            enumerated,
489            vec![
490                ("items[0].id".to_string(), json!(1)),
491                ("items[1].id".to_string(), json!(2))
492            ]
493        );
494    }
495}