Skip to main content

ocpp_types/
validate.rs

1//! Checking a payload against the parts of the OCPP specification the
2//! types themselves cannot carry.
3//!
4//! Most spec limits are in the type: a field the schema bounds at
5//! `maxLength: 20` is a `heapless::String<20>`, so an over-long value is
6//! rejected at construction and never reaches the wire. Two categories
7//! escape that:
8//!
9//! * **Bounds too large to inline.** A string bounded above 512 characters,
10//!   or an array above 16 elements, would make every message that contains
11//!   it enormous by value, so it is not stored at its spec ceiling. Under
12//!   the `alloc` feature it becomes a plain `alloc::string::String` /
13//!   `alloc::vec::Vec`, which has no bound at all; without `alloc` it is a
14//!   `heapless` collection at a *caller-chosen* capacity, which may sit
15//!   either side of the spec's. Either way the specification's own limit is
16//!   no longer enforced by the type. `AuthorizeRequest::certificate` is one:
17//!   `maxLength: 5500` in the schema, a growable `String` in the `alloc`
18//!   build.
19//! * **Constraints no collection expresses.** `minItems` (usually `1`, i.e.
20//!   "must not be empty"), `minimum`/`maximum` on numbers, and 1.6J's
21//!   `multipleOf` on charging limits. A `heapless::Vec<T, N>` can say "at
22//!   most N"; it cannot say "at least one".
23//!
24//! [`Validate`] covers exactly those. It is implemented for every generated
25//! message and nested type, recurses through nested structs and arrays, and
26//! reports the first violation with the JSON path that reaches it:
27//!
28//! ```
29//! # #[cfg(feature = "alloc")] {
30//! use ocpp_types::v201::AuthorizeRequest;
31//! use ocpp_types::v201::common::{IdToken, IdTokenEnum};
32//! use ocpp_types::validate::{Validate, ValidationErrorKind};
33//!
34//! let request: AuthorizeRequest = AuthorizeRequest {
35//!     certificate: Some("-".repeat(6000)),
36//!     custom_data: None,
37//!     id_token: IdToken {
38//!         additional_info: None,
39//!         custom_data: None,
40//!         id_token: heapless::String::try_from("ABC123").unwrap(),
41//!         r#type: IdTokenEnum::ISO14443,
42//!     },
43//!     iso15118_certificate_hash_data: None,
44//! };
45//!
46//! let error = request.validate().unwrap_err();
47//! assert_eq!(
48//!     error.kind(),
49//!     ValidationErrorKind::TooLong { len: 6000, max: 5500 },
50//! );
51//! # }
52//! ```
53//!
54//! # What it does not check
55//!
56//! Only what the JSON schemas state. Cross-field rules from the
57//! specification's prose (a `TransactionEventRequest` with
58//! `eventType: Started` must carry a `triggerReason`, and so on) are out of
59//! scope, as is anything about the caller's own `CustomDataType` payload or
60//! 2.x's deliberately untyped `DataTransfer.data` -- the spec constrains
61//! neither, so neither is visited. Validating those is the caller's, and
62//! nothing stops a caller from implementing [`Validate`] for their own
63//! payload type.
64//!
65//! Validation is never automatic: nothing on the serialize path calls it,
66//! so a payload is checked exactly when you ask for it. Sending is the
67//! natural place --- an over-long field comes back from the peer as a
68//! `CALLERROR` you cannot correlate to a field, whereas
69//! [`ValidationError`] names it.
70
71use core::fmt;
72
73/// How many path segments a [`ValidationError`] carries before it starts
74/// truncating (see [`ValidationError::path_truncated`]).
75///
76/// The path is built as the error unwinds and is held inline, so this is
77/// most of [`ValidationError`]'s size -- 296 bytes, which
78/// `Result<(), ValidationError>` costs on the stack at every level of a
79/// recursive `validate`. It is sized for the longest path OCPP can produce
80/// rather than trimmed below that: array indices are segments too, so 2.1's
81/// `chargingProfile.chargingSchedule[i].salesTariff.salesTariffEntry[j].`
82/// `consumptionCost[k].cost[l].amount` is eleven of them, and truncating
83/// real paths would blunt the errors exactly where they are hardest to
84/// diagnose by hand.
85pub const MAX_PATH_DEPTH: usize = 16;
86
87/// A type that can be checked against the constraints its schema states.
88///
89/// Implemented for every generated message and nested type. The
90/// implementation checks this type's own fields and recurses into nested
91/// structs and arrays, so calling it on a message validates the whole
92/// payload.
93pub trait Validate {
94    /// `Ok(())` if every schema constraint reachable from `self` holds, or
95    /// the first violation found. Field order within a struct is the
96    /// schema's, but which violation comes first is not otherwise a
97    /// stable part of the API.
98    fn validate(&self) -> Result<(), ValidationError>;
99}
100
101/// One step of the JSON path to a failing value: an object key, or a
102/// position within an array.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum PathSegment {
105    /// A property name, as it appears on the wire (camelCase), not the
106    /// Rust field name.
107    Field(&'static str),
108    /// A zero-based index into an array.
109    Index(usize),
110}
111
112/// Which of OCPP's two constraint categories a violation falls into, and so
113/// which `CALLERROR` code answers it.
114///
115/// Each version names these slightly differently -- 1.6J's
116/// `OccurenceConstraintViolation` lost a letter that 2.x's
117/// `OccurrenceConstraintViolation` restored -- so this is the version-
118/// independent classification, and the caller picks the code from their
119/// version's `RpcErrorCode`.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum ConstraintClass {
122    /// A value breaks its own field's rule: too long, out of range, not on
123    /// the required step. Answered with `PropertyConstraintViolation`.
124    Property,
125    /// An array holds the wrong number of elements. Answered with
126    /// `Occur(r)enceConstraintViolation`.
127    Occurrence,
128}
129
130/// What was wrong with the value.
131///
132/// The numeric variants carry `f64` for uniformity across integer and
133/// number fields; comparison itself is done in the field's own type, so an
134/// integer bound is never decided in floating point.
135#[derive(Debug, Clone, Copy, PartialEq)]
136pub enum ValidationErrorKind {
137    /// A string longer than its schema's `maxLength`, counted in
138    /// characters.
139    TooLong { len: usize, max: usize },
140    /// An array longer than its schema's `maxItems`.
141    TooManyItems { len: usize, max: usize },
142    /// An array shorter than its schema's `minItems`.
143    TooFewItems { len: usize, min: usize },
144    /// A number below its schema's `minimum`.
145    BelowMinimum { value: f64, min: f64 },
146    /// A number above its schema's `maximum`.
147    AboveMaximum { value: f64, max: f64 },
148    /// A number that is not a whole multiple of its schema's `multipleOf`.
149    NotMultipleOf { value: f64, multiple: f64 },
150}
151
152impl ValidationErrorKind {
153    /// Which OCPP constraint category this violates, and so which
154    /// `CALLERROR` code answers it. See [`ConstraintClass`].
155    pub fn constraint_class(&self) -> ConstraintClass {
156        match self {
157            Self::TooManyItems { .. } | Self::TooFewItems { .. } => ConstraintClass::Occurrence,
158            Self::TooLong { .. }
159            | Self::BelowMinimum { .. }
160            | Self::AboveMaximum { .. }
161            | Self::NotMultipleOf { .. } => ConstraintClass::Property,
162        }
163    }
164}
165
166impl fmt::Display for ValidationErrorKind {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        match self {
169            Self::TooLong { len, max } => {
170                write!(f, "expected at most {max} characters, got {len}")
171            }
172            Self::TooManyItems { len, max } => {
173                write!(f, "expected at most {max} item{}, got {len}", plural(*max))
174            }
175            Self::TooFewItems { len, min } => {
176                write!(f, "expected at least {min} item{}, got {len}", plural(*min))
177            }
178            Self::BelowMinimum { value, min } => write!(f, "expected at least {min}, got {value}"),
179            Self::AboveMaximum { value, max } => write!(f, "expected at most {max}, got {value}"),
180            Self::NotMultipleOf { value, multiple } => {
181                write!(f, "expected a multiple of {multiple}, got {value}")
182            }
183        }
184    }
185}
186
187fn plural(count: usize) -> &'static str {
188    if count == 1 { "" } else { "s" }
189}
190
191/// A schema constraint that a payload broke, and the path to the value that
192/// broke it.
193///
194/// Built innermost-first: the check that failed creates it with an empty
195/// path, and each enclosing field or array index is prepended as the error
196/// unwinds, so [`Display`](fmt::Display) prints a JSON path a peer's error
197/// message can be matched against.
198#[derive(Debug, Clone, PartialEq)]
199pub struct ValidationError {
200    path: heapless::Vec<PathSegment, MAX_PATH_DEPTH>,
201    truncated: bool,
202    kind: ValidationErrorKind,
203}
204
205impl ValidationError {
206    /// A violation at the root, with no path yet. Prepend context with
207    /// [`in_field`](Self::in_field) / [`in_index`](Self::in_index).
208    pub fn new(kind: ValidationErrorKind) -> Self {
209        Self {
210            path: heapless::Vec::new(),
211            truncated: false,
212            kind,
213        }
214    }
215
216    /// What was wrong with the value.
217    pub fn kind(&self) -> ValidationErrorKind {
218        self.kind
219    }
220
221    /// The path from the validated payload's root to the failing value,
222    /// outermost segment first. Empty when the payload itself is what
223    /// failed.
224    pub fn path(&self) -> &[PathSegment] {
225        &self.path
226    }
227
228    /// Whether the path lost its outermost segments to
229    /// [`MAX_PATH_DEPTH`]. The innermost ones -- the part that says what
230    /// actually failed -- are always kept.
231    pub fn path_truncated(&self) -> bool {
232        self.truncated
233    }
234
235    /// Records that this violation was found inside property `name`
236    /// (its wire name, camelCase).
237    pub fn in_field(self, name: &'static str) -> Self {
238        self.prepend(PathSegment::Field(name))
239    }
240
241    /// Records that this violation was found at index `index` of the array
242    /// currently outermost in the path.
243    pub fn in_index(self, index: usize) -> Self {
244        self.prepend(PathSegment::Index(index))
245    }
246
247    /// Dropping the segment rather than the error is the only option that
248    /// keeps `validate` infallible in the presence of a pathological type
249    /// graph, and dropping the *outermost* one loses the least: the
250    /// segments nearest the failure are what identify it.
251    fn prepend(mut self, segment: PathSegment) -> Self {
252        if self.path.insert(0, segment).is_err() {
253            self.truncated = true;
254        }
255
256        self
257    }
258}
259
260impl fmt::Display for ValidationError {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        if self.truncated {
263            f.write_str("...")?;
264        }
265
266        if self.path.is_empty() && !self.truncated {
267            // An empty path means the payload itself failed -- printing
268            // nothing there would render as a bare `: expected ...`.
269            f.write_str("<payload>")?;
270        }
271
272        for (position, segment) in self.path.iter().enumerate() {
273            match segment {
274                PathSegment::Field(name) => {
275                    if position > 0 || self.truncated {
276                        f.write_str(".")?;
277                    }
278                    f.write_str(name)?;
279                }
280                PathSegment::Index(index) => write!(f, "[{index}]")?,
281            }
282        }
283
284        write!(f, ": {}", self.kind)
285    }
286}
287
288impl core::error::Error for ValidationError {}
289
290/// Rejects a string longer than `max` *characters*.
291///
292/// OCPP states `maxLength` in characters and Rust measures strings in
293/// bytes, so this counts `char`s: a field bounded at 20 accepts twenty
294/// non-ASCII characters, as the specification intends, even though they
295/// occupy more than twenty bytes.
296pub fn check_max_length(value: &str, max: usize) -> Result<(), ValidationError> {
297    let len = value.chars().count();
298
299    if len > max {
300        return Err(ValidationError::new(ValidationErrorKind::TooLong {
301            len,
302            max,
303        }));
304    }
305
306    Ok(())
307}
308
309/// Rejects an array shorter than its schema's `minItems`.
310pub fn check_min_items(len: usize, min: usize) -> Result<(), ValidationError> {
311    if len < min {
312        return Err(ValidationError::new(ValidationErrorKind::TooFewItems {
313            len,
314            min,
315        }));
316    }
317
318    Ok(())
319}
320
321/// Rejects an array longer than its schema's `maxItems`.
322pub fn check_max_items(len: usize, max: usize) -> Result<(), ValidationError> {
323    if len > max {
324        return Err(ValidationError::new(ValidationErrorKind::TooManyItems {
325            len,
326            max,
327        }));
328    }
329
330    Ok(())
331}
332
333/// Rejects an integer below its schema's `minimum`.
334pub fn check_min_i64(value: i64, min: i64) -> Result<(), ValidationError> {
335    if value < min {
336        return Err(ValidationError::new(ValidationErrorKind::BelowMinimum {
337            value: value as f64,
338            min: min as f64,
339        }));
340    }
341
342    Ok(())
343}
344
345/// Rejects an integer above its schema's `maximum`.
346pub fn check_max_i64(value: i64, max: i64) -> Result<(), ValidationError> {
347    if value > max {
348        return Err(ValidationError::new(ValidationErrorKind::AboveMaximum {
349            value: value as f64,
350            max: max as f64,
351        }));
352    }
353
354    Ok(())
355}
356
357/// Rejects a number below its schema's `minimum`.
358///
359/// A `NaN` compares false against every bound, so it passes here rather
360/// than being reported as out of range -- it is not a number the schema has
361/// anything to say about, and it cannot be serialized as JSON at all.
362pub fn check_min_f64(value: f64, min: f64) -> Result<(), ValidationError> {
363    if value < min {
364        return Err(ValidationError::new(ValidationErrorKind::BelowMinimum {
365            value,
366            min,
367        }));
368    }
369
370    Ok(())
371}
372
373/// Rejects a number above its schema's `maximum`. `NaN` passes, as in
374/// [`check_min_f64`].
375pub fn check_max_f64(value: f64, max: f64) -> Result<(), ValidationError> {
376    if value > max {
377        return Err(ValidationError::new(ValidationErrorKind::AboveMaximum {
378            value,
379            max,
380        }));
381    }
382
383    Ok(())
384}
385
386/// How far `value / multiple` may sit from a whole number and still count
387/// as a multiple, relative to the size of the quotient.
388///
389/// Every `multipleOf` in every OCPP version is 1.6J's `0.1` on charging
390/// limits, and a tenth has no exact binary representation: `16.1 / 0.1` is
391/// `160.99999999999997`, not `161`. An exact test would reject values that
392/// arrived from the wire as conformant tenths. This tolerance is far larger
393/// than the accumulated representation error and far smaller than the
394/// smallest gap the spec cares about (half a step, `0.05`), so it separates
395/// the two cleanly.
396const MULTIPLE_OF_TOLERANCE: f64 = 1e-9;
397
398/// The largest quotient this can decide. Beyond `2^53` consecutive integers
399/// are no longer distinguishable in an `f64`, so "is this a whole number"
400/// stops meaning anything; such a value is accepted rather than reported
401/// against a test that cannot be performed. No OCPP field comes close.
402const MULTIPLE_OF_MAX_QUOTIENT: f64 = 9_007_199_254_740_992.0;
403
404/// Rejects a number that is not a whole multiple of `multiple`, within
405/// [`MULTIPLE_OF_TOLERANCE`].
406pub fn check_multiple_of(value: f64, multiple: f64) -> Result<(), ValidationError> {
407    if multiple == 0.0 {
408        return Ok(());
409    }
410
411    let quotient = value / multiple;
412    let magnitude = abs(quotient);
413
414    // `NaN` is not a value the schema has anything to say about, and it
415    // cannot be serialized as JSON at all -- same reasoning as the
416    // `minimum`/`maximum` checks above.
417    if quotient.is_nan() || magnitude > MULTIPLE_OF_MAX_QUOTIENT {
418        return Ok(());
419    }
420
421    // `as i64` truncates toward zero, so nudging by half a step in the
422    // quotient's own direction rounds to nearest -- `round` itself is not
423    // available in `core`.
424    let nearest = if quotient >= 0.0 {
425        (quotient + 0.5) as i64 as f64
426    } else {
427        (quotient - 0.5) as i64 as f64
428    };
429
430    let tolerance = MULTIPLE_OF_TOLERANCE * if magnitude > 1.0 { magnitude } else { 1.0 };
431
432    if abs(quotient - nearest) > tolerance {
433        return Err(ValidationError::new(ValidationErrorKind::NotMultipleOf {
434            value,
435            multiple,
436        }));
437    }
438
439    Ok(())
440}
441
442/// `f64::abs` lives in `std`, and this crate is `no_std`.
443fn abs(value: f64) -> f64 {
444    if value < 0.0 { -value } else { value }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    #[test]
452    fn a_string_within_its_limit_passes() {
453        assert!(check_max_length("abc", 3).is_ok());
454    }
455
456    #[test]
457    fn a_string_over_its_limit_reports_the_length_and_the_limit() {
458        let error = check_max_length("abcd", 3).unwrap_err();
459
460        assert_eq!(error.kind(), ValidationErrorKind::TooLong { len: 4, max: 3 });
461    }
462
463    /// OCPP states `maxLength` in characters, not bytes -- a 20-character
464    /// name with an umlaut in it is 21 bytes and still conformant.
465    #[test]
466    fn max_length_counts_characters_not_bytes() {
467        assert!(check_max_length("Ärger", 5).is_ok());
468    }
469
470    #[test]
471    fn an_empty_array_fails_a_min_items_of_one() {
472        let error = check_min_items(0, 1).unwrap_err();
473
474        assert_eq!(error.kind(), ValidationErrorKind::TooFewItems { len: 0, min: 1 });
475    }
476
477    #[test]
478    fn an_integer_below_its_minimum_is_rejected() {
479        let error = check_min_i64(-1, 0).unwrap_err();
480
481        assert_eq!(
482            error.kind(),
483            ValidationErrorKind::BelowMinimum { value: -1.0, min: 0.0 }
484        );
485    }
486
487    /// 1.6J's charging limits are the only `multipleOf` in any version, and
488    /// they are all `0.1` -- which is not representable in binary floating
489    /// point, so an exact remainder test rejects values that came off the
490    /// wire as valid tenths.
491    #[test]
492    fn a_tenth_is_a_multiple_of_one_tenth_despite_float_representation() {
493        assert!(check_multiple_of(16.1, 0.1).is_ok());
494        assert!(check_multiple_of(0.3, 0.1).is_ok());
495        assert!(check_multiple_of(-2.5, 0.1).is_ok());
496    }
497
498    #[test]
499    fn a_value_between_steps_is_not_a_multiple() {
500        let error = check_multiple_of(16.15, 0.1).unwrap_err();
501
502        assert_eq!(
503            error.kind(),
504            ValidationErrorKind::NotMultipleOf { value: 16.15, multiple: 0.1 }
505        );
506    }
507
508    #[test]
509    fn an_error_starts_with_an_empty_path() {
510        let error = check_max_length("abcd", 3).unwrap_err();
511
512        assert_eq!(error.path(), &[]);
513    }
514
515    #[test]
516    fn nesting_an_error_builds_the_path_from_the_inside_out() {
517        let error = check_min_items(0, 1)
518            .unwrap_err()
519            .in_field("chargingSchedulePeriod")
520            .in_index(0)
521            .in_field("chargingSchedule");
522
523        assert_eq!(
524            error.path(),
525            &[
526                PathSegment::Field("chargingSchedule"),
527                PathSegment::Index(0),
528                PathSegment::Field("chargingSchedulePeriod"),
529            ]
530        );
531    }
532
533    #[test]
534    fn display_renders_the_path_in_json_terms() {
535        let error = check_min_items(0, 1)
536            .unwrap_err()
537            .in_field("chargingSchedulePeriod")
538            .in_index(0)
539            .in_field("chargingSchedule");
540
541        let mut rendered = heapless::String::<256>::new();
542        core::fmt::write(&mut rendered, format_args!("{error}")).unwrap();
543
544        assert_eq!(
545            rendered.as_str(),
546            "chargingSchedule[0].chargingSchedulePeriod: expected at least 1 item, got 0"
547        );
548    }
549
550    #[test]
551    fn display_of_a_root_level_error_says_so_rather_than_printing_an_empty_path() {
552        let error = check_max_length("abcd", 3).unwrap_err();
553
554        let mut rendered = heapless::String::<256>::new();
555        core::fmt::write(&mut rendered, format_args!("{error}")).unwrap();
556
557        assert_eq!(
558            rendered.as_str(),
559            "<payload>: expected at most 3 characters, got 4"
560        );
561    }
562
563    /// The path is a fixed-capacity buffer, so a pathologically deep type
564    /// graph has to degrade rather than panic or silently drop the *inner*
565    /// segments that say what actually failed.
566    #[test]
567    fn a_path_deeper_than_the_buffer_is_marked_truncated_and_keeps_the_innermost_segments() {
568        let mut error = check_max_length("abcd", 3).unwrap_err();
569        for _ in 0..(MAX_PATH_DEPTH + 3) {
570            error = error.in_field("nested");
571        }
572
573        assert!(error.path_truncated());
574        assert_eq!(error.path().len(), MAX_PATH_DEPTH);
575
576        let mut rendered = heapless::String::<512>::new();
577        core::fmt::write(&mut rendered, format_args!("{error}")).unwrap();
578        assert!(rendered.starts_with("..."), "{rendered}");
579    }
580
581    /// A CSMS answering a bad payload needs the OCPP error code that goes
582    /// with it, and the spec splits these two ways: a value that breaks a
583    /// field's own rule is a property constraint, a wrong *number* of
584    /// elements is an occurrence constraint.
585    #[test]
586    fn error_kinds_classify_as_the_ocpp_constraint_they_violate() {
587        assert_eq!(
588            ValidationErrorKind::TooLong { len: 4, max: 3 }.constraint_class(),
589            ConstraintClass::Property
590        );
591        assert_eq!(
592            ValidationErrorKind::TooFewItems { len: 0, min: 1 }.constraint_class(),
593            ConstraintClass::Occurrence
594        );
595        assert_eq!(
596            ValidationErrorKind::TooManyItems { len: 9, max: 8 }.constraint_class(),
597            ConstraintClass::Occurrence
598        );
599    }
600
601    /// `ValidationError` is returned by value through every level of a
602    /// recursive `validate`, so its size is a real cost on a target
603    /// counting stack bytes -- and one that would otherwise grow silently
604    /// as variants are added. See [`MAX_PATH_DEPTH`] for why the path is
605    /// sized the way it is.
606    #[test]
607    fn the_error_stays_the_size_its_path_capacity_implies() {
608        assert_eq!(core::mem::size_of::<PathSegment>(), 16);
609        assert_eq!(core::mem::size_of::<ValidationError>(), 296);
610    }
611
612    #[test]
613    fn validation_error_is_a_core_error() {
614        fn assert_error<E: core::error::Error>() {}
615
616        assert_error::<ValidationError>();
617    }
618}