utoipa_gen/component/features/
validation.rs

1use std::str::FromStr;
2
3use proc_macro2::{Ident, Literal, Span, TokenStream, TokenTree};
4use quote::{quote, ToTokens};
5use syn::parse::ParseStream;
6use syn::LitStr;
7
8use crate::{parse_utils, Diagnostics};
9
10use super::validators::Validator;
11use super::{impl_feature, Feature, Parse, Validate};
12
13#[inline]
14fn from_str<T: FromStr>(number: &str, span: Span) -> syn::Result<T>
15where
16    <T as std::str::FromStr>::Err: std::fmt::Display,
17{
18    T::from_str(number).map_err(|error| syn::Error::new(span, error))
19}
20
21#[derive(Clone)]
22#[cfg_attr(feature = "debug", derive(Debug))]
23pub struct NumberValue {
24    minus: bool,
25    pub lit: Literal,
26}
27
28impl NumberValue {
29    pub fn try_from_str<T>(&self) -> syn::Result<T>
30    where
31        T: FromStr,
32        <T as std::str::FromStr>::Err: std::fmt::Display,
33    {
34        let number = if self.minus {
35            format!("-{}", &self.lit)
36        } else {
37            self.lit.to_string()
38        };
39
40        let parsed = from_str::<T>(&number, self.lit.span())?;
41        Ok(parsed)
42    }
43}
44
45impl syn::parse::Parse for NumberValue {
46    fn parse(input: ParseStream) -> syn::Result<Self> {
47        let mut minus = false;
48        let result = input.step(|cursor| {
49            let mut rest = *cursor;
50
51            while let Some((tt, next)) = rest.token_tree() {
52                match &tt {
53                    TokenTree::Punct(punct) if punct.as_char() == '-' => {
54                        minus = true;
55                    }
56                    TokenTree::Literal(lit) => return Ok((lit.clone(), next)),
57                    _ => (),
58                }
59                rest = next;
60            }
61            Err(cursor.error("no `literal` value found after this point"))
62        })?;
63
64        Ok(Self { minus, lit: result })
65    }
66}
67
68impl ToTokens for NumberValue {
69    fn to_tokens(&self, tokens: &mut TokenStream) {
70        let punct = if self.minus { Some(quote! {-}) } else { None };
71        let lit = &self.lit;
72
73        tokens.extend(quote! {
74            #punct #lit
75        })
76    }
77}
78
79#[inline]
80fn parse_next_number_value(input: ParseStream) -> syn::Result<NumberValue> {
81    use syn::parse::Parse;
82    parse_utils::parse_next(input, || NumberValue::parse(input))
83}
84
85impl_feature! {
86    #[cfg_attr(feature = "debug", derive(Debug))]
87    #[derive(Clone)]
88    pub struct MultipleOf(pub(super) NumberValue, Ident);
89}
90
91impl Validate for MultipleOf {
92    fn validate(&self, validator: impl Validator) -> Option<Diagnostics> {
93        match validator.is_valid() {
94            Err(error) => Some(Diagnostics::with_span(self.1.span(), format!( "`multiple_of` error: {}", error))
95                .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-multipleof`")),
96            _ => None
97        }
98    }
99}
100
101impl Parse for MultipleOf {
102    fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> {
103        parse_next_number_value(input).map(|number| Self(number, ident))
104    }
105}
106
107impl ToTokens for MultipleOf {
108    fn to_tokens(&self, tokens: &mut TokenStream) {
109        self.0.to_tokens(tokens);
110    }
111}
112
113impl From<MultipleOf> for Feature {
114    fn from(value: MultipleOf) -> Self {
115        Feature::MultipleOf(value)
116    }
117}
118
119impl_feature! {
120    #[cfg_attr(feature = "debug", derive(Debug))]
121    #[derive(Clone)]
122    pub struct Maximum(pub(super) NumberValue, Ident);
123}
124
125impl Validate for Maximum {
126    fn validate(&self, validator: impl Validator) -> Option<Diagnostics> {
127        match validator.is_valid() {
128            Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`maximum` error: {}", error))
129                .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maximum`")),
130            _ => None,
131        }
132    }
133}
134
135impl Parse for Maximum {
136    fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self>
137    where
138        Self: Sized,
139    {
140        parse_next_number_value(input).map(|number| Self(number, ident))
141    }
142}
143
144impl ToTokens for Maximum {
145    fn to_tokens(&self, tokens: &mut TokenStream) {
146        self.0.to_tokens(tokens);
147    }
148}
149
150impl From<Maximum> for Feature {
151    fn from(value: Maximum) -> Self {
152        Feature::Maximum(value)
153    }
154}
155
156impl_feature! {
157    #[cfg_attr(feature = "debug", derive(Debug))]
158    #[derive(Clone)]
159    pub struct Minimum(NumberValue, Ident);
160}
161
162impl Minimum {
163    pub fn new(value: f64, span: Span) -> Self {
164        Self(
165            NumberValue {
166                minus: value < 0.0,
167                lit: Literal::f64_suffixed(value),
168            },
169            Ident::new("empty", span),
170        )
171    }
172}
173
174impl Validate for Minimum {
175    fn validate(&self, validator: impl Validator) -> Option<Diagnostics> {
176        match validator.is_valid() {
177            Err(error) => Some(
178                Diagnostics::with_span(self.1.span(), format!("`minimum` error: {}", error))
179                .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minimum`")
180            ),
181            _ => None,
182        }
183    }
184}
185
186impl Parse for Minimum {
187    fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self>
188    where
189        Self: Sized,
190    {
191        parse_next_number_value(input).map(|number| Self(number, ident))
192    }
193}
194
195impl ToTokens for Minimum {
196    fn to_tokens(&self, tokens: &mut TokenStream) {
197        self.0.to_tokens(tokens);
198    }
199}
200
201impl From<Minimum> for Feature {
202    fn from(value: Minimum) -> Self {
203        Feature::Minimum(value)
204    }
205}
206
207impl_feature! {
208    #[cfg_attr(feature = "debug", derive(Debug))]
209    #[derive(Clone)]
210    pub struct ExclusiveMaximum(NumberValue, Ident);
211}
212
213impl Validate for ExclusiveMaximum {
214    fn validate(&self, validator: impl Validator) -> Option<Diagnostics> {
215        match validator.is_valid() {
216            Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`exclusive_maximum` error: {}", error))
217                .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-exclusivemaximum`")),
218            _ => None,
219        }
220    }
221}
222
223impl Parse for ExclusiveMaximum {
224    fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self>
225    where
226        Self: Sized,
227    {
228        parse_next_number_value(input).map(|number| Self(number, ident))
229    }
230}
231
232impl ToTokens for ExclusiveMaximum {
233    fn to_tokens(&self, tokens: &mut TokenStream) {
234        self.0.to_tokens(tokens);
235    }
236}
237
238impl From<ExclusiveMaximum> for Feature {
239    fn from(value: ExclusiveMaximum) -> Self {
240        Feature::ExclusiveMaximum(value)
241    }
242}
243
244impl_feature! {
245    #[cfg_attr(feature = "debug", derive(Debug))]
246    #[derive(Clone)]
247    pub struct ExclusiveMinimum(NumberValue, Ident);
248}
249
250impl Validate for ExclusiveMinimum {
251    fn validate(&self, validator: impl Validator) -> Option<Diagnostics> {
252        match validator.is_valid() {
253            Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`exclusive_minimum` error: {}", error))
254                .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-exclusiveminimum`")),
255            _ => None,
256        }
257    }
258}
259
260impl Parse for ExclusiveMinimum {
261    fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self>
262    where
263        Self: Sized,
264    {
265        parse_next_number_value(input).map(|number| Self(number, ident))
266    }
267}
268
269impl ToTokens for ExclusiveMinimum {
270    fn to_tokens(&self, tokens: &mut TokenStream) {
271        self.0.to_tokens(tokens);
272    }
273}
274
275impl From<ExclusiveMinimum> for Feature {
276    fn from(value: ExclusiveMinimum) -> Self {
277        Feature::ExclusiveMinimum(value)
278    }
279}
280
281impl_feature! {
282    #[cfg_attr(feature = "debug", derive(Debug))]
283    #[derive(Clone)]
284    pub struct MaxLength(pub(super) NumberValue, Ident);
285}
286
287impl Validate for MaxLength {
288    fn validate(&self, validator: impl Validator) -> Option<Diagnostics> {
289        match validator.is_valid() {
290            Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`max_length` error: {}", error))
291                .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maxlength`")),
292            _ => None,
293        }
294    }
295}
296
297impl Parse for MaxLength {
298    fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self>
299    where
300        Self: Sized,
301    {
302        parse_next_number_value(input).map(|number| Self(number, ident))
303    }
304}
305
306impl ToTokens for MaxLength {
307    fn to_tokens(&self, tokens: &mut TokenStream) {
308        self.0.to_tokens(tokens);
309    }
310}
311
312impl From<MaxLength> for Feature {
313    fn from(value: MaxLength) -> Self {
314        Feature::MaxLength(value)
315    }
316}
317
318impl_feature! {
319    #[cfg_attr(feature = "debug", derive(Debug))]
320    #[derive(Clone)]
321    pub struct MinLength(pub(super) NumberValue, Ident);
322}
323
324impl Validate for MinLength {
325    fn validate(&self, validator: impl Validator) -> Option<Diagnostics> {
326        match validator.is_valid() {
327            Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`min_length` error: {}", error))
328                .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minlength`")),
329            _ => None,
330        }
331    }
332}
333
334impl Parse for MinLength {
335    fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self>
336    where
337        Self: Sized,
338    {
339        parse_next_number_value(input).map(|number| Self(number, ident))
340    }
341}
342
343impl ToTokens for MinLength {
344    fn to_tokens(&self, tokens: &mut TokenStream) {
345        self.0.to_tokens(tokens);
346    }
347}
348
349impl From<MinLength> for Feature {
350    fn from(value: MinLength) -> Self {
351        Feature::MinLength(value)
352    }
353}
354
355impl_feature! {
356    #[cfg_attr(feature = "debug", derive(Debug))]
357    #[derive(Clone)]
358    pub struct Pattern(String, Ident);
359}
360
361impl Validate for Pattern {
362    fn validate(&self, validator: impl Validator) -> Option<Diagnostics> {
363        match validator.is_valid() {
364            Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`pattern` error: {}", error))
365                .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-pattern`")
366            ),
367            _ => None,
368        }
369    }
370}
371
372impl Parse for Pattern {
373    fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self>
374    where
375        Self: Sized,
376    {
377        parse_utils::parse_next(input, || input.parse::<LitStr>())
378            .map(|pattern| Self(pattern.value(), ident))
379    }
380}
381
382impl ToTokens for Pattern {
383    fn to_tokens(&self, tokens: &mut TokenStream) {
384        self.0.to_tokens(tokens);
385    }
386}
387
388impl From<Pattern> for Feature {
389    fn from(value: Pattern) -> Self {
390        Feature::Pattern(value)
391    }
392}
393
394impl_feature! {
395    #[cfg_attr(feature = "debug", derive(Debug))]
396    #[derive(Clone)]
397    pub struct MaxItems(pub(super) NumberValue, Ident);
398}
399
400impl Validate for MaxItems {
401    fn validate(&self, validator: impl Validator) -> Option<Diagnostics> {
402        match validator.is_valid() {
403            Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`max_items` error: {}", error))
404                .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maxitems")),
405            _ => None,
406        }
407    }
408}
409
410impl Parse for MaxItems {
411    fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self>
412    where
413        Self: Sized,
414    {
415        parse_next_number_value(input).map(|number| Self(number, ident))
416    }
417}
418
419impl ToTokens for MaxItems {
420    fn to_tokens(&self, tokens: &mut TokenStream) {
421        self.0.to_tokens(tokens);
422    }
423}
424
425impl From<MaxItems> for Feature {
426    fn from(value: MaxItems) -> Self {
427        Feature::MaxItems(value)
428    }
429}
430
431impl_feature! {
432    #[cfg_attr(feature = "debug", derive(Debug))]
433    #[derive(Clone)]
434    pub struct MinItems(pub(super) NumberValue, Ident);
435}
436
437impl Validate for MinItems {
438    fn validate(&self, validator: impl Validator) -> Option<Diagnostics> {
439        match validator.is_valid() {
440            Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`min_items` error: {}", error))
441                .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minitems")),
442            _ => None,
443        }
444    }
445}
446
447impl Parse for MinItems {
448    fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self>
449    where
450        Self: Sized,
451    {
452        parse_next_number_value(input).map(|number| Self(number, ident))
453    }
454}
455
456impl ToTokens for MinItems {
457    fn to_tokens(&self, tokens: &mut TokenStream) {
458        self.0.to_tokens(tokens);
459    }
460}
461
462impl From<MinItems> for Feature {
463    fn from(value: MinItems) -> Self {
464        Feature::MinItems(value)
465    }
466}
467
468impl_feature! {
469    #[cfg_attr(feature = "debug", derive(Debug))]
470    #[derive(Clone)]
471    pub struct MaxProperties(NumberValue, ());
472}
473
474impl Parse for MaxProperties {
475    fn parse(input: ParseStream, _ident: Ident) -> syn::Result<Self>
476    where
477        Self: Sized,
478    {
479        parse_next_number_value(input).map(|number| Self(number, ()))
480    }
481}
482
483impl ToTokens for MaxProperties {
484    fn to_tokens(&self, tokens: &mut TokenStream) {
485        self.0.to_tokens(tokens);
486    }
487}
488
489impl From<MaxProperties> for Feature {
490    fn from(value: MaxProperties) -> Self {
491        Feature::MaxProperties(value)
492    }
493}
494
495impl_feature! {
496    #[cfg_attr(feature = "debug", derive(Debug))]
497    #[derive(Clone)]
498    pub struct MinProperties(NumberValue, ());
499}
500
501impl Parse for MinProperties {
502    fn parse(input: ParseStream, _ident: Ident) -> syn::Result<Self>
503    where
504        Self: Sized,
505    {
506        parse_next_number_value(input).map(|number| Self(number, ()))
507    }
508}
509
510impl ToTokens for MinProperties {
511    fn to_tokens(&self, tokens: &mut TokenStream) {
512        self.0.to_tokens(tokens);
513    }
514}
515
516impl From<MinProperties> for Feature {
517    fn from(value: MinProperties) -> Self {
518        Feature::MinProperties(value)
519    }
520}