pact_consumer/patterns/
json_macros.rs

1//! Macros for building `JsonPattern` objects.
2//!
3//! Much of the macro code below is directly copied from `serde_json` and
4//! modified to construct `JsonPattern` objects instead of `serde_json::Value`
5//! objects. The following copyright notice applies to this code:
6//!
7//! Copyright 2017 Serde Developers
8//!
9//! Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
10//! http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT
11//! or http://opensource.org/licenses/MIT>, at your option. This file may not be
12//! copied, modified, or distributed except according to those terms.
13
14/// Construct a `JsonPattern` object using a convenient syntax.
15///
16/// ```rust
17/// use pact_consumer::*;
18///
19/// # fn main() {
20/// json_pattern!({
21///     "message": "Hello, world!",
22///     "location": { "x": 1, "y": 2 },
23///     "tags": ["interesting"]
24/// });
25/// # }
26/// ```
27///
28/// The `json_pattern!` macro supports nested Rust expressions:
29///
30/// ```
31/// use pact_consumer::*;
32/// use serde_json::*;
33///
34/// #[derive(serde::Serialize)]
35/// struct Point {
36///    x: f32,
37///    y: f32,
38/// }
39///
40/// # fn main() {
41/// json_pattern!({
42///     // You can use Rust expressions, as long as they support
43///     // `Into<JsonPattern>`.
44///     "message": format!("Hello, {}!", "world"),
45///
46///     // You can also nest the `json!` macro to embed types which
47///     // support `Serialize`.
48///     "location": json!(Point { x: 1.0, y: 2.0 }),
49///
50///     // You can use `something_like` to match by type only.
51///     "comment": like!("A comment goes here"),
52///
53///     // You can use `array_like` to match an array of values which
54///     // look like the example.
55///     "tags": each_like!("tag"),
56/// });
57/// # }
58/// ```
59#[macro_export]
60macro_rules! json_pattern {
61    // Hide distracting implementation details from the generated rustdoc.
62    ($($json:tt)+) => {
63      $crate::json_pattern_internal!($($json)+)
64    };
65}
66
67// Our internal helper macro.
68#[macro_export]
69#[doc(hidden)]
70macro_rules! json_pattern_internal {
71    //////////////////////////////////////////////////////////////////////////
72    // TT muncher for parsing the inside of an array [...]. Produces a vec![...]
73    // of the elements.
74    //
75    // Must be invoked as: json_internal!(@array [] $($tt)*)
76    //////////////////////////////////////////////////////////////////////////
77
78    // Done with trailing comma.
79    (@array [$($elems:expr,)*]) => {
80        vec![$($elems,)*]
81    };
82
83    // Done without trailing comma.
84    (@array [$($elems:expr),*]) => {
85        vec![$($elems),*]
86    };
87
88    // Next element is `null`.
89    (@array [$($elems:expr,)*] null $($rest:tt)*) => {
90        json_pattern_internal!(@array [$($elems,)* json_pattern_internal!(null)] $($rest)*)
91    };
92
93    // Next element is an array.
94    (@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => {
95        json_pattern_internal!(@array [$($elems,)* json_pattern_internal!([$($array)*])] $($rest)*)
96    };
97
98    // Next element is a map.
99    (@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => {
100        json_pattern_internal!(@array [$($elems,)* json_pattern_internal!({$($map)*})] $($rest)*)
101    };
102
103    // Next element is an expression followed by comma.
104    (@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => {
105        json_pattern_internal!(@array [$($elems,)* json_pattern_internal!($next),] $($rest)*)
106    };
107
108    // Last element is an expression with no trailing comma.
109    (@array [$($elems:expr,)*] $last:expr) => {
110        json_pattern_internal!(@array [$($elems,)* json_pattern_internal!($last)])
111    };
112
113    // Comma after the most recent element.
114    (@array [$($elems:expr),*] , $($rest:tt)*) => {
115        json_pattern_internal!(@array [$($elems,)*] $($rest)*)
116    };
117
118    //////////////////////////////////////////////////////////////////////////
119    // TT muncher for parsing the inside of an object {...}. Each entry is
120    // inserted into the given map variable.
121    //
122    // Must be invoked as: json_pattern_internal!(@object $map () ($($tt)*) ($($tt)*))
123    //
124    // We require two copies of the input tokens so that we can match on one
125    // copy and trigger errors on the other copy.
126    //////////////////////////////////////////////////////////////////////////
127
128    // Done.
129    (@object $object:ident () () ()) => {};
130
131    // Insert the current entry followed by trailing comma.
132    (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
133        $object.insert(($($key)+).into(), $value);
134        json_pattern_internal!(@object $object () ($($rest)*) ($($rest)*));
135    };
136
137    // Insert the last entry without trailing comma.
138    (@object $object:ident [$($key:tt)+] ($value:expr)) => {
139        $object.insert(($($key)+).into(), $value);
140    };
141
142    // Next value is `null`.
143    (@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => {
144        json_pattern_internal!(@object $object [$($key)+] (json_pattern_internal!(null)) $($rest)*);
145    };
146
147    // Next value is an array.
148    (@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => {
149        json_pattern_internal!(@object $object [$($key)+] (json_pattern_internal!([$($array)*])) $($rest)*);
150    };
151
152    // Next value is a map.
153    (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
154        json_pattern_internal!(@object $object [$($key)+] (json_pattern_internal!({$($map)*})) $($rest)*);
155    };
156
157    // Next value is an expression followed by comma.
158    (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
159        json_pattern_internal!(@object $object [$($key)+] (json_pattern_internal!($value)) , $($rest)*);
160    };
161
162    // Last value is an expression with no trailing comma.
163    (@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
164        json_pattern_internal!(@object $object [$($key)+] (json_pattern_internal!($value)));
165    };
166
167    // Missing value for last entry. Trigger a reasonable error message.
168    (@object $object:ident ($($key:tt)+) (:) $copy:tt) => {
169        // "unexpected end of macro invocation"
170        json_pattern_internal!();
171    };
172
173    // Missing colon and value for last entry. Trigger a reasonable error
174    // message.
175    (@object $object:ident ($($key:tt)+) () $copy:tt) => {
176        // "unexpected end of macro invocation"
177        json_pattern_internal!();
178    };
179
180    // Misplaced colon. Trigger a reasonable error message.
181    (@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => {
182        // Takes no arguments so "no rules expected the token `:`".
183        unimplemented!($colon);
184    };
185
186    // Found a comma inside a key. Trigger a reasonable error message.
187    (@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => {
188        // Takes no arguments so "no rules expected the token `,`".
189        unimplemented!($comma);
190    };
191
192    // Key is fully parenthesized. This avoids clippy double_parens false
193    // positives because the parenthesization may be necessary here.
194    (@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
195        json_pattern_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
196    };
197
198    // Munch a token into the current key.
199    (@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
200        json_pattern_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
201    };
202
203    //////////////////////////////////////////////////////////////////////////
204    // The main implementation.
205    //
206    // Must be invoked as: json_internal!($($json)+)
207    //////////////////////////////////////////////////////////////////////////
208
209    (null) => {
210        $crate::patterns::JsonPattern::null()
211    };
212
213    ([]) => {
214        $crate::patterns::JsonPattern::Array(vec![])
215    };
216
217    ([ $($tt:tt)+ ]) => {
218        $crate::patterns::JsonPattern::Array(
219            json_pattern_internal!(@array [] $($tt)+)
220        )
221    };
222
223    ({}) => {
224        $crate::patterns::JsonPattern::Object(::std::collections::HashMap::new())
225    };
226
227    ({ $($tt:tt)+ }) => {
228        $crate::patterns::JsonPattern::Object({
229            let mut object = ::std::collections::HashMap::new();
230            json_pattern_internal!(@object object () ($($tt)+) ($($tt)+));
231            object
232        })
233    };
234
235    // Any Serialize type: numbers, strings, struct literals, variables etc.
236    // Must be below every other rule.
237    ($other:expr) => {
238        {
239            let v: $crate::patterns::JsonPattern = $other.into();
240            v
241        }
242    };
243}
244
245#[test]
246fn trailing_commas() {
247    json_pattern!({
248        "a": 1,
249        "b": 2,
250    });
251
252    json_pattern!([
253        true,
254        false,
255    ]);
256}
257
258#[test]
259fn true_false_and_null() {
260    // These were all special-cased in the original `json!` macro, so make sure
261    // they all still work.
262    json_pattern!([
263        true,
264        null,
265        false,
266        { "false": false, "null": null, "true": true },
267    ]);
268}