Skip to main content

parse_rust_rest/
query_parse.rs

1//! Parsing the `where` parameter into constraints.
2//!
3//! A `where` value is `{"field": <literal>}` for equality, or `{"field": {"$op": <value>}}` for
4//! everything else. An operator document may carry several operators, which is how a range is
5//! expressed.
6//!
7//! **Anything unsupported is an error.** Silently ignoring an operator returns more rows than the
8//! caller asked for, which is an authorization failure rather than a missing feature, and it is
9//! the failure mode this rule exists to prevent.
10
11use parse_rust_core::{classify, ParseError};
12use parse_rust_storage::{Comparison, Constraint};
13use serde_json::Value as Json;
14
15/// Parse a decoded `where` object into constraints.
16pub fn parse_where(where_json: &Json) -> Result<Vec<Constraint>, ParseError> {
17    let Json::Object(map) = where_json else {
18        return Err(ParseError::invalid_query(
19            "where must be an object".to_string(),
20        ));
21    };
22
23    let mut out = Vec::new();
24    for (field, value) in map {
25        // A top-level `$` key is a query-level operator such as `$or`. Out of scope for 0.1.0,
26        // and refused rather than treated as a field name, which would match nothing and look
27        // like an empty result rather than an unsupported query.
28        if field.starts_with('$') {
29            return Err(ParseError::invalid_query(format!(
30                "unsupported query operator: {field}"
31            )));
32        }
33
34        match value {
35            // An operator document, unless it is a tagged Parse value like a Pointer or Date.
36            Json::Object(inner) if is_operator_document(inner) => {
37                for (op, operand) in inner {
38                    let operand = classify(operand.clone())?;
39                    out.push(Constraint {
40                        field: field.clone(),
41                        comparison: Comparison::from_operator(op, operand)?,
42                    });
43                }
44            }
45            literal => out.push(Constraint {
46                field: field.clone(),
47                comparison: Comparison::Equal(classify(literal.clone())?),
48            }),
49        }
50    }
51    Ok(out)
52}
53
54/// Is this object a set of `$` operators rather than a literal value?
55///
56/// The distinction matters because `{"__type":"Pointer",...}` is a literal and
57/// `{"$gt":3}` is not. Upstream decides the same way: it looks for `$`-prefixed keys.
58fn is_operator_document(map: &serde_json::Map<String, Json>) -> bool {
59    !map.is_empty() && map.keys().all(|k| k.starts_with('$'))
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use parse_rust_core::ParseValue;
66
67    fn j(s: &str) -> Json {
68        serde_json::from_str(s).expect("test literal")
69    }
70
71    #[test]
72    fn a_bare_value_is_equality() {
73        let c = parse_where(&j(r#"{"title":"hello"}"#)).expect("parse");
74        assert_eq!(c.len(), 1);
75        assert_eq!(c[0].field, "title");
76        assert!(matches!(
77            &c[0].comparison,
78            Comparison::Equal(ParseValue::String(s)) if s == "hello"
79        ));
80    }
81
82    #[test]
83    fn several_operators_on_one_field_become_several_constraints() {
84        // The storage layer merges these into a range. Producing one constraint here and letting
85        // the adapter merge keeps the "constraints never overwrite" property in one place.
86        let c = parse_where(&j(r#"{"views":{"$gt":1,"$lt":9}}"#)).expect("parse");
87        assert_eq!(c.len(), 2);
88        assert!(c.iter().all(|x| x.field == "views"));
89    }
90
91    #[test]
92    fn a_tagged_value_is_a_literal_not_an_operator_document() {
93        let c = parse_where(&j(
94            r#"{"author":{"__type":"Pointer","className":"_User","objectId":"u1"}}"#,
95        ))
96        .expect("parse");
97        assert_eq!(c.len(), 1);
98        assert!(matches!(
99            &c[0].comparison,
100            Comparison::Equal(ParseValue::Pointer { object_id, .. }) if object_id == "u1"
101        ));
102    }
103
104    #[test]
105    fn an_unsupported_operator_is_refused() {
106        let e = parse_where(&j(r#"{"title":{"$regex":"^a"}}"#)).unwrap_err();
107        assert_eq!(e.code, parse_rust_core::ErrorCode::InvalidQuery);
108        assert!(e.message.contains("$regex"));
109    }
110
111    /// `$or` at the top level must not be read as a field named `$or`, which would match nothing
112    /// and look like a legitimate empty result.
113    #[test]
114    fn a_top_level_operator_is_refused_rather_than_treated_as_a_field() {
115        let e = parse_where(&j(r#"{"$or":[{"a":1},{"a":2}]}"#)).unwrap_err();
116        assert_eq!(e.code, parse_rust_core::ErrorCode::InvalidQuery);
117        assert!(e.message.contains("$or"));
118    }
119
120    #[test]
121    fn in_and_exists_parse() {
122        let c =
123            parse_where(&j(r#"{"tag":{"$in":["a","b"]},"x":{"$exists":true}}"#)).expect("parse");
124        assert_eq!(c.len(), 2);
125    }
126
127    #[test]
128    fn an_empty_object_is_an_empty_query_not_an_operator_document() {
129        assert!(parse_where(&j("{}")).expect("parse").is_empty());
130        // A field set to an empty object is a literal empty object, not an operator document.
131        let c = parse_where(&j(r#"{"meta":{}}"#)).expect("parse");
132        assert!(matches!(
133            &c[0].comparison,
134            Comparison::Equal(ParseValue::Object(_))
135        ));
136    }
137
138    #[test]
139    fn where_must_be_an_object() {
140        assert!(parse_where(&j("[]")).is_err());
141        assert!(parse_where(&j("3")).is_err());
142    }
143}