schemars_derive/attr/
parse_meta.rs

1use proc_macro2::{TokenStream, TokenTree};
2use syn::{
3    parse::{Parse, ParseStream, Parser},
4    punctuated::Punctuated,
5    Expr, ExprLit, Lit, LitStr, MetaNameValue,
6};
7
8use super::{path_str, AttrCtxt, CustomMeta};
9
10pub fn require_path_only(meta: &CustomMeta, cx: &AttrCtxt) -> Result<(), ()> {
11    let error_args = || {
12        format!(
13            "unexpected value of {} {} attribute item",
14            cx.attr_type,
15            path_str(meta.path())
16        )
17    };
18
19    match &meta {
20        CustomMeta::Path(_) => Ok(()),
21        CustomMeta::List(meta) => {
22            cx.syn_error(syn::Error::new(meta.delimiter.span().join(), error_args()));
23            Err(())
24        }
25        CustomMeta::NameValue(meta) => {
26            let eq_token = &meta.eq_token;
27            let value = &meta.value;
28            cx.error_spanned_by(quote!(#eq_token #value), error_args());
29            Err(())
30        }
31        CustomMeta::Not(..) => {
32            // Validation of "unset" attributes is currently done in schemars_to_serde
33            Err(())
34        }
35    }
36}
37
38pub fn parse_name_value_expr(meta: CustomMeta, cx: &AttrCtxt) -> Result<Expr, ()> {
39    if let CustomMeta::NameValue(m) = meta {
40        Ok(m.value)
41    } else {
42        let name = path_str(meta.path());
43        cx.error_spanned_by(
44            meta,
45            format_args!(
46                "expected {} {} attribute item to have a value: `{} = ...`",
47                cx.attr_type, name, name
48            ),
49        );
50        Err(())
51    }
52}
53
54pub fn require_name_value_lit_str(meta: CustomMeta, cx: &AttrCtxt) -> Result<LitStr, ()> {
55    if let CustomMeta::NameValue(MetaNameValue {
56        value: Expr::Lit(ExprLit {
57            lit: Lit::Str(lit_str),
58            ..
59        }),
60        ..
61    }) = meta
62    {
63        Ok(lit_str)
64    } else {
65        let name = path_str(meta.path());
66        cx.error_spanned_by(
67            meta,
68            format_args!(
69                "expected {} {} attribute item to have a string value: `{} = \"...\"`",
70                cx.attr_type, name, name
71            ),
72        );
73        Err(())
74    }
75}
76
77pub fn parse_name_value_lit_str<T: Parse>(meta: CustomMeta, cx: &AttrCtxt) -> Result<T, ()> {
78    let lit_str = require_name_value_lit_str(meta, cx)?;
79
80    parse_lit_str(&lit_str, cx)
81}
82
83fn parse_lit_str<T: Parse>(lit_str: &LitStr, cx: &AttrCtxt) -> Result<T, ()> {
84    lit_str.parse().map_err(|_| {
85        cx.error_spanned_by(
86            lit_str,
87            format_args!(
88                "failed to parse \"{}\" as a {}",
89                lit_str.value(),
90                std::any::type_name::<T>()
91                    .rsplit("::")
92                    .next()
93                    .unwrap_or_default()
94                    .to_ascii_lowercase(),
95            ),
96        );
97    })
98}
99
100pub fn parse_extensions(
101    meta: &CustomMeta,
102    cx: &AttrCtxt,
103) -> Result<impl IntoIterator<Item = Extension>, ()> {
104    let parser = Punctuated::<Extension, Token![,]>::parse_terminated;
105    parse_meta_list_with(meta, cx, parser)
106}
107
108pub fn parse_length_or_range(outer_meta: &CustomMeta, cx: &AttrCtxt) -> Result<LengthOrRange, ()> {
109    let outer_name = path_str(outer_meta.path());
110    let mut result = LengthOrRange::default();
111
112    for nested_meta in parse_nested_meta(outer_meta, cx)? {
113        match path_str(nested_meta.path()).as_str() {
114            "min" => match (&result.min, &result.equal) {
115                (Some(_), _) => cx.duplicate_error(&nested_meta),
116                (_, Some(_)) => cx.mutual_exclusive_error(&nested_meta, "equal"),
117                _ => result.min = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(),
118            },
119            "max" => match (&result.max, &result.equal) {
120                (Some(_), _) => cx.duplicate_error(&nested_meta),
121                (_, Some(_)) => cx.mutual_exclusive_error(&nested_meta, "equal"),
122                _ => result.max = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(),
123            },
124            "equal" => match (&result.min, &result.max, &result.equal) {
125                (Some(_), _, _) => cx.mutual_exclusive_error(&nested_meta, "min"),
126                (_, Some(_), _) => cx.mutual_exclusive_error(&nested_meta, "max"),
127                (_, _, Some(_)) => cx.duplicate_error(&nested_meta),
128                _ => result.equal = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(),
129            },
130            unknown => {
131                if cx.attr_type == "schemars" {
132                    cx.error_spanned_by(
133                        nested_meta,
134                        format_args!(
135                            "unknown item in schemars {outer_name} attribute: `{unknown}`",
136                        ),
137                    );
138                }
139            }
140        }
141    }
142
143    Ok(result)
144}
145
146pub fn parse_pattern(meta: &CustomMeta, cx: &AttrCtxt) -> Result<Expr, ()> {
147    parse_meta_list_with(meta, cx, Expr::parse)
148}
149
150pub fn parse_schemars_regex(outer_meta: &CustomMeta, cx: &AttrCtxt) -> Result<Expr, ()> {
151    let mut pattern = None;
152
153    for nested_meta in parse_nested_meta(outer_meta, cx)? {
154        match path_str(nested_meta.path()).as_str() {
155            "pattern" => match &pattern {
156                Some(_) => cx.duplicate_error(&nested_meta),
157                None => pattern = parse_name_value_expr(nested_meta, cx).ok(),
158            },
159            "path" => {
160                cx.error_spanned_by(nested_meta, "`path` is not supported in `schemars(regex(...))` attribute - use `schemars(regex(pattern = ...))` instead");
161            }
162            unknown => {
163                cx.error_spanned_by(
164                    nested_meta,
165                    format_args!("unknown item in schemars `regex` attribute: `{unknown}`"),
166                );
167            }
168        }
169    }
170
171    pattern.ok_or_else(|| {
172        cx.error_spanned_by(
173            outer_meta,
174            "`schemars(regex(...))` attribute requires `pattern = ...`",
175        );
176    })
177}
178
179pub fn parse_validate_regex(outer_meta: &CustomMeta, cx: &AttrCtxt) -> Result<Expr, ()> {
180    let mut path = None;
181
182    for nested_meta in parse_nested_meta(outer_meta, cx)? {
183        match path_str(nested_meta.path()).as_str() {
184            "path" => match &path {
185                Some(_) => cx.duplicate_error(&nested_meta),
186                None => path = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(),
187            },
188            "pattern" => {
189                cx.error_spanned_by(nested_meta, "`pattern` is not supported in `validate(regex(...))` attribute - use either `validate(regex(path = ...))` or `schemars(regex(pattern = ...))` instead");
190            }
191            _ => {
192                // ignore unknown properties in `validate` attribute
193            }
194        }
195    }
196
197    path.ok_or_else(|| {
198        cx.error_spanned_by(
199            outer_meta,
200            "`validate(regex(...))` attribute requires `path = ...`",
201        );
202    })
203}
204
205pub fn parse_contains(outer_meta: CustomMeta, cx: &AttrCtxt) -> Result<Expr, ()> {
206    enum ContainsFormat {
207        Metas(Punctuated<CustomMeta, Token![,]>),
208        Expr(Expr),
209    }
210
211    impl Parse for ContainsFormat {
212        fn parse(input: ParseStream) -> syn::Result<Self> {
213            // An imperfect but good-enough heuristic for determining whether it looks more like a
214            // comma-separated meta list (validator-style), or a single expression (garde-style).
215            // This heuristic may not generalise well-enough for attributes other than `contains`!
216            // `foo = bar` => Metas (not Expr::Assign)
217            // `foo, bar`  => Metas
218            // `foo`       => Expr (not CustomMeta::Path)
219            // `foo(bar)`  => Expr (not CustomMeta::List)
220            if input.peek2(Token![,]) || input.peek2(Token![=]) {
221                Punctuated::parse_terminated(input).map(Self::Metas)
222            } else {
223                input.parse().map(Self::Expr)
224            }
225        }
226    }
227
228    let nested_meta_or_expr = match cx.attr_type {
229        "validate" => parse_meta_list_with(&outer_meta, cx, Punctuated::parse_terminated)
230            .map(ContainsFormat::Metas),
231        "garde" => parse_meta_list_with(&outer_meta, cx, Expr::parse).map(ContainsFormat::Expr),
232        "schemars" => parse_meta_list_with(&outer_meta, cx, ContainsFormat::parse),
233        wat => {
234            unreachable!("Unexpected attr type `{wat}` for `contains` item. This is a bug in schemars, please raise an issue!")
235        }
236    }?;
237
238    let nested_metas = match nested_meta_or_expr {
239        ContainsFormat::Expr(expr) => return Ok(expr),
240        ContainsFormat::Metas(m) => m,
241    };
242
243    let mut pattern = None;
244
245    for nested_meta in nested_metas {
246        match path_str(nested_meta.path()).as_str() {
247            "pattern" => match &pattern {
248                Some(_) => cx.duplicate_error(&nested_meta),
249                None => pattern = parse_name_value_expr(nested_meta, cx).ok(),
250            },
251            unknown => {
252                if cx.attr_type == "schemars" {
253                    cx.error_spanned_by(
254                        nested_meta,
255                        format_args!("unknown item in schemars `contains` attribute: `{unknown}`"),
256                    );
257                }
258            }
259        }
260    }
261
262    pattern.ok_or_else(|| {
263        cx.error_spanned_by(
264            outer_meta,
265            "`contains` attribute item requires `pattern = ...`",
266        );
267    })
268}
269
270pub fn parse_nested_meta(
271    meta: &CustomMeta,
272    cx: &AttrCtxt,
273) -> Result<impl IntoIterator<Item = CustomMeta>, ()> {
274    let parser = Punctuated::<CustomMeta, Token![,]>::parse_terminated;
275    parse_meta_list_with(meta, cx, parser)
276}
277
278fn parse_meta_list_with<F: Parser>(
279    meta: &CustomMeta,
280    cx: &AttrCtxt,
281    parser: F,
282) -> Result<F::Output, ()> {
283    let CustomMeta::List(meta_list) = meta else {
284        let name = path_str(meta.path());
285        cx.error_spanned_by(
286            meta,
287            format_args!(
288                "expected {} {} attribute item to be of the form `{}(...)`",
289                cx.attr_type, name, name
290            ),
291        );
292        return Err(());
293    };
294
295    meta_list.parse_args_with(parser).map_err(|err| {
296        cx.syn_error(err);
297    })
298}
299
300// Like `parse_name_value_expr`, but if the result is a string literal, then parse its contents.
301pub fn parse_name_value_expr_handle_lit_str(meta: CustomMeta, cx: &AttrCtxt) -> Result<Expr, ()> {
302    let expr = parse_name_value_expr(meta, cx)?;
303
304    if let Expr::Lit(ExprLit {
305        lit: Lit::Str(lit_str),
306        ..
307    }) = &expr
308    {
309        parse_lit_str(lit_str, cx)
310    } else {
311        Ok(expr)
312    }
313}
314
315#[derive(Default)]
316pub struct LengthOrRange {
317    pub min: Option<Expr>,
318    pub max: Option<Expr>,
319    pub equal: Option<Expr>,
320}
321
322pub struct Extension {
323    pub key_str: String,
324    pub key_lit: LitStr,
325    pub value: TokenStream,
326}
327
328impl Parse for Extension {
329    fn parse(input: ParseStream) -> syn::Result<Self> {
330        let key = input.parse::<LitStr>()?;
331        input.parse::<Token![=]>()?;
332        let mut value = TokenStream::new();
333
334        while !input.is_empty() && !input.peek(Token![,]) {
335            value.extend([input.parse::<TokenTree>()?]);
336        }
337
338        if value.is_empty() {
339            return Err(syn::Error::new(input.span(), "Expected extension value"));
340        }
341
342        Ok(Extension {
343            key_str: key.value(),
344            key_lit: key,
345            value,
346        })
347    }
348}