Skip to main content

patchloom/selector/
mod.rs

1pub mod eval;
2pub mod parser;
3
4pub use eval::eval;
5pub use parser::{Segment, Selector, parse};
6
7/// Parse a selector string, mapping parse errors to `anyhow::Error` with
8/// a "selector error:" prefix for consistent error formatting.
9pub fn parse_anyhow(input: &str) -> anyhow::Result<Selector> {
10    parse(input).map_err(|e| {
11        anyhow::Error::new(crate::exit::InvalidInputError {
12            msg: format!("selector error: {e}"),
13        })
14    })
15}
16
17/// Navigate a dotted path like `"settings.theme"` into a JSON value.
18///
19/// For flat keys (no dots), this is equivalent to `value.get(key)`.
20/// For dotted keys, it first tries a direct `get(key)` to handle literal
21/// dot-containing keys (e.g. `"my.key"`), then falls back to walking
22/// each dot-separated segment. On ties, the first-found result wins
23/// (direct lookup takes priority).
24pub fn get_nested<'a>(value: &'a serde_json::Value, key: &str) -> Option<&'a serde_json::Value> {
25    // Fast path: no dots means plain key lookup.
26    if !key.contains('.') {
27        return value.get(key);
28    }
29    // Try direct lookup first (handles literal-dot keys like "my.key").
30    if let Some(v) = value.get(key) {
31        return Some(v);
32    }
33    // Fall back to dotted path traversal.
34    let mut current = value;
35    for segment in key.split('.') {
36        current = current.get(segment)?;
37    }
38    Some(current)
39}
40
41/// Check whether a JSON value matches a predicate string using string comparison.
42/// Numbers and booleans are compared via their string representation.
43pub fn value_matches_str(field: &serde_json::Value, pred_val: &str) -> bool {
44    match field {
45        serde_json::Value::String(s) => s == pred_val,
46        serde_json::Value::Number(n) => n.to_string() == pred_val,
47        serde_json::Value::Bool(b) => b.to_string() == pred_val,
48        serde_json::Value::Null => pred_val == "null",
49        _ => false,
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use serde_json::json;
57
58    #[test]
59    fn value_matches_str_string() {
60        assert!(value_matches_str(&json!("hello"), "hello"));
61        assert!(!value_matches_str(&json!("hello"), "world"));
62    }
63
64    #[test]
65    fn value_matches_str_number() {
66        assert!(value_matches_str(&json!(42), "42"));
67        assert!(!value_matches_str(&json!(42), "43"));
68    }
69
70    #[test]
71    fn value_matches_str_bool() {
72        assert!(value_matches_str(&json!(true), "true"));
73        assert!(value_matches_str(&json!(false), "false"));
74        assert!(!value_matches_str(&json!(true), "false"));
75    }
76
77    /// Null values match the string "null" (#1164).
78    #[test]
79    fn value_matches_str_null_matches_null_string() {
80        assert!(value_matches_str(&json!(null), "null"));
81        assert!(!value_matches_str(&json!(null), "other"));
82    }
83
84    #[test]
85    fn get_nested_flat_key() {
86        let data = json!({"name": "Alice"});
87        assert_eq!(get_nested(&data, "name"), Some(&json!("Alice")));
88    }
89
90    #[test]
91    fn get_nested_dotted_path() {
92        let data = json!({"settings": {"theme": "dark"}});
93        assert_eq!(get_nested(&data, "settings.theme"), Some(&json!("dark")));
94    }
95
96    #[test]
97    fn get_nested_deep_path() {
98        let data = json!({"a": {"b": {"c": 42}}});
99        assert_eq!(get_nested(&data, "a.b.c"), Some(&json!(42)));
100    }
101
102    #[test]
103    fn get_nested_literal_dot_key_takes_priority() {
104        // A key literally named "a.b" should match before dotted traversal.
105        let data = json!({"a.b": "literal", "a": {"b": "nested"}});
106        assert_eq!(get_nested(&data, "a.b"), Some(&json!("literal")));
107    }
108
109    #[test]
110    fn get_nested_missing_returns_none() {
111        let data = json!({"a": {"b": 1}});
112        assert_eq!(get_nested(&data, "a.c"), None);
113    }
114
115    #[test]
116    fn value_matches_str_object_returns_false() {
117        assert!(!value_matches_str(&json!({"a": 1}), ""));
118    }
119}