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 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}