rsonpath_syntax_proptest/
lib.rs

1//! Utilities for property testing with types in [`rsonpath-syntax`](https://docs.rs/rsonpath-syntax/latest/rsonpath_syntax/).
2//!
3//! Implementation of [`proptest::arbitrary::Arbitrary`]
4//! for JSONPath queries via the [`ArbitraryJsonPathQuery`] struct.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use proptest::prelude::*;
10//! use rsonpath_syntax_proptest::ArbitraryJsonPathQuery;
11//!
12//! proptest! {
13//!     #[test]
14//!     fn example(ArbitraryJsonPathQuery { parsed, string } in prop::arbitrary::any::<ArbitraryJsonPathQuery>()) {
15//!         assert_eq!(parsed, rsonpath_syntax::parse(&string)?);
16//!     }
17//! }
18//! ```
19use proptest::{option, prelude::*, strategy};
20use rsonpath_syntax::{
21    builder::SliceBuilder, num::JsonInt, str::JsonString, JsonPathQuery, LogicalExpr, Segment, Selector, Selectors,
22};
23
24/// A valid JSONPath string and the [`JsonPathQuery`] object parsed from it.
25///
26/// This is the struct through which an [`proptest::arbitrary::Arbitrary`] implementation
27/// for [`JsonPathQuery`] is provided.
28#[derive(Debug)]
29pub struct ArbitraryJsonPathQuery {
30    /// The JSONPath query string.
31    pub string: String,
32    /// The parsed JSONPath query.
33    pub parsed: JsonPathQuery,
34}
35
36/// Parameters of the [`ArbitraryJsonPathQuery`] [`Arbitrary`](`proptest::arbitrary::Arbitrary`) implementation.
37#[derive(Debug)]
38pub struct ArbitraryJsonPathQueryParams {
39    /// Depth limit for recursion for generated JSONPath queries. Default value: 3.
40    ///
41    /// JSONPath queries are recursive since a filter selector can contain an arbitrary JSONPath query.
42    /// This limits the nesting level.
43    /// See [proptest::strategy::Strategy::prop_recursive] for details of how this affects the recursive generation.
44    pub recursive_depth: u32,
45    /// Desired size in terms of tree nodes of a generated JSONPath query. Default value: 10.
46    ///
47    /// JSONPath queries are recursive since a filter selector can contain an arbitrary JSONPath query.
48    /// This limits the nesting level.
49    /// See [proptest::strategy::Strategy::prop_recursive] for details of how this affects the recursive generation.
50    pub desired_size: u32,
51    /// Limit on the number of segments in the generated query, not including the initial root `$` selector.
52    /// Default value: 10.
53    pub max_segments: usize,
54    /// Minimum number of selectors in each of the generated segments. Default value: 1.
55    ///
56    /// Must be non-zero.
57    pub min_selectors: usize,
58    /// Maximum number of selectors in each of the generated segments. Default value: 5.
59    ///
60    /// Must be at least `min_segments`.
61    pub max_selectors: usize,
62    /// Only generate query elements that are supported by the [`rsonpath`](https://docs.rs/rsonpath-lib/latest/rsonpath/) crate.
63    ///
64    /// Consult rsonpath's documentation for details on what this entails.
65    pub only_rsonpath_supported_subset: bool,
66}
67
68impl ArbitraryJsonPathQuery {
69    #[inline]
70    #[must_use]
71    pub fn new(string: String, parsed: JsonPathQuery) -> Self {
72        Self { string, parsed }
73    }
74}
75
76impl Default for ArbitraryJsonPathQueryParams {
77    #[inline]
78    fn default() -> Self {
79        Self {
80            only_rsonpath_supported_subset: false,
81            recursive_depth: 3,
82            desired_size: 10,
83            max_segments: 10,
84            min_selectors: 1,
85            max_selectors: 5,
86        }
87    }
88}
89
90impl proptest::arbitrary::Arbitrary for ArbitraryJsonPathQuery {
91    type Parameters = ArbitraryJsonPathQueryParams;
92    type Strategy = BoxedStrategy<Self>;
93
94    #[inline]
95    fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
96        assert!(args.min_selectors > 0);
97        assert!(args.max_selectors >= args.min_selectors);
98
99        if args.only_rsonpath_supported_subset {
100            rsonpath_valid_query(&args).prop_map(|x| Self::new(x.0, x.1)).boxed()
101        } else {
102            any_valid_query(&args).prop_map(|x| Self::new(x.0, x.1)).boxed()
103        }
104    }
105}
106
107/* Approach: we generate the query string bit by bit, each time attaching what the expected
108 * typed element is. At the end we have the input string all ready, and the expected
109 * parser result can be easily obtained by a 1-1 translation.
110 */
111#[derive(Debug, Clone)]
112enum PropSegment {
113    // .*
114    ShortChildWildcard,
115    // .name
116    ShortChildName(JsonString),
117    // ..*
118    ShortDescendantWildcard,
119    // ..name
120    ShortDescendantName(JsonString),
121    // [<vec>]
122    BracketedChild(Vec<PropSelector>),
123    // ..[<vec>]
124    BracketedDescendant(Vec<PropSelector>),
125}
126
127#[derive(Debug, Clone)]
128enum PropSelector {
129    Wildcard,
130    Name(JsonString),
131    Index(JsonInt),
132    Slice(Option<JsonInt>, Option<JsonInt>, Option<JsonInt>),
133    Filter(LogicalExpr),
134}
135
136fn any_valid_query(props: &ArbitraryJsonPathQueryParams) -> impl Strategy<Value = (String, JsonPathQuery)> {
137    let ArbitraryJsonPathQueryParams {
138        min_selectors,
139        max_selectors,
140        max_segments,
141        recursive_depth,
142        desired_size,
143        ..
144    } = *props;
145
146    prop::collection::vec(any_segment(None, min_selectors, max_selectors), 0..max_segments)
147        .prop_map(map_prop_segments)
148        .prop_recursive(recursive_depth, desired_size, 5, move |query_strategy| {
149            prop::collection::vec(
150                any_segment(Some(query_strategy), min_selectors, max_selectors),
151                0..max_segments,
152            )
153            .prop_map(map_prop_segments)
154        })
155}
156
157fn rsonpath_valid_query(props: &ArbitraryJsonPathQueryParams) -> impl Strategy<Value = (String, JsonPathQuery)> {
158    let ArbitraryJsonPathQueryParams { max_segments, .. } = *props;
159    prop::collection::vec(rsonpath_valid_segment(), 0..max_segments).prop_map(map_prop_segments)
160}
161
162fn map_prop_segments(segments: Vec<(String, PropSegment)>) -> (String, JsonPathQuery) {
163    let mut s = "$".to_string();
164    let mut v = vec![];
165
166    for (segment_s, segment) in segments {
167        s.push_str(&segment_s);
168        match segment {
169            PropSegment::ShortChildWildcard => v.push(Segment::Child(Selectors::one(Selector::Wildcard))),
170            PropSegment::ShortChildName(n) => v.push(Segment::Child(Selectors::one(Selector::Name(n)))),
171            PropSegment::ShortDescendantWildcard => v.push(Segment::Descendant(Selectors::one(Selector::Wildcard))),
172            PropSegment::ShortDescendantName(n) => v.push(Segment::Descendant(Selectors::one(Selector::Name(n)))),
173            PropSegment::BracketedChild(ss) => v.push(Segment::Child(Selectors::many(
174                ss.into_iter().map(map_prop_selector).collect(),
175            ))),
176            PropSegment::BracketedDescendant(ss) => v.push(Segment::Descendant(Selectors::many(
177                ss.into_iter().map(map_prop_selector).collect(),
178            ))),
179        }
180    }
181
182    (s, JsonPathQuery::from_iter(v))
183}
184
185fn map_prop_selector(s: PropSelector) -> Selector {
186    match s {
187        PropSelector::Wildcard => Selector::Wildcard,
188        PropSelector::Name(n) => Selector::Name(n),
189        PropSelector::Index(i) => Selector::Index(i.into()),
190        PropSelector::Slice(start, end, step) => Selector::Slice({
191            let mut builder = SliceBuilder::new();
192            if let Some(start) = start {
193                builder.with_start(start);
194            }
195            if let Some(step) = step {
196                builder.with_step(step);
197            }
198            if let Some(end) = end {
199                builder.with_end(end);
200            }
201            builder.into()
202        }),
203        PropSelector::Filter(logical) => Selector::Filter(logical),
204    }
205}
206
207fn any_segment(
208    recursive_query_strategy: Option<BoxedStrategy<(String, JsonPathQuery)>>,
209    min_selectors: usize,
210    max_selectors: usize,
211) -> impl Strategy<Value = (String, PropSegment)> {
212    return prop_oneof![
213        strategy::Just((".*".to_string(), PropSegment::ShortChildWildcard)),
214        strategy::Just(("..*".to_string(), PropSegment::ShortDescendantWildcard)),
215        any_short_name().prop_map(|name| (format!(".{name}"), PropSegment::ShortChildName(JsonString::new(&name)))),
216        any_short_name().prop_map(|name| (
217            format!("..{name}"),
218            PropSegment::ShortDescendantName(JsonString::new(&name))
219        )),
220        prop::collection::vec(
221            any_selector(recursive_query_strategy.clone()),
222            min_selectors..max_selectors
223        )
224        .prop_map(|reprs| {
225            let mut s = "[".to_string();
226            let v = collect_reprs(reprs, &mut s);
227            s.push(']');
228            (s, PropSegment::BracketedChild(v))
229        }),
230        prop::collection::vec(any_selector(recursive_query_strategy), min_selectors..max_selectors).prop_map(|reprs| {
231            let mut s = "..[".to_string();
232            let v = collect_reprs(reprs, &mut s);
233            s.push(']');
234            (s, PropSegment::BracketedDescendant(v))
235        }),
236    ];
237
238    fn collect_reprs(reprs: Vec<(String, PropSelector)>, s: &mut String) -> Vec<PropSelector> {
239        let mut result = Vec::with_capacity(reprs.len());
240        let mut first = true;
241        for (repr_s, prop_selector) in reprs {
242            if !first {
243                s.push(',');
244            }
245            first = false;
246            s.push_str(&repr_s);
247            result.push(prop_selector);
248        }
249        result
250    }
251}
252
253fn rsonpath_valid_segment() -> impl Strategy<Value = (String, PropSegment)> {
254    prop_oneof![
255        strategy::Just((".*".to_string(), PropSegment::ShortChildWildcard)),
256        strategy::Just(("..*".to_string(), PropSegment::ShortDescendantWildcard)),
257        any_short_name().prop_map(|name| (format!(".{name}"), PropSegment::ShortChildName(JsonString::new(&name)))),
258        any_short_name().prop_map(|name| (
259            format!("..{name}"),
260            PropSegment::ShortDescendantName(JsonString::new(&name))
261        )),
262        rsonpath_valid_selector().prop_map(|repr| {
263            let mut s = "[".to_string();
264            s.push_str(&repr.0);
265            s.push(']');
266            (s, PropSegment::BracketedChild(vec![repr.1]))
267        }),
268        rsonpath_valid_selector().prop_map(|repr| {
269            let mut s = "..[".to_string();
270            s.push_str(&repr.0);
271            s.push(']');
272            (s, PropSegment::BracketedDescendant(vec![repr.1]))
273        }),
274    ]
275}
276
277fn any_selector(
278    recursive_query_strategy: Option<BoxedStrategy<(String, JsonPathQuery)>>,
279) -> impl Strategy<Value = (String, PropSelector)> {
280    prop_oneof![
281        strategy::Just(("*".to_string(), PropSelector::Wildcard)),
282        strings::any_json_string().prop_map(|(raw, s)| (raw, PropSelector::Name(s))),
283        any_json_int().prop_map(|(raw, i)| (raw, PropSelector::Index(i))),
284        any_slice().prop_map(|(raw, a, b, c)| (raw, PropSelector::Slice(a, b, c))),
285        filters::any_logical_expr(recursive_query_strategy)
286            .prop_map(|(raw, expr)| (format!("?{raw}"), PropSelector::Filter(expr)))
287    ]
288}
289
290fn rsonpath_valid_selector() -> impl Strategy<Value = (String, PropSelector)> {
291    prop_oneof![
292        strategy::Just(("*".to_string(), PropSelector::Wildcard)),
293        strings::any_json_string().prop_map(|(raw, s)| (raw, PropSelector::Name(s))),
294        rsonpath_valid_json_int().prop_map(|(raw, i)| (raw, PropSelector::Index(i))),
295        rsonpath_valid_slice().prop_map(|(raw, a, b, c)| (raw, PropSelector::Slice(a, b, c))),
296    ]
297}
298
299fn any_json_int() -> impl Strategy<Value = (String, JsonInt)> {
300    (-((1_i64 << 53) + 1)..((1_i64 << 53) - 1)).prop_map(|i| (i.to_string(), JsonInt::try_from(i).unwrap()))
301}
302
303fn rsonpath_valid_json_int() -> impl Strategy<Value = (String, JsonInt)> {
304    (0..((1_i64 << 53) - 1)).prop_map(|i| (i.to_string(), JsonInt::try_from(i).unwrap()))
305}
306
307fn any_slice() -> impl Strategy<Value = (String, Option<JsonInt>, Option<JsonInt>, Option<JsonInt>)> {
308    (
309        option::of(any_json_int()),
310        option::of(any_json_int()),
311        option::of(any_json_int()),
312    )
313        .prop_map(|(a, b, c)| {
314            let mut s = String::new();
315            let a = a.map(|(a_s, a_i)| {
316                s.push_str(&a_s);
317                a_i
318            });
319            s.push(':');
320            let b = b.map(|(b_s, b_i)| {
321                s.push_str(&b_s);
322                b_i
323            });
324            s.push(':');
325            let c = c.map(|(c_s, c_i)| {
326                s.push_str(&c_s);
327                c_i
328            });
329            (s, a, b, c)
330        })
331}
332
333fn rsonpath_valid_slice() -> impl Strategy<Value = (String, Option<JsonInt>, Option<JsonInt>, Option<JsonInt>)> {
334    (
335        option::of(rsonpath_valid_json_int()),
336        option::of(rsonpath_valid_json_int()),
337        option::of(rsonpath_valid_json_int()),
338    )
339        .prop_map(|(a, b, c)| {
340            let mut s = String::new();
341            let a = a.map(|(a_s, a_i)| {
342                s.push_str(&a_s);
343                a_i
344            });
345            s.push(':');
346            let b = b.map(|(b_s, b_i)| {
347                s.push_str(&b_s);
348                b_i
349            });
350            s.push(':');
351            let c = c.map(|(c_s, c_i)| {
352                s.push_str(&c_s);
353                c_i
354            });
355            (s, a, b, c)
356        })
357}
358
359fn any_short_name() -> impl Strategy<Value = String> {
360    r"([A-Za-z]|_|[^\u0000-\u007F])([A-Za-z0-9]|_|[^\u0000-\u007F])*"
361}
362
363mod strings {
364    use proptest::{prelude::*, sample::SizeRange};
365    use rsonpath_syntax::str::JsonString;
366
367    #[derive(Debug, PartialEq, Eq, Clone, Copy)]
368    enum JsonStringToken {
369        EncodeNormally(char),
370        ForceUnicodeEscape(char),
371    }
372
373    #[derive(Debug, PartialEq, Eq, Clone, Copy)]
374    enum JsonStringTokenEncodingMode {
375        SingleQuoted,
376        DoubleQuoted,
377    }
378
379    impl JsonStringToken {
380        fn raw(self) -> char {
381            match self {
382                Self::EncodeNormally(x) | Self::ForceUnicodeEscape(x) => x,
383            }
384        }
385
386        fn encode(self, mode: JsonStringTokenEncodingMode) -> String {
387            return match self {
388                Self::EncodeNormally('\u{0008}') => r"\b".to_owned(),
389                Self::EncodeNormally('\t') => r"\t".to_owned(),
390                Self::EncodeNormally('\n') => r"\n".to_owned(),
391                Self::EncodeNormally('\u{000C}') => r"\f".to_owned(),
392                Self::EncodeNormally('\r') => r"\r".to_owned(),
393                Self::EncodeNormally('"') => match mode {
394                    JsonStringTokenEncodingMode::DoubleQuoted => r#"\""#.to_owned(),
395                    JsonStringTokenEncodingMode::SingleQuoted => r#"""#.to_owned(),
396                },
397                Self::EncodeNormally('\'') => match mode {
398                    JsonStringTokenEncodingMode::DoubleQuoted => r#"'"#.to_owned(),
399                    JsonStringTokenEncodingMode::SingleQuoted => r#"\'"#.to_owned(),
400                },
401                Self::EncodeNormally('/') => r"\/".to_owned(),
402                Self::EncodeNormally('\\') => r"\\".to_owned(),
403                Self::EncodeNormally(c @ ..='\u{001F}') | Self::ForceUnicodeEscape(c) => encode_unicode_escape(c),
404                Self::EncodeNormally(c) => c.to_string(),
405            };
406
407            fn encode_unicode_escape(c: char) -> String {
408                let mut buf = [0; 2];
409                let enc = c.encode_utf16(&mut buf);
410                let mut res = String::new();
411                for x in enc {
412                    res += &format!("\\u{x:0>4x}");
413                }
414                res
415            }
416        }
417    }
418
419    pub(super) fn any_json_string() -> impl Strategy<Value = (String, JsonString)> {
420        prop_oneof![
421            Just(JsonStringTokenEncodingMode::SingleQuoted),
422            Just(JsonStringTokenEncodingMode::DoubleQuoted)
423        ]
424        .prop_flat_map(|mode| {
425            prop::collection::vec(
426                (prop::char::any(), prop::bool::ANY).prop_map(|(c, b)| {
427                    if b {
428                        JsonStringToken::EncodeNormally(c)
429                    } else {
430                        JsonStringToken::ForceUnicodeEscape(c)
431                    }
432                }),
433                SizeRange::default(),
434            )
435            .prop_map(move |v| {
436                let q = match mode {
437                    JsonStringTokenEncodingMode::SingleQuoted => '\'',
438                    JsonStringTokenEncodingMode::DoubleQuoted => '"',
439                };
440                let mut s = String::new();
441                let mut l = String::new();
442                for x in v {
443                    s += &x.encode(mode);
444                    l.push(x.raw());
445                }
446                (format!("{q}{s}{q}"), JsonString::new(&l))
447            })
448        })
449    }
450}
451
452mod filters {
453    use proptest::{num, prelude::*, strategy};
454    use rsonpath_syntax::{
455        num::{JsonFloat, JsonNumber},
456        str::JsonString,
457        Comparable, ComparisonExpr, ComparisonOp, JsonPathQuery, Literal, LogicalExpr, SingularJsonPathQuery,
458        SingularSegment, TestExpr,
459    };
460
461    pub(super) fn any_logical_expr(
462        test_query_strategy: Option<BoxedStrategy<(String, JsonPathQuery)>>,
463    ) -> impl Strategy<Value = (String, LogicalExpr)> {
464        any_atomic_logical_expr(test_query_strategy).prop_recursive(8, 32, 2, |inner| {
465            prop_oneof![
466                (inner.clone(), proptest::bool::ANY).prop_map(|((s, f), force_paren)| (
467                    match f {
468                        LogicalExpr::Test(_) if !force_paren => format!("!{s}"),
469                        _ => format!("!({s})"),
470                    },
471                    LogicalExpr::Not(Box::new(f))
472                )),
473                (inner.clone(), inner.clone(), proptest::bool::ANY, proptest::bool::ANY).prop_map(
474                    |((lhs_s, lhs_e), (rhs_s, rhs_e), force_left_paren, force_right_paren)| {
475                        let put_left_paren = force_left_paren || matches!(lhs_e, LogicalExpr::Or(_, _));
476                        let put_right_paren =
477                            force_right_paren || matches!(rhs_e, LogicalExpr::Or(_, _) | LogicalExpr::And(_, _));
478                        let s = match (put_left_paren, put_right_paren) {
479                            (true, true) => format!("({lhs_s})&&({rhs_s})"),
480                            (true, false) => format!("({lhs_s})&&{rhs_s}"),
481                            (false, true) => format!("{lhs_s}&&({rhs_s})"),
482                            (false, false) => format!("{lhs_s}&&{rhs_s}"),
483                        };
484                        (s, LogicalExpr::And(Box::new(lhs_e), Box::new(rhs_e)))
485                    }
486                ),
487                (inner.clone(), inner.clone(), proptest::bool::ANY, proptest::bool::ANY).prop_map(
488                    |((lhs_s, lhs_e), (rhs_s, rhs_e), force_left_paren, force_right_paren)| {
489                        let put_left_paren = force_left_paren || matches!(lhs_e, LogicalExpr::Or(_, _));
490                        let put_right_paren = force_right_paren;
491                        let s = match (put_left_paren, put_right_paren) {
492                            (true, true) => format!("({lhs_s})||({rhs_s})"),
493                            (true, false) => format!("({lhs_s})||{rhs_s}"),
494                            (false, true) => format!("{lhs_s}||({rhs_s})"),
495                            (false, false) => format!("{lhs_s}||{rhs_s}"),
496                        };
497                        (s, LogicalExpr::Or(Box::new(lhs_e), Box::new(rhs_e)))
498                    }
499                )
500            ]
501        })
502    }
503
504    fn any_atomic_logical_expr(
505        test_query_strategy: Option<BoxedStrategy<(String, JsonPathQuery)>>,
506    ) -> impl Strategy<Value = (String, LogicalExpr)> {
507        if let Some(test_query_strategy) = test_query_strategy {
508            prop_oneof![
509                any_test(test_query_strategy).prop_map(|(s, t)| (s, LogicalExpr::Test(t))),
510                any_comparison().prop_map(|(s, c)| (s, LogicalExpr::Comparison(c))),
511            ]
512            .boxed()
513        } else {
514            any_comparison()
515                .prop_map(|(s, c)| (s, LogicalExpr::Comparison(c)))
516                .boxed()
517        }
518    }
519
520    fn any_test(
521        test_query_strategy: BoxedStrategy<(String, JsonPathQuery)>,
522    ) -> impl Strategy<Value = (String, TestExpr)> {
523        (proptest::bool::ANY, test_query_strategy).prop_map(|(relative, (mut s, q))| {
524            if relative {
525                assert_eq!(s.as_bytes()[0], b'$');
526                s.replace_range(0..1, "@");
527                (s, TestExpr::Relative(q))
528            } else {
529                (s, TestExpr::Absolute(q))
530            }
531        })
532    }
533
534    fn any_comparison() -> impl Strategy<Value = (String, ComparisonExpr)> {
535        (any_comparable(), any_comparison_op(), any_comparable()).prop_map(
536            |((lhs_s, lhs_e), (op_s, op_e), (rhs_s, rhs_e))| {
537                (
538                    format!("{lhs_s}{op_s}{rhs_s}"),
539                    ComparisonExpr::from_parts(lhs_e, op_e, rhs_e),
540                )
541            },
542        )
543    }
544
545    fn any_comparable() -> impl Strategy<Value = (String, Comparable)> {
546        prop_oneof![
547            any_literal().prop_map(|(s, l)| (s, Comparable::Literal(l))),
548            (proptest::bool::ANY, any_singular_query()).prop_map(|(relative, (mut s, q))| {
549                if relative {
550                    assert_eq!(s.as_bytes()[0], b'$');
551                    s.replace_range(0..1, "@");
552                    (s, Comparable::RelativeSingularQuery(q))
553                } else {
554                    (s, Comparable::AbsoluteSingularQuery(q))
555                }
556            })
557        ]
558    }
559
560    prop_compose! {
561        fn any_singular_query()(segments in prop::collection::vec(any_singular_segment(), 0..10)) -> (String, SingularJsonPathQuery) {
562            let mut s = "$".to_string();
563            let mut v = vec![];
564
565            for (segment_s, segment) in segments {
566                s.push_str(&segment_s);
567                v.push(segment);
568            }
569
570            (s, SingularJsonPathQuery::from_iter(v))
571        }
572    }
573
574    fn any_singular_segment() -> impl Strategy<Value = (String, SingularSegment)> {
575        prop_oneof![
576            super::any_json_int().prop_map(|(s, i)| (format!("[{s}]"), SingularSegment::Index(i.into()))),
577            super::any_short_name().prop_map(|n| (format!(".{n}"), SingularSegment::Name(JsonString::new(&n)))),
578            super::strings::any_json_string().prop_map(|(s, n)| (format!("[{s}]"), SingularSegment::Name(n))),
579        ]
580    }
581
582    fn any_literal() -> impl Strategy<Value = (String, Literal)> {
583        prop_oneof![
584            strategy::Just(("null".to_string(), Literal::Null)),
585            proptest::bool::ANY.prop_map(|b| (b.to_string(), Literal::Bool(b))),
586            any_json_number().prop_map(|(s, n)| (s, Literal::Number(n))),
587            super::strings::any_json_string().prop_map(|(raw, s)| (raw, Literal::String(s)))
588        ]
589    }
590
591    fn any_json_number() -> impl Strategy<Value = (String, JsonNumber)> {
592        prop_oneof![
593            super::any_json_int().prop_map(|(s, i)| (s, JsonNumber::Int(i))),
594            any_json_float().prop_map(|(s, f)| (s, JsonNumber::Float(f))),
595        ]
596        .prop_map(|(x, n)| (x, n.normalize()))
597    }
598
599    fn any_json_float() -> impl Strategy<Value = (String, JsonFloat)> {
600        // We first generate the target f64 value we want and then pick one of its possible string reprs.
601        // Because an "int float" is also interesting we generate those half the time.
602        // If there is no exponent, there is only one possible representation.
603        // If we include an exponent we can move the floating point however far we want one way or the other.
604        return prop_oneof![
605            any_float().prop_map(|f| (f.to_string(), JsonFloat::try_from(f).unwrap())),
606            any_float()
607                .prop_flat_map(|f| arbitrary_exp_repr(f).prop_map(move |s| (s, JsonFloat::try_from(f).unwrap()))),
608        ];
609
610        fn any_float() -> impl Strategy<Value = f64> {
611            prop_oneof![num::f64::NORMAL, num::f64::NORMAL.prop_map(f64::trunc)]
612        }
613
614        fn arbitrary_exp_repr(f: f64) -> impl Strategy<Value = String> {
615            let s = f.to_string();
616            let fp_pos: isize = s.find('.').unwrap_or(s.len()).try_into().unwrap();
617            let num_digits = if fp_pos == s.len() as isize {
618                s.len()
619            } else {
620                s.len() - 1
621            } - if f.is_sign_negative() {
622                // Subtract the minus char.
623                1
624            } else {
625                0
626            };
627            (-1024..=1024_isize, proptest::bool::ANY, proptest::bool::ANY).prop_map(
628                move |(exp, force_sign, uppercase_e)| {
629                    let new_pos = fp_pos - exp;
630                    let mut res = String::new();
631                    if f.is_sign_negative() {
632                        res.push('-');
633                    }
634                    let mut orig_digits = s.chars().filter(|c| *c != '.');
635
636                    // There are three cases:
637                    //   1. the new point is before all existing digits;
638                    //     in this case we need to append 0.000... at the front
639                    //   2. the new point position falls within the existing string;
640                    //     this is straightforward, we just emplace it there
641                    //   3. the new point is after all existing digits;
642                    //     in this case we need to append 0000... at the end
643                    // After this operation we need to manually trim the zeroes.
644                    if new_pos <= 0 {
645                        // Case 1.
646                        res.push_str("0.");
647                        for _ in 0..(-new_pos) {
648                            res.push('0');
649                        }
650                        for orig_digit in orig_digits {
651                            res.push(orig_digit);
652                        }
653                    } else if new_pos < num_digits as isize {
654                        // Case 2.
655                        let mut pos = 0;
656                        let mut pushed_non_zero = false;
657                        loop {
658                            if pos == new_pos {
659                                if !pushed_non_zero {
660                                    res.push('0');
661                                }
662                                pushed_non_zero = true;
663                                res.push('.');
664                            } else {
665                                let Some(orig_digit) = orig_digits.next() else { break };
666                                if orig_digit == '0' {
667                                    if pushed_non_zero {
668                                        res.push(orig_digit);
669                                    }
670                                } else {
671                                    pushed_non_zero = true;
672                                    res.push(orig_digit);
673                                }
674                            }
675                            pos += 1;
676                        }
677                    } else if f == 0.0 {
678                        // Case 3. special case.
679                        // Note that -0.0 is handled here as well, as it is equal to 0.0 and the sign is appended above.
680                        res.push('0');
681                    } else {
682                        // Case 3.
683                        // First skip zeroes. There has to be at least one non-zero since we checked
684                        // f == 0.0 above.
685                        let skip_zeroes = orig_digits.skip_while(|x| *x == '0');
686                        for orig_digit in skip_zeroes {
687                            res.push(orig_digit);
688                        }
689                        for _ in 0..(new_pos - num_digits as isize) {
690                            res.push('0');
691                        }
692                    }
693
694                    res.push(if uppercase_e { 'E' } else { 'e' });
695
696                    if exp > 0 {
697                        if force_sign {
698                            res.push('+');
699                        }
700                        res.push_str(&exp.to_string());
701                    } else {
702                        res.push_str(&exp.to_string());
703                    }
704
705                    res
706                },
707            )
708        }
709    }
710
711    fn any_comparison_op() -> impl Strategy<Value = (String, ComparisonOp)> {
712        prop_oneof![
713            strategy::Just(("==".to_string(), ComparisonOp::EqualTo)),
714            strategy::Just(("!=".to_string(), ComparisonOp::NotEqualTo)),
715            strategy::Just(("<".to_string(), ComparisonOp::LessThan)),
716            strategy::Just((">".to_string(), ComparisonOp::GreaterThan)),
717            strategy::Just(("<=".to_string(), ComparisonOp::LesserOrEqualTo)),
718            strategy::Just((">=".to_string(), ComparisonOp::GreaterOrEqualTo)),
719        ]
720    }
721}