Skip to main content

apollo_configuration/
validate.rs

1//! Implementation for custom validation rules in configuration.
2
3use apollo_redaction::Redacted;
4
5use crate::ValidationErrors;
6use crate::YamlLocationData;
7use crate::errors::ValidationError;
8use jsonschema::paths::Location;
9use jsonschema::paths::LocationSegment;
10use miette::Diagnostic;
11use std::fmt::Display;
12use std::sync::Arc;
13
14/// Error collector for custom validation rules.
15///
16/// When implementing your own validation rule for an object or list of elements, use the
17/// [`ErrorCollector::nest`] method to track the current location for error reporting.
18// XXX(@goto-bus-stop): an alternative API could not use the `.nest(property)` style, but have
19// several methods that take JSON pointers:
20//  - `report_at(JSONPointer, Diagnostic)` (and `report(Diagnostic)`)
21//  - `span_at(JSONPointer)` (and `span()`)
22//  - `at(JSONPointer)`
23// This may be nicer to use, but I think we can start with this more minimal, more manual approach.
24pub struct ErrorCollector<'e> {
25    location_data: &'e YamlLocationData,
26    errors: &'e mut Vec<Box<dyn Diagnostic + Send + Sync + 'static>>,
27    path: Location,
28}
29
30impl<'e> ErrorCollector<'e> {
31    pub(crate) fn new(
32        location_data: &'e YamlLocationData,
33        errors: &'e mut Vec<Box<dyn Diagnostic + Send + Sync + 'static>>,
34    ) -> Self {
35        Self {
36            location_data,
37            errors,
38            path: Location::new(),
39        }
40    }
41
42    /// Produce a validator for an "inner" type. This means a type that's nested in Rust-land, but
43    /// not in the source YAML.
44    pub fn inner(&mut self) -> ErrorCollector<'_> {
45        ErrorCollector {
46            location_data: self.location_data,
47            errors: self.errors,
48            path: self.path.clone(),
49        }
50    }
51
52    /// Produce a validator for a nested object property or array index.
53    pub fn nest<'s, 'a>(
54        &'s mut self,
55        segment: impl Into<LocationSegment<'a>>,
56    ) -> ErrorCollector<'s> {
57        ErrorCollector {
58            location_data: self.location_data,
59            errors: self.errors,
60            path: self.path.join(segment),
61        }
62    }
63
64    /// Return the current location.
65    pub fn span(&self) -> miette::SourceSpan {
66        self.location_data
67            .resolve_instance_span(&self.path)
68            .unwrap_or_else(|| miette::SourceSpan::new(0.into(), 0))
69    }
70
71    /// Report a validation error at the current location.
72    ///
73    /// Any [`miette::Diagnostic`] can be used as the error value, but the recommendation for
74    /// Apollo products is to use [`apollo_errors`], for its error code support.
75    pub fn report(&mut self, diag: impl Diagnostic + Send + Sync + 'static) {
76        self.errors.push(Box::new(diag));
77    }
78
79    /// Report a validation error at the current location. This is intended for one-off, simple,
80    /// single-field errors pointing at the current value.
81    ///
82    /// We generally recommend you use [`apollo_errors`] for errors, even configuration validation
83    /// errors, for example for its error codes.
84    pub fn report_simple(&mut self, message: impl Display) {
85        self.report(ValidationError {
86            label: self.span(),
87            message: message.to_string(),
88        });
89    }
90
91    /// Returns the number of errors already reported.
92    ///
93    /// This concerns *all* errors, not just errors generated in the current call to
94    /// [`Validate::validate`] or its descendents. For example, when validating the second item in
95    /// a list, `.len()` will include validation errors from the first item in the list.
96    pub fn len(&self) -> usize {
97        self.errors.len()
98    }
99
100    /// Returns true if any errors have been reported.
101    pub fn is_empty(&self) -> bool {
102        self.errors.is_empty()
103    }
104}
105
106/// Validation trait for configuration values.
107///
108/// Configuration is primarily validated with JSON Schema. But not all invalid configurations can
109/// be prevented that way -- for example, encoding a constraint across different members of an
110/// object can be difficult or impossible. The `Validate` trait supports running additional
111/// validations on top of JSON Schema.
112///
113/// Most of the time, you should not use the `Validate` trait directly, but instead use
114/// `#[config(validate)]` attributes. You can implement the `Validate` trait to use a custom type
115/// as a configuration value.
116///
117/// # Lifecycle
118///
119/// Custom validation works on the actual Rust types. This means that any error that occurs
120/// _before_ values are deserialized into Rust types short-circuits validation. Custom validation
121/// can only enforce _additional constraints_ on top of the other steps.
122///
123/// One way this can manifest is that a user might get a single JSON schema validation error for a
124/// field, but once they fix it, they might start getting an unrelated custom validation error for
125/// a different field. This is similar to how fixing a function signature error in your Rust code
126/// can make your code reach the "next stage" of compilation, so after fixing the one error you
127/// might get dozens of new borrow-checker errors.
128pub trait Validate {
129    /// The default implementation of `validate` accepts all inputs.
130    fn validate<'a>(&self, _errors: ErrorCollector<'a>) {}
131}
132
133// Implement a wildcard `Validate` for a bunch of types, accepting any input as valid.
134impl Validate for bool {}
135impl Validate for i8 {}
136impl Validate for u8 {}
137impl Validate for i16 {}
138impl Validate for u16 {}
139impl Validate for i32 {}
140impl Validate for u32 {}
141impl Validate for i64 {}
142impl Validate for u64 {}
143impl Validate for isize {}
144impl Validate for usize {}
145impl Validate for std::num::NonZeroI8 {}
146impl Validate for std::num::NonZeroU8 {}
147impl Validate for std::num::NonZeroI16 {}
148impl Validate for std::num::NonZeroU16 {}
149impl Validate for std::num::NonZeroI32 {}
150impl Validate for std::num::NonZeroU32 {}
151impl Validate for std::num::NonZeroI64 {}
152impl Validate for std::num::NonZeroU64 {}
153impl Validate for std::num::NonZeroIsize {}
154impl Validate for std::num::NonZeroUsize {}
155impl Validate for str {}
156impl Validate for String {}
157impl Validate for () {}
158
159// Blanket impl for Redacted<T> - delegates validation to the inner type.
160// This allows any type that implements Validate to be wrapped in Redacted
161// without needing a separate impl.
162impl<T: Validate, R> Validate for Redacted<T, R> {
163    fn validate<'a>(&self, errors: ErrorCollector<'a>) {
164        self.unredact().validate(errors);
165    }
166}
167
168/// Validate a programmatically created value.
169///
170/// This is intended for use in testing custom validation rules, so you can
171/// create a Rust value and just run the validator.
172///
173/// In the real world, configurations should be deserialized with [`crate::parse_yaml`], and
174/// validation will be done by the apollo-configuration crate.
175///
176/// We also recommend using YAML configurations in tests as much as possible, to make sure
177/// that you're testing configurations that your users can actually write.
178pub fn validate<T: Validate>(value: T) -> Result<T, ValidationErrors> {
179    let location_data = YamlLocationData::empty();
180
181    let mut errors = vec![];
182    value.validate(ErrorCollector::new(&location_data, &mut errors));
183
184    if errors.is_empty() {
185        Ok(value)
186    } else {
187        Err(ValidationErrors {
188            source_code: Arc::from(""),
189            errors,
190        })
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::ErrorCollector;
197    use super::Validate;
198    use super::validate;
199
200    #[test]
201    fn programmatic_validation() {
202        validate("".to_string()).expect("should pass for a simple value");
203        validate(0).expect("should pass for a primitive value");
204
205        #[derive(Debug)]
206        struct Config {
207            #[expect(unused)]
208            a: usize,
209            #[expect(unused)]
210            b: usize,
211        }
212        impl Validate for Config {
213            fn validate(&self, mut errors: ErrorCollector<'_>) {
214                errors.nest("a").report_simple("always wrong");
215                errors.nest("b").report_simple("never correct");
216            }
217        }
218
219        let err = validate(Config { a: 0, b: 1 }).expect_err("should return errors");
220        assert_eq!(err.errors.len(), 2);
221    }
222}