Skip to main content

codetics_ocpp_core/
validate.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)
3
4//! Schema constraint validation for generated message types.
5//!
6//! `serde` enforces structure (required fields, types, enum values, unknown
7//! fields). The generated `Validate` impls enforce the remaining JSON-schema
8//! constraints: `maxLength`, `minimum`/`maximum`, `minItems`/`maxItems` and
9//! `multipleOf`.
10
11use std::fmt;
12
13use crate::ErrorCode;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ViolationKind {
17    /// Not valid JSON / unknown property / structurally wrong.
18    Format,
19    /// Required field missing, or array cardinality violated.
20    Occurrence,
21    /// Value not allowed (enum, length, range).
22    Property,
23    /// Wrong JSON data type.
24    Type,
25}
26
27impl ViolationKind {
28    pub fn error_code(self) -> ErrorCode {
29        match self {
30            ViolationKind::Format => ErrorCode::FormatViolation,
31            ViolationKind::Occurrence => ErrorCode::OccurrenceConstraintViolation,
32            ViolationKind::Property => ErrorCode::PropertyConstraintViolation,
33            ViolationKind::Type => ErrorCode::TypeConstraintViolation,
34        }
35    }
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub struct Violation {
40    pub kind: ViolationKind,
41    pub message: String,
42}
43
44impl Violation {
45    pub fn new(kind: ViolationKind, message: impl Into<String>) -> Self {
46        Violation {
47            kind,
48            message: message.into(),
49        }
50    }
51
52    /// Classifies a serde_json error. The messages are produced by serde's
53    /// default `de::Error` constructors, whose wording is part of serde's API.
54    pub fn from_serde(e: serde_json::Error) -> Self {
55        use serde_json::error::Category;
56        let msg = e.to_string();
57        let kind = match e.classify() {
58            Category::Syntax | Category::Eof | Category::Io => ViolationKind::Format,
59            Category::Data => {
60                if msg.starts_with("missing field") {
61                    ViolationKind::Occurrence
62                } else if msg.starts_with("invalid type") {
63                    ViolationKind::Type
64                } else if msg.starts_with("unknown variant")
65                    || msg.starts_with("invalid value")
66                    || msg.starts_with("invalid length")
67                {
68                    ViolationKind::Property
69                } else {
70                    // unknown field, and anything else structural
71                    ViolationKind::Format
72                }
73            }
74        };
75        Violation { kind, message: msg }
76    }
77}
78
79impl fmt::Display for Violation {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        f.write_str(&self.message)
82    }
83}
84
85impl std::error::Error for Violation {}
86
87/// A JSON path built on the stack; only rendered when an error occurs.
88#[derive(Clone, Copy)]
89pub struct Path<'a> {
90    parent: Option<&'a Path<'a>>,
91    seg: Seg<'a>,
92}
93
94#[derive(Clone, Copy)]
95enum Seg<'a> {
96    Root,
97    Field(&'a str),
98    Index(usize),
99}
100
101impl<'a> Path<'a> {
102    pub const fn root() -> Path<'static> {
103        Path {
104            parent: None,
105            seg: Seg::Root,
106        }
107    }
108    pub fn field(&'a self, name: &'a str) -> Path<'a> {
109        Path {
110            parent: Some(self),
111            seg: Seg::Field(name),
112        }
113    }
114    pub fn index(&'a self, i: usize) -> Path<'a> {
115        Path {
116            parent: Some(self),
117            seg: Seg::Index(i),
118        }
119    }
120}
121
122impl fmt::Display for Path<'_> {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        if let Some(p) = self.parent {
125            fmt::Display::fmt(p, f)?;
126        }
127        match self.seg {
128            Seg::Root => f.write_str("$"),
129            Seg::Field(n) => write!(f, ".{n}"),
130            Seg::Index(i) => write!(f, "[{i}]"),
131        }
132    }
133}
134
135pub trait Validate {
136    fn validate(&self, path: &Path<'_>) -> Result<(), Violation>;
137}
138
139impl<T: Validate> Validate for Option<T> {
140    fn validate(&self, path: &Path<'_>) -> Result<(), Violation> {
141        match self {
142            Some(v) => v.validate(path),
143            None => Ok(()),
144        }
145    }
146}
147
148impl<T: Validate> Validate for Vec<T> {
149    fn validate(&self, path: &Path<'_>) -> Result<(), Violation> {
150        for (i, v) in self.iter().enumerate() {
151            v.validate(&path.index(i))?;
152        }
153        Ok(())
154    }
155}
156
157impl<T: Validate> Validate for Box<T> {
158    fn validate(&self, path: &Path<'_>) -> Result<(), Violation> {
159        (**self).validate(path)
160    }
161}
162
163macro_rules! no_constraints {
164    ($($t:ty),*) => { $(impl Validate for $t { #[inline] fn validate(&self, _: &Path<'_>) -> Result<(), Violation> { Ok(()) } })* };
165}
166no_constraints!(String, i64, f64, bool, crate::DateTime, serde_json::Value);
167
168// Helpers called by generated code.
169
170#[inline]
171pub fn max_length(v: &str, max: usize, path: &Path<'_>) -> Result<(), Violation> {
172    // JSON-schema maxLength counts characters (code points), not bytes.
173    if v.len() > max && v.chars().count() > max {
174        return Err(Violation::new(
175            ViolationKind::Property,
176            format!("{path}: string longer than {max} characters"),
177        ));
178    }
179    Ok(())
180}
181
182#[inline]
183pub fn minimum<T: PartialOrd + fmt::Display>(
184    v: T,
185    min: T,
186    path: &Path<'_>,
187) -> Result<(), Violation> {
188    if v < min {
189        return Err(Violation::new(
190            ViolationKind::Property,
191            format!("{path}: value {v} is below minimum {min}"),
192        ));
193    }
194    Ok(())
195}
196
197#[inline]
198pub fn maximum<T: PartialOrd + fmt::Display>(
199    v: T,
200    max: T,
201    path: &Path<'_>,
202) -> Result<(), Violation> {
203    if v > max {
204        return Err(Violation::new(
205            ViolationKind::Property,
206            format!("{path}: value {v} is above maximum {max}"),
207        ));
208    }
209    Ok(())
210}
211
212#[inline]
213pub fn items<T>(
214    v: &[T],
215    min: Option<usize>,
216    max: Option<usize>,
217    path: &Path<'_>,
218) -> Result<(), Violation> {
219    if let Some(min) = min
220        && v.len() < min
221    {
222        return Err(Violation::new(
223            ViolationKind::Occurrence,
224            format!("{path}: expected at least {min} items, got {}", v.len()),
225        ));
226    }
227    if let Some(max) = max
228        && v.len() > max
229    {
230        return Err(Violation::new(
231            ViolationKind::Occurrence,
232            format!("{path}: expected at most {max} items, got {}", v.len()),
233        ));
234    }
235    Ok(())
236}
237
238#[inline]
239pub fn multiple_of(v: f64, step: f64, path: &Path<'_>) -> Result<(), Violation> {
240    let q = v / step;
241    if (q - q.round()).abs() > 1e-6 {
242        return Err(Violation::new(
243            ViolationKind::Property,
244            format!("{path}: value {v} is not a multiple of {step}"),
245        ));
246    }
247    Ok(())
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn path_rendering() {
256        let r = Path::root();
257        let a = r.field("a");
258        let b = a.index(3);
259        let c = b.field("c");
260        assert_eq!(c.to_string(), "$.a[3].c");
261    }
262
263    #[test]
264    fn serde_classification() {
265        #[derive(serde::Deserialize, Debug)]
266        #[serde(deny_unknown_fields)]
267        #[allow(dead_code)]
268        struct T {
269            a: i64,
270            e: Option<E>,
271        }
272        #[derive(serde::Deserialize, Debug)]
273        enum E {
274            X,
275        }
276        let k = |s: &str| Violation::from_serde(serde_json::from_str::<T>(s).unwrap_err()).kind;
277        assert_eq!(k("{}"), ViolationKind::Occurrence);
278        assert_eq!(k(r#"{"a":"x"}"#), ViolationKind::Type);
279        assert_eq!(k(r#"{"a":1,"e":"Y"}"#), ViolationKind::Property);
280        assert_eq!(k(r#"{"a":1,"z":1}"#), ViolationKind::Format);
281        assert_eq!(k(r#"{"a":1"#), ViolationKind::Format);
282    }
283
284    #[test]
285    fn helpers() {
286        let p = Path::root();
287        assert!(max_length("ééé", 3, &p).is_ok());
288        assert!(max_length("éééé", 3, &p).is_err());
289        assert!(multiple_of(8.1, 0.1, &p).is_ok());
290        assert!(multiple_of(8.15, 0.1, &p).is_err());
291        assert!(items(&[1, 2], Some(1), Some(2), &p).is_ok());
292        assert!(items::<i32>(&[], Some(1), None, &p).is_err());
293    }
294}