schemars_derive/attr/
validation.rs

1use proc_macro2::TokenStream;
2use syn::Expr;
3
4use crate::idents::SCHEMA;
5
6use super::{
7    parse_meta::{
8        parse_contains, parse_length_or_range, parse_nested_meta, parse_pattern,
9        parse_schemars_regex, parse_validate_regex, require_path_only, LengthOrRange,
10    },
11    AttrCtxt, CustomMeta,
12};
13
14#[derive(Clone, Copy, PartialEq)]
15pub enum Format {
16    Email,
17    Uri,
18    Ip,
19    Ipv4,
20    Ipv6,
21}
22
23impl Format {
24    fn attr_str(self) -> &'static str {
25        match self {
26            Format::Email => "email",
27            Format::Uri => "url",
28            Format::Ip => "ip",
29            Format::Ipv4 => "ipv4",
30            Format::Ipv6 => "ipv6",
31        }
32    }
33
34    fn schema_str(self) -> &'static str {
35        match self {
36            Format::Email => "email",
37            Format::Uri => "uri",
38            Format::Ip => "ip",
39            Format::Ipv4 => "ipv4",
40            Format::Ipv6 => "ipv6",
41        }
42    }
43
44    fn from_attr_str(s: &str) -> Option<Self> {
45        Some(match s {
46            "email" => Format::Email,
47            "url" => Format::Uri,
48            "ip" => Format::Ip,
49            "ipv4" => Format::Ipv4,
50            "ipv6" => Format::Ipv6,
51            _ => return None,
52        })
53    }
54}
55
56#[derive(Default)]
57pub struct ValidationAttrs {
58    pub length: Option<LengthOrRange>,
59    pub range: Option<LengthOrRange>,
60    pub pattern: Option<Expr>,
61    pub regex: Option<Expr>,
62    pub contains: Option<Expr>,
63    pub required: bool,
64    pub format: Option<Format>,
65    pub inner: Option<Box<ValidationAttrs>>,
66}
67
68impl ValidationAttrs {
69    pub fn add_mutators(&self, mutators: &mut Vec<TokenStream>) {
70        self.add_mutators2(mutators, &quote!(&mut #SCHEMA));
71    }
72
73    fn add_mutators2(&self, mutators: &mut Vec<TokenStream>, mut_ref_schema: &TokenStream) {
74        if let Some(length) = &self.length {
75            Self::add_length_or_range(length, mutators, "string", "Length", mut_ref_schema);
76            Self::add_length_or_range(length, mutators, "array", "Items", mut_ref_schema);
77        }
78
79        if let Some(range) = &self.range {
80            Self::add_length_or_range(range, mutators, "number", "imum", mut_ref_schema);
81        }
82
83        if let Some(regex) = self.regex.as_ref().or(self.pattern.as_ref()) {
84            mutators.push(quote! {
85                schemars::_private::insert_validation_property(#mut_ref_schema, "string", "pattern", (#regex).to_string());
86            });
87        }
88
89        if let Some(contains) = &self.contains {
90            mutators.push(quote! {
91                schemars::_private::must_contain(#mut_ref_schema, &#contains.to_string());
92            });
93        }
94
95        if let Some(format) = &self.format {
96            let f = format.schema_str();
97            mutators.push(quote! {
98                    (#mut_ref_schema).insert("format".into(), #f.into());
99            });
100        }
101
102        if let Some(inner) = &self.inner {
103            let mut inner_mutators = Vec::new();
104            inner.add_mutators2(&mut inner_mutators, &quote!(inner_schema));
105
106            if !inner_mutators.is_empty() {
107                mutators.push(quote! {
108                    schemars::_private::apply_inner_validation(#mut_ref_schema, |inner_schema| { #(#inner_mutators)* });
109                });
110            }
111        }
112    }
113
114    fn add_length_or_range(
115        value: &LengthOrRange,
116        mutators: &mut Vec<TokenStream>,
117        required_format: &str,
118        key_suffix: &str,
119        mut_ref_schema: &TokenStream,
120    ) {
121        if let Some(min) = value.min.as_ref().or(value.equal.as_ref()) {
122            let key = format!("min{key_suffix}");
123            mutators.push(quote!{
124                schemars::_private::insert_validation_property(#mut_ref_schema, #required_format, #key, #min);
125            });
126        }
127
128        if let Some(max) = value.max.as_ref().or(value.equal.as_ref()) {
129            let key = format!("max{key_suffix}");
130            mutators.push(quote!{
131                schemars::_private::insert_validation_property(#mut_ref_schema, #required_format, #key, #max);
132            });
133        }
134    }
135
136    pub(super) fn populate(
137        &mut self,
138        schemars_cx: &mut AttrCtxt,
139        validate_cx: &mut AttrCtxt,
140        garde_cx: &mut AttrCtxt,
141    ) {
142        self.process_attr(schemars_cx);
143        self.process_attr(validate_cx);
144        self.process_attr(garde_cx);
145    }
146
147    fn process_attr(&mut self, cx: &mut AttrCtxt) {
148        cx.parse_meta(|m, n, c| self.process_meta(m, n, c));
149    }
150
151    fn process_meta(
152        &mut self,
153        meta: CustomMeta,
154        meta_name: &str,
155        cx: &AttrCtxt,
156    ) -> Result<(), CustomMeta> {
157        if let Some(format) = Format::from_attr_str(meta_name) {
158            self.handle_format(&meta, format, cx);
159            return Ok(());
160        }
161        match meta_name {
162            "length" => match self.length {
163                Some(_) => cx.duplicate_error(&meta),
164                None => self.length = parse_length_or_range(&meta, cx).ok(),
165            },
166
167            "range" => match self.range {
168                Some(_) => cx.duplicate_error(&meta),
169                None => self.range = parse_length_or_range(&meta, cx).ok(),
170            },
171
172            "required" => {
173                if self.required {
174                    cx.duplicate_error(&meta);
175                } else if require_path_only(&meta, cx).is_ok() {
176                    self.required = true;
177                }
178            }
179
180            "pattern" if cx.attr_type != "validate" => {
181                match (&self.pattern, &self.regex, &self.contains) {
182                    (Some(_p), _, _) => cx.duplicate_error(&meta),
183                    (_, Some(_r), _) => cx.mutual_exclusive_error(&meta, "regex"),
184                    (_, _, Some(_c)) => cx.mutual_exclusive_error(&meta, "contains"),
185                    (None, None, None) => self.pattern = parse_pattern(&meta, cx).ok(),
186                }
187            }
188            "regex" if cx.attr_type != "garde" => {
189                match (&self.pattern, &self.regex, &self.contains) {
190                    (Some(_p), _, _) => cx.mutual_exclusive_error(&meta, "pattern"),
191                    (_, Some(_r), _) => cx.duplicate_error(&meta),
192                    (_, _, Some(_c)) => cx.mutual_exclusive_error(&meta, "contains"),
193                    (None, None, None) => {
194                        if cx.attr_type == "validate" {
195                            self.regex = parse_validate_regex(&meta, cx).ok();
196                        } else {
197                            self.regex = parse_schemars_regex(&meta, cx).ok();
198                        }
199                    }
200                }
201            }
202            "contains" => match (&self.pattern, &self.regex, &self.contains) {
203                (Some(_p), _, _) => cx.mutual_exclusive_error(&meta, "pattern"),
204                (_, Some(_r), _) => cx.mutual_exclusive_error(&meta, "regex"),
205                (_, _, Some(_c)) => cx.duplicate_error(&meta),
206                (None, None, None) => self.contains = parse_contains(meta, cx).ok(),
207            },
208
209            "inner" if cx.attr_type != "validate" => {
210                if let Ok(nested_meta) = parse_nested_meta(&meta, cx) {
211                    let inner = self
212                        .inner
213                        .get_or_insert_with(|| Box::new(ValidationAttrs::default()));
214                    let mut inner_cx = cx.new_nested_meta(nested_meta.into_iter().collect());
215                    inner.process_attr(&mut inner_cx);
216                }
217            }
218
219            _ => return Err(meta),
220        }
221
222        Ok(())
223    }
224
225    fn handle_format(&mut self, meta: &CustomMeta, format: Format, cx: &AttrCtxt) {
226        match self.format {
227            Some(current) if current == format => cx.duplicate_error(meta),
228            Some(current) => cx.mutual_exclusive_error(meta, current.attr_str()),
229            None => {
230                // Allow a MetaList in validator attr (e.g. with message/code items),
231                // but restrict it to path only in schemars attr.
232                if cx.attr_type == "validate" || require_path_only(meta, cx).is_ok() {
233                    self.format = Some(format);
234                }
235            }
236        }
237    }
238
239    pub(crate) fn is_default(&self) -> bool {
240        matches!(
241            self,
242            Self {
243                contains: None,
244                format: None,
245                length: None,
246                range: None,
247                pattern: None,
248                regex: None,
249                required: false,
250                inner: None,
251            }
252        )
253    }
254}