mik_sql/builder/
parse.rs

1//! Runtime JSON parsing for Mongo-style filters.
2//!
3//! Parse user-provided JSON into `FilterExpr` at runtime, using the same
4//! Mongo-style syntax as the compile-time macros.
5//!
6//! # Quick Start
7//!
8//! ```
9//! use mik_sql::prelude::*;
10//!
11//! // Parse from JSON string
12//! let filter = parse_filter(r#"{"name": "Alice", "age": {"$gte": 18}}"#).unwrap();
13//! ```
14//!
15//! # Usage with mik-sdk Request
16//!
17//! ```ignore
18//! use mik_sdk::prelude::*;
19//! use mik_sql::{parse_filter, sql_read};
20//!
21//! fn search(query: Pagination, req: &Request) -> Response {
22//!     // Extract body as text, early return if missing
23//!     let body = ensure!(req.text(), 400, "Filter body required");
24//!
25//!     // Parse as filter, early return if invalid
26//!     let filter = ensure!(parse_filter(body), 400, "Invalid filter");
27//!
28//!     // Merge with trusted filter, validate against whitelist
29//!     let (sql, params) = ensure!(sql_read!(users {
30//!         select: [id, name, email],
31//!         filter: { active: true },
32//!         merge: filter,
33//!         allow: [name, email, status],
34//!         page: query.page,
35//!         limit: query.limit,
36//!     }), 400, "Invalid filter field");
37//!
38//!     // Execute query...
39//!     ok!({ "sql": sql })
40//! }
41//! ```
42//!
43//! # Supported Syntax
44//!
45//! | Syntax | Example | SQL |
46//! |--------|---------|-----|
47//! | Implicit `$eq` | `{"name": "Alice"}` | `name = 'Alice'` |
48//! | Explicit operator | `{"age": {"$gte": 18}}` | `age >= 18` |
49//! | Multiple fields | `{"a": 1, "b": 2}` | `a = 1 AND b = 2` |
50//! | `$and` | `{"$and": [{...}, {...}]}` | `(...) AND (...)` |
51//! | `$or` | `{"$or": [{...}, {...}]}` | `(...) OR (...)` |
52//! | `$not` | `{"$not": {...}}` | `NOT (...)` |
53//! | `$in` | `{"status": {"$in": ["a", "b"]}}` | `status IN ('a', 'b')` |
54//! | `$between` | `{"age": {"$between": [18, 65]}}` | `age BETWEEN 18 AND 65` |
55
56use super::types::{CompoundFilter, Filter, FilterExpr, Operator, Value};
57use miniserde::json::{Number, Value as JsonValue};
58use std::fmt;
59
60/// Error type for JSON filter parsing.
61#[derive(Debug, Clone, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum ParseError {
64    /// Invalid JSON syntax or encoding.
65    InvalidJson,
66    /// Unknown operator (e.g., `$foo`).
67    UnknownOperator(String),
68    /// Expected an object but got something else.
69    ExpectedObject,
70    /// Expected an array but got something else.
71    ExpectedArray,
72    /// Expected a value but got something else.
73    ExpectedValue,
74    /// Field name is empty.
75    EmptyFieldName,
76    /// Filter object is empty.
77    EmptyFilter,
78    /// Invalid operator value type.
79    InvalidOperatorValue {
80        /// The operator that had the wrong value type.
81        op: String,
82        /// Description of what was expected.
83        expected: &'static str,
84    },
85    /// $not requires exactly one condition.
86    NotRequiresOneCondition,
87}
88
89/// Parse a Mongo-style filter from a JSON string.
90///
91/// This is a convenience function that calls [`FilterExpr::parse`].
92///
93/// # Example
94///
95/// ```
96/// use mik_sql::prelude::*;
97///
98/// // Simple filter
99/// let filter = parse_filter(r#"{"active": true}"#).unwrap();
100///
101/// // Complex filter with operators
102/// let filter = parse_filter(r#"{
103///     "status": {"$in": ["active", "pending"]},
104///     "age": {"$gte": 18}
105/// }"#).unwrap();
106///
107/// // Logical operators
108/// let filter = parse_filter(r#"{
109///     "$or": [
110///         {"role": "admin"},
111///         {"role": "moderator"}
112///     ]
113/// }"#).unwrap();
114/// ```
115///
116/// # Errors
117///
118/// Returns `ParseError` if the JSON is invalid.
119pub fn parse_filter(json_str: &str) -> Result<FilterExpr, ParseError> {
120    FilterExpr::parse(json_str)
121}
122
123impl fmt::Display for ParseError {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        match self {
126            Self::InvalidJson => write!(f, "Invalid JSON syntax or encoding"),
127            Self::UnknownOperator(op) => write!(f, "Unknown operator '{op}'"),
128            Self::ExpectedObject => write!(f, "Expected JSON object"),
129            Self::ExpectedArray => write!(f, "Expected JSON array"),
130            Self::ExpectedValue => write!(f, "Expected a value"),
131            Self::EmptyFieldName => write!(f, "Field name cannot be empty"),
132            Self::EmptyFilter => write!(f, "Filter object cannot be empty"),
133            Self::InvalidOperatorValue { op, expected } => {
134                write!(f, "Operator '{op}' expects {expected}")
135            },
136            Self::NotRequiresOneCondition => {
137                write!(f, "$not requires exactly one condition")
138            },
139        }
140    }
141}
142
143impl std::error::Error for ParseError {}
144
145impl Operator {
146    /// Parse from Mongo-style operator string (e.g., "$eq", "$gte").
147    ///
148    /// Accepts both with and without the `$` prefix.
149    ///
150    /// # Example
151    ///
152    /// ```
153    /// use mik_sql::Operator;
154    ///
155    /// assert_eq!(Operator::from_mongo("$eq"), Some(Operator::Eq));
156    /// assert_eq!(Operator::from_mongo("gte"), Some(Operator::Gte));
157    /// assert_eq!(Operator::from_mongo("$unknown"), None);
158    /// ```
159    #[must_use]
160    pub fn from_mongo(s: &str) -> Option<Self> {
161        // Strip leading $ if present
162        let s = s.strip_prefix('$').unwrap_or(s);
163
164        match s {
165            "eq" => Some(Self::Eq),
166            "ne" => Some(Self::Ne),
167            "gt" => Some(Self::Gt),
168            "gte" => Some(Self::Gte),
169            "lt" => Some(Self::Lt),
170            "lte" => Some(Self::Lte),
171            "in" => Some(Self::In),
172            "nin" => Some(Self::NotIn),
173            "like" => Some(Self::Like),
174            "ilike" => Some(Self::ILike),
175            "regex" => Some(Self::Regex),
176            "startsWith" | "starts_with" => Some(Self::StartsWith),
177            "endsWith" | "ends_with" => Some(Self::EndsWith),
178            "contains" => Some(Self::Contains),
179            "between" => Some(Self::Between),
180            _ => None,
181        }
182    }
183}
184
185impl Value {
186    /// Convert from miniserde JSON value.
187    ///
188    /// # Example
189    ///
190    /// ```
191    /// use mik_sql::Value;
192    /// use miniserde::json::{Value as JsonValue, Number};
193    ///
194    /// let json = JsonValue::String("hello".to_string());
195    /// assert_eq!(Value::from_json(&json), Some(Value::String("hello".to_string())));
196    ///
197    /// let json = JsonValue::Number(Number::I64(42));
198    /// assert_eq!(Value::from_json(&json), Some(Value::Int(42)));
199    /// ```
200    #[must_use]
201    pub fn from_json(json: &JsonValue) -> Option<Self> {
202        match json {
203            JsonValue::Null => Some(Self::Null),
204            JsonValue::Bool(b) => Some(Self::Bool(*b)),
205            JsonValue::Number(n) => match n {
206                Number::I64(i) => Some(Self::Int(*i)),
207                Number::U64(u) => i64::try_from(*u).ok().map(Self::Int),
208                Number::F64(f) => Some(Self::Float(*f)),
209            },
210            JsonValue::String(s) => Some(Self::String(s.clone())),
211            JsonValue::Array(arr) => {
212                let values: Option<Vec<Self>> = arr.iter().map(Self::from_json).collect();
213                values.map(Self::Array)
214            },
215            JsonValue::Object(_) => None, // Objects are not valid filter values
216        }
217    }
218}
219
220impl FilterExpr {
221    /// Parse a Mongo-style filter from a JSON string.
222    ///
223    /// This is the recommended way to parse user-provided filters.
224    ///
225    /// # Example
226    ///
227    /// ```
228    /// use mik_sql::FilterExpr;
229    ///
230    /// let filter = FilterExpr::parse(r#"{"name": "Alice", "active": true}"#).unwrap();
231    /// ```
232    ///
233    /// # Errors
234    ///
235    /// Returns `ParseError` if the JSON is invalid or malformed.
236    pub fn parse(json_str: &str) -> Result<Self, ParseError> {
237        let json: JsonValue =
238            miniserde::json::from_str(json_str).map_err(|_| ParseError::InvalidJson)?;
239        Self::from_json(&json)
240    }
241
242    /// Parse a Mongo-style filter from JSON bytes.
243    ///
244    /// Useful when working with raw request bodies.
245    ///
246    /// # Example
247    ///
248    /// ```
249    /// use mik_sql::FilterExpr;
250    ///
251    /// let bytes = br#"{"status": {"$in": ["active", "pending"]}}"#;
252    /// let filter = FilterExpr::parse_bytes(bytes).unwrap();
253    /// ```
254    ///
255    /// # Errors
256    ///
257    /// Returns `ParseError` if the bytes are not valid UTF-8 or valid JSON.
258    pub fn parse_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
259        let s = std::str::from_utf8(bytes).map_err(|_| ParseError::InvalidJson)?;
260        Self::parse(s)
261    }
262
263    /// Parse a Mongo-style filter from a parsed JSON value.
264    ///
265    /// Use this when you already have a parsed `miniserde::json::Value`.
266    /// For most cases, prefer [`parse`](Self::parse) or [`parse_bytes`](Self::parse_bytes).
267    ///
268    /// # Errors
269    ///
270    /// Returns `ParseError` if the JSON structure is invalid.
271    pub fn from_json(json: &JsonValue) -> Result<Self, ParseError> {
272        let obj = match json {
273            JsonValue::Object(o) => o,
274            _ => return Err(ParseError::ExpectedObject),
275        };
276
277        if obj.is_empty() {
278            return Err(ParseError::EmptyFilter);
279        }
280
281        let mut filters = Vec::new();
282
283        for (key, value) in obj {
284            if key.is_empty() {
285                return Err(ParseError::EmptyFieldName);
286            }
287
288            // Check for logical operators
289            if key.starts_with('$') {
290                match key.as_str() {
291                    "$and" => {
292                        let exprs = parse_filter_array(value)?;
293                        filters.push(Self::Compound(CompoundFilter::and(exprs)));
294                    },
295                    "$or" => {
296                        let exprs = parse_filter_array(value)?;
297                        filters.push(Self::Compound(CompoundFilter::or(exprs)));
298                    },
299                    "$not" => {
300                        let inner = Self::from_json(value)?;
301                        filters.push(Self::Compound(CompoundFilter::not(inner)));
302                    },
303                    _ => return Err(ParseError::UnknownOperator(key.clone())),
304                }
305            } else {
306                // Field filter
307                let filter = parse_field_filter(key, value)?;
308                filters.push(filter);
309            }
310        }
311
312        // Combine multiple filters with implicit AND
313        Ok(match filters.len() {
314            0 => return Err(ParseError::EmptyFilter),
315            1 => filters.remove(0),
316            _ => Self::Compound(CompoundFilter::and(filters)),
317        })
318    }
319}
320
321/// Parse an array of filter expressions (for $and/$or).
322fn parse_filter_array(json: &JsonValue) -> Result<Vec<FilterExpr>, ParseError> {
323    let arr = match json {
324        JsonValue::Array(a) => a,
325        _ => return Err(ParseError::ExpectedArray),
326    };
327
328    arr.iter().map(FilterExpr::from_json).collect()
329}
330
331/// Parse a field filter: `{"$op": value}` or just `value` (implicit $eq).
332fn parse_field_filter(field: &str, value: &JsonValue) -> Result<FilterExpr, ParseError> {
333    // Check for operator syntax: {"$eq": value}
334    if let JsonValue::Object(obj) = value {
335        if let Some((op_key, op_value)) = obj.iter().next()
336            && op_key.starts_with('$')
337        {
338            let op = Operator::from_mongo(op_key)
339                .ok_or_else(|| ParseError::UnknownOperator(op_key.clone()))?;
340
341            let val = parse_operator_value(op, op_value)?;
342
343            return Ok(FilterExpr::Simple(Filter {
344                field: field.to_string(),
345                op,
346                value: val,
347            }));
348        }
349        // Not an operator object, treat as error
350        return Err(ParseError::ExpectedValue);
351    }
352
353    // Implicit $eq
354    let val = Value::from_json(value).ok_or(ParseError::ExpectedValue)?;
355    Ok(FilterExpr::Simple(Filter {
356        field: field.to_string(),
357        op: Operator::Eq,
358        value: val,
359    }))
360}
361
362/// Parse the value for an operator, with type validation.
363fn parse_operator_value(op: Operator, value: &JsonValue) -> Result<Value, ParseError> {
364    match op {
365        // Array operators require arrays
366        Operator::In | Operator::NotIn => match value {
367            JsonValue::Array(arr) => {
368                let values: Option<Vec<Value>> = arr.iter().map(Value::from_json).collect();
369                values
370                    .map(Value::Array)
371                    .ok_or_else(|| ParseError::InvalidOperatorValue {
372                        op: format!("${op:?}").to_lowercase(),
373                        expected: "array of values",
374                    })
375            },
376            _ => Err(ParseError::InvalidOperatorValue {
377                op: "$in/$nin".to_string(),
378                expected: "array",
379            }),
380        },
381
382        // Between requires array of exactly 2 values
383        Operator::Between => match value {
384            JsonValue::Array(arr) if arr.len() == 2 => {
385                let values: Option<Vec<Value>> = arr.iter().map(Value::from_json).collect();
386                values
387                    .map(Value::Array)
388                    .ok_or_else(|| ParseError::InvalidOperatorValue {
389                        op: "$between".to_string(),
390                        expected: "array of 2 values",
391                    })
392            },
393            JsonValue::Array(_) => Err(ParseError::InvalidOperatorValue {
394                op: "$between".to_string(),
395                expected: "array of exactly 2 values",
396            }),
397            _ => Err(ParseError::InvalidOperatorValue {
398                op: "$between".to_string(),
399                expected: "array of 2 values",
400            }),
401        },
402
403        // All other operators accept scalar values
404        _ => Value::from_json(value).ok_or(ParseError::ExpectedValue),
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411    use crate::LogicalOp;
412    use miniserde::json::{self, Array as JsonArray};
413
414    // =========================================================================
415    // Operator::from_mongo tests
416    // =========================================================================
417
418    #[test]
419    fn test_operator_from_mongo_with_prefix() {
420        assert_eq!(Operator::from_mongo("$eq"), Some(Operator::Eq));
421        assert_eq!(Operator::from_mongo("$ne"), Some(Operator::Ne));
422        assert_eq!(Operator::from_mongo("$gt"), Some(Operator::Gt));
423        assert_eq!(Operator::from_mongo("$gte"), Some(Operator::Gte));
424        assert_eq!(Operator::from_mongo("$lt"), Some(Operator::Lt));
425        assert_eq!(Operator::from_mongo("$lte"), Some(Operator::Lte));
426        assert_eq!(Operator::from_mongo("$in"), Some(Operator::In));
427        assert_eq!(Operator::from_mongo("$nin"), Some(Operator::NotIn));
428        assert_eq!(Operator::from_mongo("$like"), Some(Operator::Like));
429        assert_eq!(Operator::from_mongo("$ilike"), Some(Operator::ILike));
430        assert_eq!(Operator::from_mongo("$regex"), Some(Operator::Regex));
431        assert_eq!(Operator::from_mongo("$between"), Some(Operator::Between));
432    }
433
434    #[test]
435    fn test_operator_from_mongo_without_prefix() {
436        assert_eq!(Operator::from_mongo("eq"), Some(Operator::Eq));
437        assert_eq!(Operator::from_mongo("gte"), Some(Operator::Gte));
438    }
439
440    #[test]
441    fn test_operator_from_mongo_camel_case() {
442        assert_eq!(
443            Operator::from_mongo("$startsWith"),
444            Some(Operator::StartsWith)
445        );
446        assert_eq!(
447            Operator::from_mongo("$starts_with"),
448            Some(Operator::StartsWith)
449        );
450        assert_eq!(Operator::from_mongo("$endsWith"), Some(Operator::EndsWith));
451        assert_eq!(Operator::from_mongo("$ends_with"), Some(Operator::EndsWith));
452    }
453
454    #[test]
455    fn test_operator_from_mongo_unknown() {
456        assert_eq!(Operator::from_mongo("$unknown"), None);
457        assert_eq!(Operator::from_mongo("$foo"), None);
458    }
459
460    // =========================================================================
461    // Value::from_json tests
462    // =========================================================================
463
464    #[test]
465    fn test_value_from_json_primitives() {
466        assert_eq!(Value::from_json(&JsonValue::Null), Some(Value::Null));
467        assert_eq!(
468            Value::from_json(&JsonValue::Bool(true)),
469            Some(Value::Bool(true))
470        );
471        assert_eq!(
472            Value::from_json(&JsonValue::Number(Number::I64(42))),
473            Some(Value::Int(42))
474        );
475        assert_eq!(
476            Value::from_json(&JsonValue::Number(Number::F64(2.5))),
477            Some(Value::Float(2.5))
478        );
479        assert_eq!(
480            Value::from_json(&JsonValue::String("hello".into())),
481            Some(Value::String("hello".into()))
482        );
483    }
484
485    #[test]
486    fn test_value_from_json_array() {
487        let mut arr = JsonArray::new();
488        arr.push(JsonValue::String("a".into()));
489        arr.push(JsonValue::String("b".into()));
490        let json_arr = JsonValue::Array(arr);
491        assert_eq!(
492            Value::from_json(&json_arr),
493            Some(Value::Array(vec![
494                Value::String("a".into()),
495                Value::String("b".into()),
496            ]))
497        );
498    }
499
500    // =========================================================================
501    // FilterExpr::from_json tests
502    // =========================================================================
503
504    #[test]
505    fn test_simple_equality() {
506        let json: JsonValue = json::from_str(r#"{"name": "Alice"}"#).unwrap();
507        let filter = FilterExpr::from_json(&json).unwrap();
508
509        assert!(matches!(
510            filter,
511            FilterExpr::Simple(Filter {
512                ref field,
513                op: Operator::Eq,
514                value: Value::String(ref s),
515            }) if field == "name" && s == "Alice"
516        ));
517    }
518
519    #[test]
520    fn test_explicit_operator() {
521        let json: JsonValue = json::from_str(r#"{"age": {"$gte": 18}}"#).unwrap();
522        let filter = FilterExpr::from_json(&json).unwrap();
523
524        assert!(matches!(
525            filter,
526            FilterExpr::Simple(Filter {
527                ref field,
528                op: Operator::Gte,
529                value: Value::Int(18),
530            }) if field == "age"
531        ));
532    }
533
534    #[test]
535    fn test_multiple_fields_implicit_and() {
536        let json: JsonValue = json::from_str(r#"{"name": "Alice", "age": 30}"#).unwrap();
537        let filter = FilterExpr::from_json(&json).unwrap();
538
539        assert!(matches!(
540            filter,
541            FilterExpr::Compound(CompoundFilter {
542                op: LogicalOp::And,
543                ..
544            })
545        ));
546    }
547
548    #[test]
549    fn test_explicit_and() {
550        let json: JsonValue =
551            json::from_str(r#"{"$and": [{"name": "Alice"}, {"age": 30}]}"#).unwrap();
552        let filter = FilterExpr::from_json(&json).unwrap();
553
554        assert!(matches!(
555            filter,
556            FilterExpr::Compound(CompoundFilter {
557                op: LogicalOp::And,
558                ..
559            })
560        ));
561    }
562
563    #[test]
564    fn test_explicit_or() {
565        let json: JsonValue =
566            json::from_str(r#"{"$or": [{"status": "active"}, {"status": "pending"}]}"#).unwrap();
567        let filter = FilterExpr::from_json(&json).unwrap();
568
569        assert!(matches!(
570            filter,
571            FilterExpr::Compound(CompoundFilter {
572                op: LogicalOp::Or,
573                ..
574            })
575        ));
576    }
577
578    #[test]
579    fn test_explicit_not() {
580        let json: JsonValue = json::from_str(r#"{"$not": {"deleted": true}}"#).unwrap();
581        let filter = FilterExpr::from_json(&json).unwrap();
582
583        assert!(matches!(
584            filter,
585            FilterExpr::Compound(CompoundFilter {
586                op: LogicalOp::Not,
587                ..
588            })
589        ));
590    }
591
592    #[test]
593    fn test_in_operator() {
594        let json: JsonValue = json::from_str(r#"{"status": {"$in": ["a", "b", "c"]}}"#).unwrap();
595        let filter = FilterExpr::from_json(&json).unwrap();
596
597        assert!(matches!(
598            filter,
599            FilterExpr::Simple(Filter {
600                op: Operator::In,
601                value: Value::Array(_),
602                ..
603            })
604        ));
605    }
606
607    #[test]
608    fn test_between_operator() {
609        let json: JsonValue = json::from_str(r#"{"age": {"$between": [18, 65]}}"#).unwrap();
610        let filter = FilterExpr::from_json(&json).unwrap();
611
612        assert!(matches!(
613            filter,
614            FilterExpr::Simple(Filter {
615                op: Operator::Between,
616                value: Value::Array(ref arr),
617                ..
618            }) if arr.len() == 2
619        ));
620    }
621
622    #[test]
623    fn test_nested_logical() {
624        let json: JsonValue = json::from_str(
625            r#"{"$and": [{"active": true}, {"$or": [{"role": "admin"}, {"role": "mod"}]}]}"#,
626        )
627        .unwrap();
628        let filter = FilterExpr::from_json(&json).unwrap();
629
630        assert!(matches!(
631            filter,
632            FilterExpr::Compound(CompoundFilter {
633                op: LogicalOp::And,
634                ..
635            })
636        ));
637    }
638
639    // =========================================================================
640    // Error cases
641    // =========================================================================
642
643    #[test]
644    fn test_error_not_object() {
645        let json: JsonValue = json::from_str(r"[1, 2, 3]").unwrap();
646        assert!(matches!(
647            FilterExpr::from_json(&json),
648            Err(ParseError::ExpectedObject)
649        ));
650    }
651
652    #[test]
653    fn test_error_empty_filter() {
654        let json: JsonValue = json::from_str(r"{}").unwrap();
655        assert!(matches!(
656            FilterExpr::from_json(&json),
657            Err(ParseError::EmptyFilter)
658        ));
659    }
660
661    #[test]
662    fn test_error_unknown_operator() {
663        let json: JsonValue = json::from_str(r#"{"field": {"$foo": 1}}"#).unwrap();
664        assert!(matches!(
665            FilterExpr::from_json(&json),
666            Err(ParseError::UnknownOperator(_))
667        ));
668    }
669
670    #[test]
671    fn test_error_between_wrong_count() {
672        let json: JsonValue = json::from_str(r#"{"age": {"$between": [18]}}"#).unwrap();
673        assert!(matches!(
674            FilterExpr::from_json(&json),
675            Err(ParseError::InvalidOperatorValue { .. })
676        ));
677    }
678
679    #[test]
680    fn test_error_in_not_array() {
681        let json: JsonValue = json::from_str(r#"{"status": {"$in": "not-array"}}"#).unwrap();
682        assert!(matches!(
683            FilterExpr::from_json(&json),
684            Err(ParseError::InvalidOperatorValue { .. })
685        ));
686    }
687}