Skip to main content

plist/
macros.rs

1/// Constructs a `plist::Value` from a JSON-like literal.
2///
3/// ```
4/// # use plist::plist;
5/// #
6/// let value = plist!({
7///     "code": 200,
8///     "success": true,
9///     "payload": {
10///         "features": [
11///             "serde",
12///         ]
13///     }
14/// });
15/// ```
16///
17/// Variables or expressions can be interpolated into the literal. Any type
18/// interpolated into an array element or object value must implement the
19/// `Into<Value>` trait, while any type interpolated into a object key must
20/// implement `Into<String>`.
21///
22/// ```
23/// # use plist::plist;
24/// #
25/// let code = 200;
26/// let features = vec!["serde", "plist"];
27///
28/// let value = plist!({
29///     "code": code,
30///     "success": code == 200,
31///     "payload": {
32///         features[0]: features[1]
33///     }
34/// });
35/// ```
36///
37/// Trailing commas are allowed inside both arrays and objects.
38///
39/// ```
40/// # use plist::plist;
41/// #
42/// let value = plist!([
43///     "notice",
44///     "the",
45///     "trailing",
46///     "comma -->",
47/// ]);
48/// ```
49#[macro_export(local_inner_macros)]
50macro_rules! plist {
51    // Hide distracting implementation details from the generated rustdoc.
52    ($($plist:tt)+) => {
53        plist_internal!($($plist)+)
54    };
55}
56
57/// Constructs a `plist::Dictionary` from a JSON-like literal.
58///
59/// ```
60/// # use plist::plist_dict;
61/// #
62/// let value = plist_dict! {
63///     "code": 200,
64///     "success": true,
65///     "payload": {
66///         "features": [
67///             "serde",
68///         ]
69///     }
70/// };
71/// ```
72///
73/// Variables or expressions can be interpolated into the literal. Any type
74/// interpolated into an array element or object value must implement the
75/// `Into<Value>` trait, while any type interpolated into a object key must
76/// implement `Into<String>`.
77///
78/// ```
79/// # use plist::plist_dict;
80/// #
81/// let code = 200;
82/// let features = vec!["serde", "plist"];
83///
84/// let value = plist_dict! {
85///     "code": code,
86///     "success": code == 200,
87///     "payload": {
88///         features[0]: features[1]
89///     }
90/// };
91/// ```
92///
93/// Trailing commas are allowed inside both arrays and objects.
94///
95/// ```
96/// # use plist::plist_dict;
97/// #
98/// let value = plist_dict! {
99///     "notice": 0,
100///     "the": 1,
101///     "trailing": 2,
102///     "comma -->": 3,
103/// };
104/// ```
105#[macro_export(local_inner_macros)]
106macro_rules! plist_dict {
107    () => {
108        $crate::Dictionary::new()
109    };
110    // Allow outer curlies if provided
111    ({$($tt:tt)+}) => {
112        {
113            let mut object = $crate::Dictionary::new();
114            plist_internal!(@object object () ($($tt)+) ($($tt)+));
115            object
116        }
117    };
118    ($($tt:tt)+) => {
119        {
120            let mut object = $crate::Dictionary::new();
121            plist_internal!(@object object () ($($tt)+) ($($tt)+));
122            object
123        }
124    };
125}
126
127#[macro_export(local_inner_macros)]
128#[doc(hidden)]
129macro_rules! plist_internal {
130    //////////////////////////////////////////////////////////////////////////
131    // TT muncher for parsing the inside of an array [...]. Produces a vec![...]
132    // of the elements.
133    //
134    // Must be invoked as: plist_internal!(@array [] $($tt)*)
135    //////////////////////////////////////////////////////////////////////////
136
137    // Done with trailing comma.
138    (@array [$($elems:expr,)*]) => {
139        plist_internal_vec![$($elems,)*]
140    };
141
142    // Done without trailing comma.
143    (@array [$($elems:expr),*]) => {
144        plist_internal_vec![$($elems),*]
145    };
146
147    // Next element is `true`.
148    (@array [$($elems:expr,)*] true $($rest:tt)*) => {
149        plist_internal!(@array [$($elems,)* plist_internal!(true)] $($rest)*)
150    };
151
152    // Next element is `false`.
153    (@array [$($elems:expr,)*] false $($rest:tt)*) => {
154        plist_internal!(@array [$($elems,)* plist_internal!(false)] $($rest)*)
155    };
156
157    // Next element is an array.
158    (@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => {
159        plist_internal!(@array [$($elems,)* plist_internal!([$($array)*])] $($rest)*)
160    };
161
162    // Next element is a map.
163    (@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => {
164        plist_internal!(@array [$($elems,)* plist_internal!({$($map)*})] $($rest)*)
165    };
166
167    // Next element is an expression followed by comma.
168    (@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => {
169        plist_internal!(@array [$($elems,)* plist_internal!($next),] $($rest)*)
170    };
171
172    // Last element is an expression with no trailing comma.
173    (@array [$($elems:expr,)*] $last:expr) => {
174        plist_internal!(@array [$($elems,)* plist_internal!($last)])
175    };
176
177    // Comma after the most recent element.
178    (@array [$($elems:expr),*] , $($rest:tt)*) => {
179        plist_internal!(@array [$($elems,)*] $($rest)*)
180    };
181
182    // Unexpected token after most recent element.
183    (@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => {
184        plist_unexpected!($unexpected)
185    };
186
187    //////////////////////////////////////////////////////////////////////////
188    // TT muncher for parsing the inside of an object {...}. Each entry is
189    // inserted into the given map variable.
190    //
191    // Must be invoked as: plist_internal!(@object $map () ($($tt)*) ($($tt)*))
192    //
193    // We require two copies of the input tokens so that we can match on one
194    // copy and trigger errors on the other copy.
195    //////////////////////////////////////////////////////////////////////////
196
197    // Done.
198    (@object $object:ident () () ()) => {};
199
200    // Insert the current entry followed by trailing comma.
201    (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
202        let _ = $object.insert(($($key)+).into(), $value);
203        plist_internal!(@object $object () ($($rest)*) ($($rest)*));
204    };
205
206    // Current entry followed by unexpected token.
207    (@object $object:ident [$($key:tt)+] ($value:expr) $unexpected:tt $($rest:tt)*) => {
208        plist_unexpected!($unexpected);
209    };
210
211    // Insert the last entry without trailing comma.
212    (@object $object:ident [$($key:tt)+] ($value:expr)) => {
213        let _ = $object.insert(($($key)+).into(), $value);
214    };
215
216    // Next value is `true`.
217    (@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => {
218        plist_internal!(@object $object [$($key)+] (plist_internal!(true)) $($rest)*);
219    };
220
221    // Next value is `false`.
222    (@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => {
223        plist_internal!(@object $object [$($key)+] (plist_internal!(false)) $($rest)*);
224    };
225
226    // Next value is an array.
227    (@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => {
228        plist_internal!(@object $object [$($key)+] (plist_internal!([$($array)*])) $($rest)*);
229    };
230
231    // Next value is a map.
232    (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
233        plist_internal!(@object $object [$($key)+] (plist_internal!({$($map)*})) $($rest)*);
234    };
235
236    // Next value is an expression followed by comma.
237    (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
238        plist_internal!(@object $object [$($key)+] (plist_internal!($value)) , $($rest)*);
239    };
240
241    // Last value is an expression with no trailing comma.
242    (@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
243        plist_internal!(@object $object [$($key)+] (plist_internal!($value)));
244    };
245
246    // Missing value for last entry. Trigger a reasonable error message.
247    (@object $object:ident ($($key:tt)+) (:) $copy:tt) => {
248        // "unexpected end of macro invocation"
249        plist_internal!();
250    };
251
252    // Missing colon and value for last entry. Trigger a reasonable error
253    // message.
254    (@object $object:ident ($($key:tt)+) () $copy:tt) => {
255        // "unexpected end of macro invocation"
256        plist_internal!();
257    };
258
259    // Misplaced colon. Trigger a reasonable error message.
260    (@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => {
261        // Takes no arguments so "no rules expected the token `:`".
262        plist_unexpected!($colon);
263    };
264
265    // Found a comma inside a key. Trigger a reasonable error message.
266    (@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => {
267        // Takes no arguments so "no rules expected the token `,`".
268        plist_unexpected!($comma);
269    };
270
271    // Key is fully parenthesized. This avoids clippy double_parens false
272    // positives because the parenthesization may be necessary here.
273    (@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
274        plist_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
275    };
276
277    // Refuse to absorb colon token into key expression.
278    (@object $object:ident ($($key:tt)*) (: $($unexpected:tt)+) $copy:tt) => {
279        plist_expect_expr_comma!($($unexpected)+);
280    };
281
282    // Munch a token into the current key.
283    (@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
284        plist_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
285    };
286
287    //////////////////////////////////////////////////////////////////////////
288    // The main implementation.
289    //
290    // Must be invoked as: plist_internal!($($plist)+)
291    //////////////////////////////////////////////////////////////////////////
292
293    (true) => {
294        $crate::Value::Boolean(true)
295    };
296
297    (false) => {
298        $crate::Value::Boolean(false)
299    };
300
301    ([]) => {
302        $crate::Value::Array(plist_internal_vec![])
303    };
304
305    ([ $($tt:tt)+ ]) => {
306        $crate::Value::Array(plist_internal!(@array [] $($tt)+))
307    };
308
309    ({}) => {
310        $crate::Value::Dictionary($crate::Dictionary::new())
311    };
312
313    ({ $($tt:tt)+ }) => {
314        $crate::Value::Dictionary({
315            let mut object = $crate::Dictionary::new();
316            plist_internal!(@object object () ($($tt)+) ($($tt)+));
317            object
318        })
319    };
320
321    // Any Into<plist::Value> type: numbers, strings, struct literals, variables etc.
322    // Must be below every other rule.
323    ($other:expr) => {
324        $crate::Value::from($other)
325    };
326}
327
328// The plist_internal macro above cannot invoke vec directly because it uses
329// local_inner_macros. A vec invocation there would resolve to plist::vec.
330// Instead invoke vec here outside of local_inner_macros.
331#[macro_export]
332#[doc(hidden)]
333macro_rules! plist_internal_vec {
334    ($($content:tt)*) => {
335        vec![$($content)*]
336    };
337}
338
339#[macro_export]
340#[doc(hidden)]
341macro_rules! plist_unexpected {
342    () => {};
343}
344
345#[macro_export]
346#[doc(hidden)]
347macro_rules! plist_expect_expr_comma {
348    ($e:expr , $($tt:tt)*) => {};
349}
350
351#[cfg(test)]
352mod tests {
353    use crate::Dictionary;
354
355    #[test]
356    fn plist_dict() {
357        let expected = Dictionary::from_iter([(String::from("foo"), String::from("bar"))]);
358
359        let d = plist_dict! {
360            "foo": "bar",
361        };
362        assert_eq!(&d, &expected);
363
364        let d = plist_dict!({
365            "foo": "bar",
366        });
367        assert_eq!(&d, &expected);
368    }
369}