schemars_derive/attr/
validation.rs

1use super::{expr_as_lit_str, get_meta_items, parse_lit_into_path, parse_lit_str};
2use proc_macro2::TokenStream;
3use quote::ToTokens;
4use serde_derive_internals::Ctxt;
5use syn::{
6    parse::Parser, punctuated::Punctuated, Expr, ExprPath, Lit, Meta, MetaList, MetaNameValue, Path,
7};
8
9pub(crate) static VALIDATION_KEYWORDS: &[&str] = &[
10    "range", "regex", "contains", "email", "phone", "url", "length", "required",
11];
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14enum Format {
15    Email,
16    Uri,
17    Phone,
18}
19
20impl Format {
21    fn attr_str(self) -> &'static str {
22        match self {
23            Format::Email => "email",
24            Format::Uri => "url",
25            Format::Phone => "phone",
26        }
27    }
28
29    fn schema_str(self) -> &'static str {
30        match self {
31            Format::Email => "email",
32            Format::Uri => "uri",
33            Format::Phone => "phone",
34        }
35    }
36}
37
38#[derive(Debug, Default)]
39pub struct ValidationAttrs {
40    length_min: Option<Expr>,
41    length_max: Option<Expr>,
42    length_equal: Option<Expr>,
43    range_min: Option<Expr>,
44    range_max: Option<Expr>,
45    regex: Option<Expr>,
46    contains: Option<String>,
47    required: bool,
48    format: Option<Format>,
49    inner: Option<Box<ValidationAttrs>>,
50}
51
52impl ValidationAttrs {
53    pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self {
54        let schemars_items = get_meta_items(attrs, "schemars", errors, false);
55        let validate_items = get_meta_items(attrs, "validate", errors, true);
56
57        ValidationAttrs::default()
58            .populate(schemars_items, "schemars", false, errors)
59            .populate(validate_items, "validate", true, errors)
60    }
61
62    pub fn required(&self) -> bool {
63        self.required
64    }
65
66    fn populate(
67        mut self,
68        meta_items: Vec<Meta>,
69        attr_type: &'static str,
70        ignore_errors: bool,
71        errors: &Ctxt,
72    ) -> Self {
73        let duplicate_error = |path: &Path| {
74            if !ignore_errors {
75                let msg = format!(
76                    "duplicate schemars attribute `{}`",
77                    path.get_ident().unwrap()
78                );
79                errors.error_spanned_by(path, msg)
80            }
81        };
82        let mutual_exclusive_error = |path: &Path, other: &str| {
83            if !ignore_errors {
84                let msg = format!(
85                    "schemars attribute cannot contain both `{}` and `{}`",
86                    path.get_ident().unwrap(),
87                    other,
88                );
89                errors.error_spanned_by(path, msg)
90            }
91        };
92        let duplicate_format_error = |existing: Format, new: Format, path: &syn::Path| {
93            if !ignore_errors {
94                let msg = if existing == new {
95                    format!("duplicate schemars attribute `{}`", existing.attr_str())
96                } else {
97                    format!(
98                        "schemars attribute cannot contain both `{}` and `{}`",
99                        existing.attr_str(),
100                        new.attr_str(),
101                    )
102                };
103                errors.error_spanned_by(path, msg)
104            }
105        };
106        let parse_nested_meta = |meta_list: MetaList| {
107            let parser = Punctuated::<syn::Meta, Token![,]>::parse_terminated;
108            match parser.parse2(meta_list.tokens) {
109                Ok(p) => p,
110                Err(e) => {
111                    if !ignore_errors {
112                        errors.syn_error(e);
113                    }
114                    Default::default()
115                }
116            }
117        };
118
119        for meta_item in meta_items {
120            match meta_item {
121                Meta::List(meta_list) if meta_list.path.is_ident("length") => {
122                    for nested in parse_nested_meta(meta_list) {
123                        match nested {
124                            Meta::NameValue(nv) if nv.path.is_ident("min") => {
125                                if self.length_min.is_some() {
126                                    duplicate_error(&nv.path)
127                                } else if self.length_equal.is_some() {
128                                    mutual_exclusive_error(&nv.path, "equal")
129                                } else {
130                                    self.length_min = str_or_num_to_expr(errors, "min", nv.value);
131                                }
132                            }
133                            Meta::NameValue(nv) if nv.path.is_ident("max") => {
134                                if self.length_max.is_some() {
135                                    duplicate_error(&nv.path)
136                                } else if self.length_equal.is_some() {
137                                    mutual_exclusive_error(&nv.path, "equal")
138                                } else {
139                                    self.length_max = str_or_num_to_expr(errors, "max", nv.value);
140                                }
141                            }
142                            Meta::NameValue(nv) if nv.path.is_ident("equal") => {
143                                if self.length_equal.is_some() {
144                                    duplicate_error(&nv.path)
145                                } else if self.length_min.is_some() {
146                                    mutual_exclusive_error(&nv.path, "min")
147                                } else if self.length_max.is_some() {
148                                    mutual_exclusive_error(&nv.path, "max")
149                                } else {
150                                    self.length_equal =
151                                        str_or_num_to_expr(errors, "equal", nv.value);
152                                }
153                            }
154                            meta => {
155                                if !ignore_errors {
156                                    errors.error_spanned_by(
157                                        meta,
158                                        "unknown item in schemars length attribute".to_string(),
159                                    );
160                                }
161                            }
162                        }
163                    }
164                }
165
166                Meta::List(meta_list) if meta_list.path.is_ident("range") => {
167                    for nested in parse_nested_meta(meta_list) {
168                        match nested {
169                            Meta::NameValue(nv) if nv.path.is_ident("min") => {
170                                if self.range_min.is_some() {
171                                    duplicate_error(&nv.path)
172                                } else {
173                                    self.range_min = str_or_num_to_expr(errors, "min", nv.value);
174                                }
175                            }
176                            Meta::NameValue(nv) if nv.path.is_ident("max") => {
177                                if self.range_max.is_some() {
178                                    duplicate_error(&nv.path)
179                                } else {
180                                    self.range_max = str_or_num_to_expr(errors, "max", nv.value);
181                                }
182                            }
183                            meta => {
184                                if !ignore_errors {
185                                    errors.error_spanned_by(
186                                        meta,
187                                        "unknown item in schemars range attribute".to_string(),
188                                    );
189                                }
190                            }
191                        }
192                    }
193                }
194
195                Meta::Path(m) if m.is_ident("required") || m.is_ident("required_nested") => {
196                    self.required = true;
197                }
198
199                Meta::Path(p) if p.is_ident(Format::Email.attr_str()) => match self.format {
200                    Some(f) => duplicate_format_error(f, Format::Email, &p),
201                    None => self.format = Some(Format::Email),
202                },
203                Meta::Path(p) if p.is_ident(Format::Uri.attr_str()) => match self.format {
204                    Some(f) => duplicate_format_error(f, Format::Uri, &p),
205                    None => self.format = Some(Format::Uri),
206                },
207                Meta::Path(p) if p.is_ident(Format::Phone.attr_str()) => match self.format {
208                    Some(f) => duplicate_format_error(f, Format::Phone, &p),
209                    None => self.format = Some(Format::Phone),
210                },
211
212                Meta::NameValue(nv) if nv.path.is_ident("regex") => {
213                    match (&self.regex, &self.contains) {
214                        (Some(_), _) => duplicate_error(&nv.path),
215                        (None, Some(_)) => mutual_exclusive_error(&nv.path, "contains"),
216                        (None, None) => {
217                            self.regex =
218                                parse_lit_into_expr_path(errors, attr_type, "regex", &nv.value).ok()
219                        }
220                    }
221                }
222
223                Meta::List(meta_list) if meta_list.path.is_ident("regex") => {
224                    match (&self.regex, &self.contains) {
225                        (Some(_), _) => duplicate_error(&meta_list.path),
226                        (None, Some(_)) => mutual_exclusive_error(&meta_list.path, "contains"),
227                        (None, None) => {
228                            for x in parse_nested_meta(meta_list) {
229                                match x {
230                                    Meta::NameValue(MetaNameValue { path, value, .. })
231                                        if path.is_ident("path") =>
232                                    {
233                                        self.regex = parse_lit_into_expr_path(
234                                            errors, attr_type, "path", &value,
235                                        )
236                                        .ok()
237                                    }
238                                    Meta::NameValue(MetaNameValue { path, value, .. })
239                                        if path.is_ident("pattern") =>
240                                    {
241                                        self.regex =
242                                            expr_as_lit_str(errors, attr_type, "pattern", &value)
243                                                .ok()
244                                                .map(|litstr| {
245                                                    Expr::Lit(syn::ExprLit {
246                                                        attrs: Vec::new(),
247                                                        lit: Lit::Str(litstr.clone()),
248                                                    })
249                                                })
250                                    }
251                                    meta => {
252                                        if !ignore_errors {
253                                            errors.error_spanned_by(
254                                                meta,
255                                                "unknown item in schemars regex attribute"
256                                                    .to_string(),
257                                            );
258                                        }
259                                    }
260                                }
261                            }
262                        }
263                    }
264                }
265
266                Meta::NameValue(MetaNameValue { path, value, .. }) if path.is_ident("contains") => {
267                    match (&self.contains, &self.regex) {
268                        (Some(_), _) => duplicate_error(&path),
269                        (None, Some(_)) => mutual_exclusive_error(&path, "regex"),
270                        (None, None) => {
271                            self.contains = expr_as_lit_str(errors, attr_type, "contains", &value)
272                                .map(|litstr| litstr.value())
273                                .ok()
274                        }
275                    }
276                }
277
278                Meta::List(meta_list) if meta_list.path.is_ident("contains") => {
279                    match (&self.contains, &self.regex) {
280                        (Some(_), _) => duplicate_error(&meta_list.path),
281                        (None, Some(_)) => mutual_exclusive_error(&meta_list.path, "regex"),
282                        (None, None) => {
283                            for x in parse_nested_meta(meta_list) {
284                                match x {
285                                    Meta::NameValue(MetaNameValue { path, value, .. })
286                                        if path.is_ident("pattern") =>
287                                    {
288                                        self.contains =
289                                            expr_as_lit_str(errors, attr_type, "contains", &value)
290                                                .ok()
291                                                .map(|litstr| litstr.value())
292                                    }
293                                    meta => {
294                                        if !ignore_errors {
295                                            errors.error_spanned_by(
296                                                meta,
297                                                "unknown item in schemars contains attribute"
298                                                    .to_string(),
299                                            );
300                                        }
301                                    }
302                                }
303                            }
304                        }
305                    }
306                }
307
308                Meta::List(meta_list) if meta_list.path.is_ident("inner") => match self.inner {
309                    Some(_) => duplicate_error(&meta_list.path),
310                    None => {
311                        let inner_attrs = ValidationAttrs::default().populate(
312                            parse_nested_meta(meta_list).into_iter().collect(),
313                            attr_type,
314                            ignore_errors,
315                            errors,
316                        );
317                        self.inner = Some(Box::new(inner_attrs));
318                    }
319                },
320
321                _ => {}
322            }
323        }
324        self
325    }
326
327    pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) {
328        if let Some(apply_expr) = self.apply_to_schema_expr() {
329            *schema_expr = quote! {
330                {
331                    let mut schema = #schema_expr;
332                    #apply_expr
333                    schema
334                }
335            }
336        }
337    }
338
339    fn apply_to_schema_expr(&self) -> Option<TokenStream> {
340        let mut array_validation = Vec::new();
341        let mut number_validation = Vec::new();
342        let mut object_validation = Vec::new();
343        let mut string_validation = Vec::new();
344
345        if let Some(length_min) = self.length_min.as_ref().or(self.length_equal.as_ref()) {
346            string_validation.push(quote! {
347                validation.min_length = Some(#length_min as u32);
348            });
349            array_validation.push(quote! {
350                validation.min_items = Some(#length_min as u32);
351            });
352        }
353
354        if let Some(length_max) = self.length_max.as_ref().or(self.length_equal.as_ref()) {
355            string_validation.push(quote! {
356                validation.max_length = Some(#length_max as u32);
357            });
358            array_validation.push(quote! {
359                validation.max_items = Some(#length_max as u32);
360            });
361        }
362
363        if let Some(range_min) = &self.range_min {
364            number_validation.push(quote! {
365                validation.minimum = Some(#range_min as f64);
366            });
367        }
368
369        if let Some(range_max) = &self.range_max {
370            number_validation.push(quote! {
371                validation.maximum = Some(#range_max as f64);
372            });
373        }
374
375        if let Some(regex) = &self.regex {
376            string_validation.push(quote! {
377                validation.pattern = Some(#regex.to_string());
378            });
379        }
380
381        if let Some(contains) = &self.contains {
382            object_validation.push(quote! {
383                validation.required.insert(#contains.to_string());
384            });
385
386            if self.regex.is_none() {
387                let pattern = crate::regex_syntax::escape(contains);
388                string_validation.push(quote! {
389                    validation.pattern = Some(#pattern.to_string());
390                });
391            }
392        }
393
394        let format = self.format.as_ref().map(|f| {
395            let f = f.schema_str();
396            quote! {
397                schema_object.format = Some(#f.to_string());
398            }
399        });
400
401        let inner_validation = self
402            .inner
403            .as_deref()
404            .and_then(|inner| inner.apply_to_schema_expr())
405            .map(|apply_expr| {
406                quote! {
407                    if schema_object.has_type(schemars::schema::InstanceType::Array) {
408                        if let Some(schemars::schema::SingleOrVec::Single(inner_schema)) = &mut schema_object.array().items {
409                            let mut schema = &mut **inner_schema;
410                            #apply_expr
411                        }
412                    }
413                }
414            });
415
416        let array_validation = wrap_array_validation(array_validation);
417        let number_validation = wrap_number_validation(number_validation);
418        let object_validation = wrap_object_validation(object_validation);
419        let string_validation = wrap_string_validation(string_validation);
420
421        if array_validation.is_some()
422            || number_validation.is_some()
423            || object_validation.is_some()
424            || string_validation.is_some()
425            || format.is_some()
426            || inner_validation.is_some()
427        {
428            Some(quote! {
429                if let schemars::schema::Schema::Object(schema_object) = &mut schema {
430                    #array_validation
431                    #number_validation
432                    #object_validation
433                    #string_validation
434                    #format
435                    #inner_validation
436                }
437            })
438        } else {
439            None
440        }
441    }
442}
443
444fn parse_lit_into_expr_path(
445    cx: &Ctxt,
446    attr_type: &'static str,
447    meta_item_name: &'static str,
448    lit: &Expr,
449) -> Result<Expr, ()> {
450    parse_lit_into_path(cx, attr_type, meta_item_name, lit).map(|path| {
451        Expr::Path(ExprPath {
452            attrs: Vec::new(),
453            qself: None,
454            path,
455        })
456    })
457}
458
459fn wrap_array_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
460    if v.is_empty() {
461        None
462    } else {
463        Some(quote! {
464            if schema_object.has_type(schemars::schema::InstanceType::Array) {
465                let validation = schema_object.array();
466                #(#v)*
467            }
468        })
469    }
470}
471
472fn wrap_number_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
473    if v.is_empty() {
474        None
475    } else {
476        Some(quote! {
477            if schema_object.has_type(schemars::schema::InstanceType::Integer)
478                || schema_object.has_type(schemars::schema::InstanceType::Number) {
479                let validation = schema_object.number();
480                #(#v)*
481            }
482        })
483    }
484}
485
486fn wrap_object_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
487    if v.is_empty() {
488        None
489    } else {
490        Some(quote! {
491            if schema_object.has_type(schemars::schema::InstanceType::Object) {
492                let validation = schema_object.object();
493                #(#v)*
494            }
495        })
496    }
497}
498
499fn wrap_string_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
500    if v.is_empty() {
501        None
502    } else {
503        Some(quote! {
504            if schema_object.has_type(schemars::schema::InstanceType::String) {
505                let validation = schema_object.string();
506                #(#v)*
507            }
508        })
509    }
510}
511
512fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, expr: Expr) -> Option<Expr> {
513    // this odd double-parsing is to make `-10` parsed as an Lit instead of an Expr::Unary
514    let lit: Lit = match syn::parse2(expr.to_token_stream()) {
515        Ok(l) => l,
516        Err(err) => {
517            cx.syn_error(err);
518            return None;
519        }
520    };
521
522    match lit {
523        Lit::Str(s) => parse_lit_str::<ExprPath>(&s).ok().map(Expr::Path),
524        Lit::Int(_) | Lit::Float(_) => Some(expr),
525        _ => {
526            cx.error_spanned_by(
527                &expr,
528                format!(
529                    "expected `{}` to be a string or number literal, not {:?}",
530                    meta_item_name, &expr
531                ),
532            );
533            None
534        }
535    }
536}