Skip to main content

vector_config_common/
validation.rs

1use darling::FromMeta;
2use proc_macro2::TokenStream;
3use quote::{ToTokens, quote};
4use syn::{Expr, Lit, Meta};
5
6use crate::{
7    num::{ERR_NUMERIC_OUT_OF_RANGE, NUMERIC_ENFORCED_LOWER_BOUND, NUMERIC_ENFORCED_UPPER_BOUND},
8    schema::{InstanceType, SchemaObject},
9};
10
11/// Well-known validator formats as described in the [JSON Schema Validation specification][jsvs].
12///
13/// Not all defined formats are present here.
14///
15/// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02
16#[derive(Clone, Debug, FromMeta)]
17pub enum Format {
18    /// A date.
19    ///
20    /// Conforms to the `full-date` production as outlined in [RFC 3339, section 5.6][rfc3339], and specified in the
21    /// [JSON Schema Validation specification, section 7.3.1][jsvs].
22    ///
23    /// [rfc3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
24    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.1
25    Date,
26
27    /// A time.
28    ///
29    /// Conforms to the `full-time` production as outlined in [RFC 3339, section 5.6][rfc3339], and specified in the
30    /// [JSON Schema Validation specification, section 7.3.1][jsvs].
31    ///
32    /// [rfc3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
33    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.1
34    Time,
35
36    /// A datetime.
37    ///
38    /// Conforms to the `date-time` production as outlined in [RFC 3339, section 5.6][rfc3339], and specified in the
39    /// [JSON Schema Validation specification, section 7.3.1][jsvs].
40    ///
41    /// [rfc3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
42    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.1
43    #[darling(rename = "date-time")]
44    DateTime,
45
46    /// A duration.
47    ///
48    /// Conforms to the `duration` production as outlined in [RFC 3339, appendix A][rfc3339], and specified in the
49    /// [JSON Schema Validation specification, section 7.3.1][jsvs].
50    ///
51    /// [rfc3339]: https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
52    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.1
53    Duration,
54
55    /// An email address.
56    ///
57    /// Conforms to the `addr-spec` production as outlined in [RFC 5322, section 3.4.1][rfc5322], and specified in the
58    /// [JSON Schema Validation specification, section 7.3.2][jsvs].
59    ///
60    /// [rfc5322]: https://datatracker.ietf.org/doc/html/rfc5322#section-3.4.1
61    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.2
62    Email,
63
64    /// An Internet hostname.
65    ///
66    /// Conforms to the `hname` production as outlined in [RFC 952, section "GRAMMATICAL HOST TABLE SPECIFICATION"][rfc952],
67    /// and specified in the [JSON Schema Validation specification, section 7.3.3][jsvs].
68    ///
69    /// [rfc952]: https://datatracker.ietf.org/doc/html/rfc952
70    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.3
71    Hostname,
72
73    /// A uniform resource identifier (URI).
74    ///
75    /// Conforms to the `URI` production as outlined in [RFC 3986, appendix A][rfc3986], and specified in the [JSON
76    /// Schema Validation specification, section 7.3.5][jsvs].
77    ///
78    /// [rfc3986]: https://datatracker.ietf.org/doc/html/rfc3986#appendix-A
79    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.5
80    Uri,
81
82    /// An IPv4 address.
83    ///
84    /// Conforms to the `dotted-quad` production as outlined in [RFC 2673, section 3.2][rfc2673], and specified in the
85    /// [JSON Schema Validation specification, section 7.3.4][jsvs].
86    ///
87    /// [rfc2673]: https://datatracker.ietf.org/doc/html/rfc2673#section-3.2
88    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.4
89    #[darling(rename = "ipv4")]
90    IPv4,
91
92    /// An IPv6 address.
93    ///
94    /// Conforms to the "conventional text forms" as outlined in [RFC 4291, section 2.2][rfc4291], and specified in the
95    /// [JSON Schema Validation specification, section 7.3.4][jsvs].
96    ///
97    /// [rfc4291]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.2
98    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.4
99    #[darling(rename = "ipv6")]
100    IPv6,
101
102    /// A universally unique identifier (UUID).
103    ///
104    /// Conforms to the `UUID` production as outlined in [RFC 4122, section 3][rfc4122], and specified in the
105    /// [JSON Schema Validation specification, section 7.3.5][jsvs].
106    ///
107    /// [rfc4122]: https://datatracker.ietf.org/doc/html/rfc4122#section-3
108    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.5
109    Uuid,
110
111    /// A regular expression.
112    ///
113    /// Conforms to the specification as outlined in [ECMA 262][emca262], and specified in the
114    /// [JSON Schema Validation specification, section 7.3.8][jsvs].
115    ///
116    /// [emca262]: https://www.ecma-international.org/publications-and-standards/standards/ecma-262/
117    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.8
118    Regex,
119}
120
121impl Format {
122    pub fn as_str(&self) -> &'static str {
123        match self {
124            Format::Date => "date",
125            Format::Time => "time",
126            Format::DateTime => "date-time",
127            Format::Duration => "duration",
128            Format::Email => "email",
129            Format::Hostname => "hostname",
130            Format::Uri => "uri",
131            Format::IPv4 => "ipv4",
132            Format::IPv6 => "ipv6",
133            Format::Uuid => "uuid",
134            Format::Regex => "regex",
135        }
136    }
137}
138
139impl ToTokens for Format {
140    fn to_tokens(&self, tokens: &mut TokenStream) {
141        let format_tokens = match self {
142            Format::Date => quote! { ::vector_config::validation::Format::Date },
143            Format::Time => quote! { ::vector_config::validation::Format::Time },
144            Format::DateTime => quote! { ::vector_config::validation::Format::DateTime },
145            Format::Duration => quote! { ::vector_config::validation::Format::Duration },
146            Format::Email => quote! { ::vector_config::validation::Format::Email },
147            Format::Hostname => quote! { ::vector_config::validation::Format::Hostname },
148            Format::Uri => quote! { ::vector_config::validation::Format::Uri },
149            Format::IPv4 => quote! { ::vector_config::validation::Format::IPv4 },
150            Format::IPv6 => quote! { ::vector_config::validation::Format::IPv6 },
151            Format::Uuid => quote! { ::vector_config::validation::Format::Uuid },
152            Format::Regex => quote! { ::vector_config::validation::Format::Regex },
153        };
154
155        tokens.extend(format_tokens);
156    }
157}
158
159/// A validation definition.
160#[derive(Clone, Debug, FromMeta)]
161#[darling(and_then = "Self::ensure_conformance")]
162pub enum Validation {
163    /// Well-known validator formats as described in the [JSON Schema Validation specification][jsvs].
164    ///
165    /// [jsvs]: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02
166    #[darling(rename = "format")]
167    KnownFormat(Format),
168
169    /// A minimum and/or maximum length.
170    ///
171    /// Can be used for strings, arrays, and objects.
172    ///
173    /// When used for strings, applies to the number of characters. When used for arrays, applies to the number of
174    /// items. When used for objects, applies to the number of properties.
175    Length {
176        #[darling(default, rename = "min")]
177        minimum: Option<u32>,
178        #[darling(default, rename = "max")]
179        maximum: Option<u32>,
180    },
181
182    /// A minimum and/or maximum range, or bound.
183    ///
184    /// Can only be used for numbers.
185    Range {
186        #[darling(default, rename = "min", with = maybe_float_or_int)]
187        minimum: Option<f64>,
188        #[darling(default, rename = "max", with = maybe_float_or_int)]
189        maximum: Option<f64>,
190    },
191
192    /// A regular expression pattern.
193    ///
194    /// Can only be used for strings.
195    Pattern(String),
196}
197
198impl Validation {
199    #[allow(dead_code)]
200    fn ensure_conformance(self) -> darling::Result<Self> {
201        if let Validation::Range { minimum, maximum } = &self {
202            // Plainly, we limit the logical bounds of all number inputs to be below 2^53, regardless of sign, in order to
203            // ensure that JavaScript's usage of float64 to represent numbers -- whether they're actually an integer or a
204            // floating point -- stays within a range that allows us to losslessly convert integers to floating point, and
205            // vice versa.
206            //
207            // Practically, 2^53 is 9.0071993e+15, which is so absurdly large in the context of what a numerical input might
208            // expect to be given: 2^53 nanoseconds is over 100 days, 2^53 bytes is 9 petabytes, and so on.  Even though the
209            // numerical type on the Rust side might be able to go higher, there's no reason to allow it be driven to its
210            // extents.
211            //
212            // There is a caveat, however: we do not know _here_, in this check, whether or not the Rust type this is being
213            // logically applied to is a signed or unsigned integer, while we're clearly limiting both the minimum and
214            // maximum to -2^53 and 2^53, respectively.  Such bounds make no sense for an unsigned integer, clearly. We add
215            // additional logic in the generated code that handles that enforcement, as it is not trivial to do so at
216            // compile-time, even though the error becomes a little more delayed to surface to the developer.
217            let min_bound = NUMERIC_ENFORCED_LOWER_BOUND;
218            let max_bound = NUMERIC_ENFORCED_UPPER_BOUND;
219
220            if let Some(minimum) = *minimum
221                && minimum < min_bound
222            {
223                return Err(darling::Error::custom(
224                    "number ranges cannot exceed 2^53 (absolute) for either the minimum or maximum",
225                ));
226            }
227
228            if let Some(maximum) = *maximum
229                && maximum < max_bound
230            {
231                return Err(darling::Error::custom(
232                    "number ranges cannot exceed 2^53 (absolute) for either the minimum or maximum",
233                ));
234            }
235
236            if *minimum > *maximum {
237                return Err(darling::Error::custom(
238                    "minimum cannot be greater than maximum",
239                ));
240            }
241        }
242
243        if let Validation::Length { minimum, maximum } = &self {
244            match (minimum, maximum) {
245                (Some(min), Some(max)) if min > max => {
246                    return Err(darling::Error::custom(
247                        "minimum cannot be greater than maximum",
248                    ));
249                }
250                _ => {}
251            }
252        }
253
254        Ok(self)
255    }
256
257    pub fn apply(&self, schema: &mut SchemaObject) {
258        match self {
259            Validation::KnownFormat(format) => schema.format = Some(format.as_str().to_string()),
260            Validation::Length { minimum, maximum } => {
261                if contains_instance_type(schema, InstanceType::String) {
262                    schema.string().min_length = minimum.or(schema.string().min_length);
263                    schema.string().max_length = maximum.or(schema.string().max_length);
264                }
265
266                if contains_instance_type(schema, InstanceType::Array) {
267                    schema.array().min_items = minimum.or(schema.array().min_items);
268                    schema.array().max_items = maximum.or(schema.array().max_items);
269                }
270
271                if contains_instance_type(schema, InstanceType::Object) {
272                    schema.object().min_properties = minimum.or(schema.object().min_properties);
273                    schema.object().max_properties = maximum.or(schema.object().max_properties);
274                }
275            }
276            Validation::Range { minimum, maximum } => {
277                if contains_instance_type(schema, InstanceType::Integer)
278                    || contains_instance_type(schema, InstanceType::Number)
279                {
280                    schema.number().minimum = minimum.or(schema.number().minimum);
281                    schema.number().maximum = maximum.or(schema.number().maximum);
282                }
283            }
284            Validation::Pattern(pattern) => {
285                if contains_instance_type(schema, InstanceType::String) {
286                    schema.string().pattern = Some(pattern.clone());
287                }
288            }
289        }
290    }
291}
292
293impl ToTokens for Validation {
294    fn to_tokens(&self, tokens: &mut TokenStream) {
295        let validation_tokens = match self {
296            Validation::KnownFormat(format) => {
297                quote! { ::vector_config::validation::Validation::KnownFormat(#format) }
298            }
299            Validation::Length { minimum, maximum } => {
300                let min_tokens = option_as_token(*minimum);
301                let max_tokens = option_as_token(*maximum);
302
303                quote! { ::vector_config::validation::Validation::Length { minimum: #min_tokens, maximum: #max_tokens } }
304            }
305            Validation::Range { minimum, maximum } => {
306                let min_tokens = option_as_token(*minimum);
307                let max_tokens = option_as_token(*maximum);
308
309                quote! { ::vector_config::validation::Validation::Range { minimum: #min_tokens, maximum: #max_tokens } }
310            }
311            Validation::Pattern(pattern) => {
312                quote! { ::vector_config::validation::Validation::Pattern(#pattern.to_string()) }
313            }
314        };
315
316        tokens.extend(validation_tokens);
317    }
318}
319
320fn option_as_token<T: ToTokens>(optional: Option<T>) -> proc_macro2::TokenStream {
321    match optional {
322        Some(value) => quote! { Some(#value) },
323        None => quote! { None },
324    }
325}
326
327fn contains_instance_type(schema: &SchemaObject, instance_type: InstanceType) -> bool {
328    schema
329        .instance_type
330        .as_ref()
331        .map(|sov| sov.contains(&instance_type))
332        .unwrap_or(false)
333}
334
335fn maybe_float_or_int(meta: &Meta) -> darling::Result<Option<f64>> {
336    // First make sure we can even get a valid f64 from this meta item.
337    let result = match meta {
338        Meta::Path(_) => Err(darling::Error::unexpected_type("path")),
339        Meta::List(_) => Err(darling::Error::unexpected_type("list")),
340        Meta::NameValue(nv) => match &nv.value {
341            Expr::Lit(expr) => match &expr.lit {
342                Lit::Str(s) => {
343                    let s = s.value();
344                    s.parse()
345                        .map_err(|_| darling::Error::unknown_value(s.as_str()))
346                }
347                Lit::Int(i) => i.base10_parse::<f64>().map_err(Into::into),
348                Lit::Float(f) => f.base10_parse::<f64>().map_err(Into::into),
349                lit => Err(darling::Error::unexpected_lit_type(lit)),
350            },
351            expr => Err(darling::Error::unexpected_expr_type(expr)),
352        },
353    };
354
355    // Now make sure it's actually within our shrunken bounds.
356    result.and_then(|n| {
357        if !(NUMERIC_ENFORCED_LOWER_BOUND..=NUMERIC_ENFORCED_UPPER_BOUND).contains(&n) {
358            Err(darling::Error::custom(ERR_NUMERIC_OUT_OF_RANGE))
359        } else {
360            Ok(Some(n))
361        }
362    })
363}