use core::fmt;
pub const MAX_PATH_DEPTH: usize = 16;
pub trait Validate {
fn validate(&self) -> Result<(), ValidationError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathSegment {
Field(&'static str),
Index(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintClass {
Property,
Occurrence,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ValidationErrorKind {
TooLong { len: usize, max: usize },
TooManyItems { len: usize, max: usize },
TooFewItems { len: usize, min: usize },
BelowMinimum { value: f64, min: f64 },
AboveMaximum { value: f64, max: f64 },
NotMultipleOf { value: f64, multiple: f64 },
}
impl ValidationErrorKind {
pub fn constraint_class(&self) -> ConstraintClass {
match self {
Self::TooManyItems { .. } | Self::TooFewItems { .. } => ConstraintClass::Occurrence,
Self::TooLong { .. }
| Self::BelowMinimum { .. }
| Self::AboveMaximum { .. }
| Self::NotMultipleOf { .. } => ConstraintClass::Property,
}
}
}
impl fmt::Display for ValidationErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooLong { len, max } => {
write!(f, "expected at most {max} characters, got {len}")
}
Self::TooManyItems { len, max } => {
write!(f, "expected at most {max} item{}, got {len}", plural(*max))
}
Self::TooFewItems { len, min } => {
write!(f, "expected at least {min} item{}, got {len}", plural(*min))
}
Self::BelowMinimum { value, min } => write!(f, "expected at least {min}, got {value}"),
Self::AboveMaximum { value, max } => write!(f, "expected at most {max}, got {value}"),
Self::NotMultipleOf { value, multiple } => {
write!(f, "expected a multiple of {multiple}, got {value}")
}
}
}
}
fn plural(count: usize) -> &'static str {
if count == 1 { "" } else { "s" }
}
#[derive(Debug, Clone, PartialEq)]
pub struct ValidationError {
path: heapless::Vec<PathSegment, MAX_PATH_DEPTH>,
truncated: bool,
kind: ValidationErrorKind,
}
impl ValidationError {
pub fn new(kind: ValidationErrorKind) -> Self {
Self {
path: heapless::Vec::new(),
truncated: false,
kind,
}
}
pub fn kind(&self) -> ValidationErrorKind {
self.kind
}
pub fn path(&self) -> &[PathSegment] {
&self.path
}
pub fn path_truncated(&self) -> bool {
self.truncated
}
pub fn in_field(self, name: &'static str) -> Self {
self.prepend(PathSegment::Field(name))
}
pub fn in_index(self, index: usize) -> Self {
self.prepend(PathSegment::Index(index))
}
fn prepend(mut self, segment: PathSegment) -> Self {
if self.path.insert(0, segment).is_err() {
self.truncated = true;
}
self
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.truncated {
f.write_str("...")?;
}
if self.path.is_empty() && !self.truncated {
f.write_str("<payload>")?;
}
for (position, segment) in self.path.iter().enumerate() {
match segment {
PathSegment::Field(name) => {
if position > 0 || self.truncated {
f.write_str(".")?;
}
f.write_str(name)?;
}
PathSegment::Index(index) => write!(f, "[{index}]")?,
}
}
write!(f, ": {}", self.kind)
}
}
impl core::error::Error for ValidationError {}
pub fn check_max_length(value: &str, max: usize) -> Result<(), ValidationError> {
let len = value.chars().count();
if len > max {
return Err(ValidationError::new(ValidationErrorKind::TooLong {
len,
max,
}));
}
Ok(())
}
pub fn check_min_items(len: usize, min: usize) -> Result<(), ValidationError> {
if len < min {
return Err(ValidationError::new(ValidationErrorKind::TooFewItems {
len,
min,
}));
}
Ok(())
}
pub fn check_max_items(len: usize, max: usize) -> Result<(), ValidationError> {
if len > max {
return Err(ValidationError::new(ValidationErrorKind::TooManyItems {
len,
max,
}));
}
Ok(())
}
pub fn check_min_i64(value: i64, min: i64) -> Result<(), ValidationError> {
if value < min {
return Err(ValidationError::new(ValidationErrorKind::BelowMinimum {
value: value as f64,
min: min as f64,
}));
}
Ok(())
}
pub fn check_max_i64(value: i64, max: i64) -> Result<(), ValidationError> {
if value > max {
return Err(ValidationError::new(ValidationErrorKind::AboveMaximum {
value: value as f64,
max: max as f64,
}));
}
Ok(())
}
pub fn check_min_f64(value: f64, min: f64) -> Result<(), ValidationError> {
if value < min {
return Err(ValidationError::new(ValidationErrorKind::BelowMinimum {
value,
min,
}));
}
Ok(())
}
pub fn check_max_f64(value: f64, max: f64) -> Result<(), ValidationError> {
if value > max {
return Err(ValidationError::new(ValidationErrorKind::AboveMaximum {
value,
max,
}));
}
Ok(())
}
const MULTIPLE_OF_TOLERANCE: f64 = 1e-9;
const MULTIPLE_OF_MAX_QUOTIENT: f64 = 9_007_199_254_740_992.0;
pub fn check_multiple_of(value: f64, multiple: f64) -> Result<(), ValidationError> {
if multiple == 0.0 {
return Ok(());
}
let quotient = value / multiple;
let magnitude = abs(quotient);
if quotient.is_nan() || magnitude > MULTIPLE_OF_MAX_QUOTIENT {
return Ok(());
}
let nearest = if quotient >= 0.0 {
(quotient + 0.5) as i64 as f64
} else {
(quotient - 0.5) as i64 as f64
};
let tolerance = MULTIPLE_OF_TOLERANCE * if magnitude > 1.0 { magnitude } else { 1.0 };
if abs(quotient - nearest) > tolerance {
return Err(ValidationError::new(ValidationErrorKind::NotMultipleOf {
value,
multiple,
}));
}
Ok(())
}
fn abs(value: f64) -> f64 {
if value < 0.0 { -value } else { value }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_string_within_its_limit_passes() {
assert!(check_max_length("abc", 3).is_ok());
}
#[test]
fn a_string_over_its_limit_reports_the_length_and_the_limit() {
let error = check_max_length("abcd", 3).unwrap_err();
assert_eq!(error.kind(), ValidationErrorKind::TooLong { len: 4, max: 3 });
}
#[test]
fn max_length_counts_characters_not_bytes() {
assert!(check_max_length("Ärger", 5).is_ok());
}
#[test]
fn an_empty_array_fails_a_min_items_of_one() {
let error = check_min_items(0, 1).unwrap_err();
assert_eq!(error.kind(), ValidationErrorKind::TooFewItems { len: 0, min: 1 });
}
#[test]
fn an_integer_below_its_minimum_is_rejected() {
let error = check_min_i64(-1, 0).unwrap_err();
assert_eq!(
error.kind(),
ValidationErrorKind::BelowMinimum { value: -1.0, min: 0.0 }
);
}
#[test]
fn a_tenth_is_a_multiple_of_one_tenth_despite_float_representation() {
assert!(check_multiple_of(16.1, 0.1).is_ok());
assert!(check_multiple_of(0.3, 0.1).is_ok());
assert!(check_multiple_of(-2.5, 0.1).is_ok());
}
#[test]
fn a_value_between_steps_is_not_a_multiple() {
let error = check_multiple_of(16.15, 0.1).unwrap_err();
assert_eq!(
error.kind(),
ValidationErrorKind::NotMultipleOf { value: 16.15, multiple: 0.1 }
);
}
#[test]
fn an_error_starts_with_an_empty_path() {
let error = check_max_length("abcd", 3).unwrap_err();
assert_eq!(error.path(), &[]);
}
#[test]
fn nesting_an_error_builds_the_path_from_the_inside_out() {
let error = check_min_items(0, 1)
.unwrap_err()
.in_field("chargingSchedulePeriod")
.in_index(0)
.in_field("chargingSchedule");
assert_eq!(
error.path(),
&[
PathSegment::Field("chargingSchedule"),
PathSegment::Index(0),
PathSegment::Field("chargingSchedulePeriod"),
]
);
}
#[test]
fn display_renders_the_path_in_json_terms() {
let error = check_min_items(0, 1)
.unwrap_err()
.in_field("chargingSchedulePeriod")
.in_index(0)
.in_field("chargingSchedule");
let mut rendered = heapless::String::<256>::new();
core::fmt::write(&mut rendered, format_args!("{error}")).unwrap();
assert_eq!(
rendered.as_str(),
"chargingSchedule[0].chargingSchedulePeriod: expected at least 1 item, got 0"
);
}
#[test]
fn display_of_a_root_level_error_says_so_rather_than_printing_an_empty_path() {
let error = check_max_length("abcd", 3).unwrap_err();
let mut rendered = heapless::String::<256>::new();
core::fmt::write(&mut rendered, format_args!("{error}")).unwrap();
assert_eq!(
rendered.as_str(),
"<payload>: expected at most 3 characters, got 4"
);
}
#[test]
fn a_path_deeper_than_the_buffer_is_marked_truncated_and_keeps_the_innermost_segments() {
let mut error = check_max_length("abcd", 3).unwrap_err();
for _ in 0..(MAX_PATH_DEPTH + 3) {
error = error.in_field("nested");
}
assert!(error.path_truncated());
assert_eq!(error.path().len(), MAX_PATH_DEPTH);
let mut rendered = heapless::String::<512>::new();
core::fmt::write(&mut rendered, format_args!("{error}")).unwrap();
assert!(rendered.starts_with("..."), "{rendered}");
}
#[test]
fn error_kinds_classify_as_the_ocpp_constraint_they_violate() {
assert_eq!(
ValidationErrorKind::TooLong { len: 4, max: 3 }.constraint_class(),
ConstraintClass::Property
);
assert_eq!(
ValidationErrorKind::TooFewItems { len: 0, min: 1 }.constraint_class(),
ConstraintClass::Occurrence
);
assert_eq!(
ValidationErrorKind::TooManyItems { len: 9, max: 8 }.constraint_class(),
ConstraintClass::Occurrence
);
}
#[test]
fn the_error_stays_the_size_its_path_capacity_implies() {
assert_eq!(core::mem::size_of::<PathSegment>(), 16);
assert_eq!(core::mem::size_of::<ValidationError>(), 296);
}
#[test]
fn validation_error_is_a_core_error() {
fn assert_error<E: core::error::Error>() {}
assert_error::<ValidationError>();
}
}