Skip to main content

ocpi_kit/types/
validate.rs

1//! Explicit validation: parse permissively, validate deliberately, construct strictly.
2//!
3//! # Why validation is a separate pass
4//!
5//! OCPI 2.3.0 requires that *"An OCPI Platform SHALL NOT reject request or response payloads
6//! based on the presence of JSON object field names that are not documented in this
7//! specification"* (`transport_and_format` — Non-specified JSON fields). In the same spirit,
8//! and because a hub that drops a peer's data is worse than a hub that forwards slightly
9//! non-conformant data, `ocpi-kit` never fails a deserialisation because a `string(45)` field
10//! turned out to be 46 characters long.
11//!
12//! Instead the crate follows one rule throughout:
13//!
14//! > **Parse permissively, validate explicitly, construct strictly.**
15//!
16//! * `Deserialize` accepts what the peer sent, as long as it is well-typed JSON.
17//! * [`Validate::validate`] reports *every* violation, each with a JSON Pointer
18//!   ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) into the object, so a
19//!   conformance report can point at the exact field.
20//! * The constructors ([`CiString::new`](crate::types::CiString::new),
21//!   [`OcpiString::new`](crate::types::OcpiString::new), …) refuse to build a value that is
22//!   already out of spec, so data *this* crate emits is conformant by construction.
23//!
24//! Spec: 2.3.0 §transport_and_format_json_http_implementation_guide
25
26use core::fmt;
27
28/// A single spec violation found by [`Validate::validate`].
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct Violation {
31    /// JSON Pointer (RFC 6901) to the offending value, relative to the validated object.
32    ///
33    /// The empty string refers to the validated object itself.
34    pub pointer: String,
35    /// Machine-readable classification of the violation.
36    pub code: ViolationCode,
37    /// Human-readable explanation, including the spec rule that was broken.
38    pub message: String,
39}
40
41impl fmt::Display for Violation {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        let at = if self.pointer.is_empty() { "/" } else { &self.pointer };
44        write!(f, "{at}: {}", self.message)
45    }
46}
47
48/// Classification of a [`Violation`].
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50#[non_exhaustive]
51pub enum ViolationCode {
52    /// A string exceeded the maximum length given in the spec's property table.
53    TooLong,
54    /// A string contained characters the spec forbids (non-printable, or non-ASCII in a
55    /// `CiString`).
56    IllegalCharacter,
57    /// A list that the spec marks with cardinality `+` (one or more) was empty.
58    EmptyRequiredList,
59    /// A value was outside the range the spec allows.
60    OutOfRange,
61    /// A field was syntactically fine but violates a cross-field rule of the spec.
62    Inconsistent,
63    /// A conditionally required field was missing.
64    MissingConditional,
65    /// The value cannot survive a JSON round-trip without loss of precision.
66    Imprecise,
67}
68
69impl ViolationCode {
70    /// A short, stable, machine-readable slug for this code.
71    #[must_use]
72    pub const fn as_str(self) -> &'static str {
73        match self {
74            Self::TooLong => "too_long",
75            Self::IllegalCharacter => "illegal_character",
76            Self::EmptyRequiredList => "empty_required_list",
77            Self::OutOfRange => "out_of_range",
78            Self::Inconsistent => "inconsistent",
79            Self::MissingConditional => "missing_conditional",
80            Self::Imprecise => "imprecise",
81        }
82    }
83}
84
85/// Every [`Violation`] found in one object, in document order.
86#[derive(Clone, Debug, PartialEq, Eq, Default)]
87pub struct Violations(Vec<Violation>);
88
89impl Violations {
90    /// The violations, in document order.
91    #[must_use]
92    pub fn as_slice(&self) -> &[Violation] {
93        &self.0
94    }
95
96    /// Whether no violation was found.
97    #[must_use]
98    pub fn is_empty(&self) -> bool {
99        self.0.is_empty()
100    }
101
102    /// How many violations were found.
103    #[must_use]
104    pub fn len(&self) -> usize {
105        self.0.len()
106    }
107
108    /// Consumes this set and yields the violations.
109    #[must_use]
110    pub fn into_vec(self) -> Vec<Violation> {
111        self.0
112    }
113
114    /// The violations, in document order.
115    pub fn iter(&self) -> core::slice::Iter<'_, Violation> {
116        self.0.iter()
117    }
118}
119
120impl IntoIterator for Violations {
121    type Item = Violation;
122    type IntoIter = std::vec::IntoIter<Violation>;
123    fn into_iter(self) -> Self::IntoIter {
124        self.0.into_iter()
125    }
126}
127
128impl<'a> IntoIterator for &'a Violations {
129    type Item = &'a Violation;
130    type IntoIter = core::slice::Iter<'a, Violation>;
131    fn into_iter(self) -> Self::IntoIter {
132        self.iter()
133    }
134}
135
136impl fmt::Display for Violations {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        for (i, v) in self.0.iter().enumerate() {
139            if i > 0 {
140                f.write_str("; ")?;
141            }
142            write!(f, "{v}")?;
143        }
144        Ok(())
145    }
146}
147
148impl std::error::Error for Violations {}
149
150/// Accumulates violations while walking an object graph, tracking the current JSON Pointer.
151#[derive(Debug, Default)]
152pub struct Validator {
153    path: String,
154    found: Vec<Violation>,
155}
156
157impl Validator {
158    /// A validator positioned at the root of an object.
159    #[must_use]
160    pub fn new() -> Self {
161        Self::default()
162    }
163
164    /// Records a violation at the current position.
165    pub fn report(&mut self, code: ViolationCode, message: impl Into<String>) {
166        self.found.push(Violation { pointer: self.path.clone(), code, message: message.into() });
167    }
168
169    /// Records a violation at `field` below the current position.
170    pub fn report_at(&mut self, field: &str, code: ViolationCode, message: impl Into<String>) {
171        self.enter(field);
172        self.report(code, message);
173        self.leave();
174    }
175
176    /// Descends into a named field or array index, escaping it per RFC 6901.
177    pub fn enter(&mut self, segment: &str) {
178        self.path.push('/');
179        for ch in segment.chars() {
180            match ch {
181                '~' => self.path.push_str("~0"),
182                '/' => self.path.push_str("~1"),
183                c => self.path.push(c),
184            }
185        }
186    }
187
188    /// Returns to the parent of the current position.
189    ///
190    /// # Panics
191    ///
192    /// Panics if called more often than [`Validator::enter`].
193    pub fn leave(&mut self) {
194        let cut = self.path.rfind('/').expect("leave() without a matching enter()");
195        self.path.truncate(cut);
196    }
197
198    /// Validates `value` at `segment` below the current position.
199    pub fn field(&mut self, segment: &str, value: &impl Validate) {
200        self.enter(segment);
201        value.validate_in(self);
202        self.leave();
203    }
204
205    /// The JSON Pointer of the current position.
206    #[must_use]
207    pub fn pointer(&self) -> &str {
208        &self.path
209    }
210
211    /// Consumes the validator and yields everything it found.
212    #[must_use]
213    pub fn finish(self) -> Violations {
214        Violations(self.found)
215    }
216}
217
218/// Checks an OCPI value against the constraints in the specification's property tables.
219///
220/// See the [module documentation](self) for why this is not part of `Deserialize`.
221pub trait Validate {
222    /// Appends this value's violations to `v`, relative to `v`'s current position.
223    fn validate_in(&self, v: &mut Validator);
224
225    /// Validates this value as the root of an object graph.
226    ///
227    /// # Errors
228    ///
229    /// Returns every violation found, in document order.
230    fn validate(&self) -> Result<(), Violations> {
231        let mut v = Validator::new();
232        self.validate_in(&mut v);
233        let found = v.finish();
234        if found.is_empty() { Ok(()) } else { Err(found) }
235    }
236}
237
238impl<T: Validate> Validate for Option<T> {
239    fn validate_in(&self, v: &mut Validator) {
240        if let Some(inner) = self {
241            inner.validate_in(v);
242        }
243    }
244}
245
246impl<T: Validate> Validate for Vec<T> {
247    fn validate_in(&self, v: &mut Validator) {
248        for (i, item) in self.iter().enumerate() {
249            v.enter(&i.to_string());
250            item.validate_in(v);
251            v.leave();
252        }
253    }
254}
255
256impl<T: Validate> Validate for Box<T> {
257    fn validate_in(&self, v: &mut Validator) {
258        T::validate_in(self, v);
259    }
260}
261
262/// Implements [`Validate`] as a no-op for types that carry no spec constraints.
263macro_rules! impl_validate_noop {
264    ($($t:ty),* $(,)?) => {
265        $(impl Validate for $t {
266            fn validate_in(&self, _v: &mut Validator) {}
267        })*
268    };
269}
270
271impl_validate_noop!(bool, i8, i16, i32, i64, u8, u16, u32, u64, usize, String, serde_json::Value);
272
273/// Validates each named field of `$self` in turn.
274///
275/// Expands to a [`Validator::field`] call per field, using the Rust field name as the JSON
276/// Pointer segment, so the pointers a violation reports are pointers into the JSON the peer
277/// actually sent. Where a field is `#[serde(rename)]`d — `type` is a Rust keyword, so
278/// `image_type` carries it — give the wire name with `field as "type"`.
279macro_rules! validate_fields {
280    ($self:ident, $v:ident, $($field:ident $(as $wire:literal)?),* $(,)?) => {
281        $( $v.field(validate_fields!(@wire $field $(, $wire)?), &$self.$field); )*
282    };
283    (@wire $field:ident) => { stringify!($field) };
284    (@wire $field:ident, $wire:literal) => { $wire };
285}
286
287pub(crate) use validate_fields;
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    struct Leaf(bool);
294    impl Validate for Leaf {
295        fn validate_in(&self, v: &mut Validator) {
296            if !self.0 {
297                v.report(ViolationCode::OutOfRange, "leaf is false");
298            }
299        }
300    }
301
302    #[test]
303    fn pointer_tracks_nesting_and_escapes_rfc6901() {
304        let mut v = Validator::new();
305        v.enter("a/b");
306        v.enter("c~d");
307        v.report(ViolationCode::TooLong, "boom");
308        v.leave();
309        v.leave();
310        let found = v.finish();
311        assert_eq!(found.as_slice()[0].pointer, "/a~1b/c~0d");
312    }
313
314    #[test]
315    fn vec_and_option_are_walked_with_indices() {
316        let value = vec![Leaf(true), Leaf(false), Leaf(false)];
317        let err = value.validate().unwrap_err();
318        assert_eq!(err.len(), 2);
319        assert_eq!(err.as_slice()[0].pointer, "/1");
320        assert_eq!(err.as_slice()[1].pointer, "/2");
321        assert!(Some(Leaf(true)).validate().is_ok());
322        assert!(Option::<Leaf>::None.validate().is_ok());
323    }
324}