Skip to main content

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