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}