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}