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}