pub mod v211;
pub mod v221;
mod build;
mod leaf;
#[cfg(test)]
mod tests;
use std::collections::BTreeSet;
use crate::{
json,
warning::{self, IntoCaveat as _},
Caveat,
};
#[derive(Clone, Copy)]
enum Schema {
Scalar(Scalar),
Object(&'static Object),
Array {
item: &'static Schema,
cardinality: Cardinality,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cardinality {
ZeroOrMore,
OneOrMore,
}
impl std::fmt::Display for Cardinality {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Cardinality::ZeroOrMore => f.write_str("zero or more"),
Cardinality::OneOrMore => f.write_str("one or more"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Scalar {
String,
StringMax(usize),
Enum(&'static [&'static str]),
Number,
Boolean,
Any,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Integrity<T> {
Ok(T),
Missing,
Err,
}
#[allow(
clippy::derivable_impls,
reason = "derive would add an unwanted T: Default bound"
)]
impl<T> Default for Integrity<T> {
fn default() -> Self {
Self::Missing
}
}
impl<T> Integrity<T> {
pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Integrity<U> {
match self {
Integrity::Ok(value) => Integrity::Ok(op(value)),
Integrity::Missing => Integrity::Missing,
Integrity::Err => Integrity::Err,
}
}
pub fn as_ref(&self) -> Integrity<&T> {
match self {
Integrity::Ok(value) => Integrity::Ok(value),
Integrity::Missing => Integrity::Missing,
Integrity::Err => Integrity::Err,
}
}
pub fn ok(self) -> Option<T> {
match self {
Integrity::Ok(value) => Some(value),
Integrity::Missing | Integrity::Err => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum BuilderKind {
Ignore,
V221Tariff,
V221Element,
V221PriceComponent,
V221Restrictions,
V221Price,
V221Cdr,
V221ChargingPeriod,
V221CdrDimension,
V211Tariff,
V211Element,
V211PriceComponent,
V211Restrictions,
V211Cdr,
V211ChargingPeriod,
V211CdrDimension,
}
#[derive(Clone, Copy)]
struct Object {
fields: &'static [Field],
kind: BuilderKind,
}
#[derive(Clone, Copy)]
struct Field {
name: &'static str,
presence: Presence,
schema: Schema,
}
impl Field {
const fn required(name: &'static str, scalar: Scalar) -> Self {
Self {
name,
presence: Presence::Required,
schema: Schema::Scalar(scalar),
}
}
const fn required_array(name: &'static str, item: &'static Schema) -> Self {
Self {
name,
presence: Presence::Required,
schema: Schema::Array {
item,
cardinality: Cardinality::OneOrMore,
},
}
}
const fn required_object(name: &'static str, schema: &'static Object) -> Self {
Self {
name,
presence: Presence::Required,
schema: Schema::Object(schema),
}
}
const fn optional(name: &'static str, scalar: Scalar) -> Self {
Self {
name,
presence: Presence::Optional,
schema: Schema::Scalar(scalar),
}
}
const fn optional_array(name: &'static str, item: &'static Schema) -> Self {
Self {
name,
presence: Presence::Optional,
schema: Schema::Array {
item,
cardinality: Cardinality::ZeroOrMore,
},
}
}
const fn optional_object(name: &'static str, schema: &'static Object) -> Self {
Self {
name,
presence: Presence::Optional,
schema: Schema::Object(schema),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Presence {
Required,
Optional,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Warning {
UnexpectedField,
MissingField {
name: &'static str,
},
NullField,
TypeMismatch {
expected: json::ValueKind,
actual: json::ValueKind,
},
StringTooLong {
max: usize,
len: usize,
},
FieldInvalidValue {
expected: &'static [&'static str],
actual: String,
},
Cardinality {
expected: Cardinality,
len: usize,
},
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::UnexpectedField => warning::Id::from_static("unexpected_field"),
Self::MissingField { name } => {
warning::Id::from_string(format!("missing_field({name})"))
}
Self::NullField => warning::Id::from_static("null_field"),
Self::TypeMismatch { actual, .. } => {
warning::Id::from_string(format!("invalid_type({actual})"))
}
Self::StringTooLong { .. } => warning::Id::from_static("string_too_long"),
Self::FieldInvalidValue { actual, .. } => {
warning::Id::from_string(format!("field_invalid_value({actual})"))
}
Self::Cardinality { expected, .. } => {
warning::Id::from_string(format!("cardinality({expected})"))
}
}
}
}
impl std::fmt::Display for Warning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnexpectedField => f.write_str("field is not part of the schema"),
Self::MissingField { name } => write!(f, "required field `{name}` is missing"),
Self::NullField => f.write_str(
"field is `null`. `null` fields have no semantic meaning for OCPI objects",
),
Self::TypeMismatch { expected, actual } => {
write!(f, "expected {expected} found {actual}")
}
Self::StringTooLong { max, len } => {
write!(
f,
"string is `{len}` characters, but the maximum allowed is `{max}`"
)
}
Self::FieldInvalidValue { expected, actual } => {
write!(
f,
"value `{actual}` is not one of the permitted values: {}",
expected.join(", ")
)
}
Self::Cardinality { expected, len } => {
write!(f, "expected {expected} elements, found {len}")
}
}
}
}
impl warning::Set<Warning> {
pub fn unexpected_fields(&self) -> json::PathSet<'_> {
let mut paths = BTreeSet::new();
for group in self {
let (element, group_warnings) = group.to_parts();
let has_unexpected_field = group_warnings
.iter()
.any(|warning| matches!(warning, Warning::UnexpectedField));
if has_unexpected_field {
paths.insert(&element.path);
}
}
json::PathSet::new(paths)
}
pub fn missing_fields(&self) -> json::PathSet<'_> {
let mut paths = BTreeSet::new();
for group in self {
let (element, group_warnings) = group.to_parts();
let has_missing_field = group_warnings
.iter()
.any(|warning| matches!(warning, Warning::MissingField { .. }));
if has_missing_field {
paths.insert(&element.path);
}
}
json::PathSet::new(paths)
}
pub fn remove_unexpected_fields(&mut self) {
self.retain(|warning| !matches!(warning, Warning::UnexpectedField));
}
pub fn remove_missing_fields(&mut self) {
self.retain(|warning| !matches!(warning, Warning::MissingField { .. }));
}
pub fn remove_type_mismatches(&mut self) {
self.retain(|warning| !matches!(warning, Warning::TypeMismatch { .. }));
}
pub fn remove_null_fields(&mut self) {
self.retain(|warning| !matches!(warning, Warning::NullField));
}
pub fn remove_cardinalities(&mut self) {
self.retain(|warning| !matches!(warning, Warning::Cardinality { .. }));
}
pub fn remove_string_too_longs(&mut self) {
self.retain(|warning| !matches!(warning, Warning::StringTooLong { .. }));
}
}
static ANY: Schema = Schema::Scalar(Scalar::Any);
enum Step<'a, 'buf> {
Visit {
elem: &'a json::Element<'buf>,
schema: &'a Schema,
slot: Slot,
},
Close { slot: Slot },
}
#[derive(Clone, Copy)]
enum Slot {
Root,
Field { name: &'static str },
Item,
Ignore,
}
fn walk<'a, 'buf>(
doc: &'a json::Document<'buf>,
schema: &'a Schema,
) -> Caveat<build::Node<'buf>, Warning> {
let mut warnings = warning::Set::new();
let mut builders: Vec<build::Node<'buf>> = Vec::new();
let mut root = build::Node::Ignore;
let mut stack = vec![Step::Visit {
elem: doc.root(),
schema,
slot: Slot::Root,
}];
while let Some(step) = stack.pop() {
match step {
Step::Visit { elem, schema, slot } => {
if let json::Value::Null = elem.value() {
warnings.insert(elem, Warning::NullField);
root.route_to_parent(&mut builders, slot, Integrity::Missing);
continue;
}
match schema {
Schema::Scalar(Scalar::Any) => {
enqueue_all_children(&mut stack, elem);
root.route_to_parent(&mut builders, slot, Integrity::Missing);
}
Schema::Scalar(scalar) => {
let built = check_scalar(&mut warnings, elem, *scalar);
root.route_to_parent(&mut builders, slot, built);
}
Schema::Array { item, cardinality } => {
let type_expectation = open_array(
&mut stack,
&mut builders,
&mut warnings,
elem,
item,
*cardinality,
slot,
);
if type_expectation.is_type_invalid() {
root.route_to_parent(&mut builders, slot, Integrity::Err);
}
}
Schema::Object(object) => {
let type_expectation = open_object(
&mut stack,
&mut builders,
&mut warnings,
elem,
object,
slot,
);
if type_expectation.is_type_invalid() {
root.route_to_parent(&mut builders, slot, Integrity::Err);
}
}
}
}
Step::Close { slot } => {
if let Some(node) = builders.pop() {
root.route_to_parent(&mut builders, slot, Integrity::Ok(node));
}
}
}
}
root.into_caveat(warnings)
}
fn check_scalar<'buf>(
warnings: &mut warning::Set<Warning>,
elem: &json::Element<'buf>,
scalar: Scalar,
) -> Integrity<build::Node<'buf>> {
let expected = match scalar {
Scalar::String | Scalar::StringMax(_) | Scalar::Enum(_) => json::ValueKind::String,
Scalar::Number => json::ValueKind::Number,
Scalar::Boolean => json::ValueKind::Bool,
Scalar::Any => return Integrity::Missing,
};
let actual = elem.value().kind();
let string_encoded_number =
expected == json::ValueKind::Number && actual == json::ValueKind::String;
if actual != expected && !string_encoded_number {
warnings.insert(elem, Warning::TypeMismatch { expected, actual });
return Integrity::Err;
}
match scalar {
Scalar::String => Integrity::Ok(build::Node::Str(Str::new(elem.clone()))),
Scalar::StringMax(max) => {
if let Some(value) = elem.value().to_raw_str() {
let len = value.decode_escapes().ignore_warnings().chars().count();
if len > max {
warnings.insert(elem, Warning::StringTooLong { max, len });
}
}
Integrity::Ok(build::Node::Str(Str::new(elem.clone())))
}
Scalar::Enum(allowed) => {
let Some(value) = elem.value().to_raw_str() else {
return Integrity::Err;
};
let canonical = allowed
.iter()
.copied()
.find(|variant| value.eq_any_escape_aware_ignore_ascii_case(&[variant]));
let Some(canonical) = canonical else {
warnings.insert(
elem,
Warning::FieldInvalidValue {
expected: allowed,
actual: value.as_unescaped_str().to_owned(),
},
);
return Integrity::Err;
};
Integrity::Ok(build::Node::Enum(Enum::new(elem.clone(), canonical)))
}
Scalar::Number => {
if string_encoded_number {
Integrity::Ok(build::Node::Number(Number::StringEncoded(elem.clone())))
} else {
Integrity::Ok(build::Node::Number(Number::Number(elem.clone())))
}
}
Scalar::Boolean => Integrity::Ok(build::Node::Bool),
Scalar::Any => Integrity::Missing,
}
}
#[derive(Copy, Clone)]
enum TypeExpectation {
Satisfied,
Invalid,
}
impl TypeExpectation {
fn is_type_invalid(self) -> bool {
matches!(self, Self::Invalid)
}
}
fn open_object<'a, 'buf>(
stack: &mut Vec<Step<'a, 'buf>>,
builders: &mut Vec<build::Node<'buf>>,
warnings: &mut warning::Set<Warning>,
elem: &'a json::Element<'buf>,
object: &'a Object,
slot: Slot,
) -> TypeExpectation {
debug_assert!(
object
.fields
.windows(2)
.all(|pair| matches!(pair, [a, b] if a.name <= b.name)),
"Object::fields must be sorted alphabetically by name"
);
let json::Value::Object(fields) = elem.value() else {
warnings.insert(
elem,
Warning::TypeMismatch {
expected: json::ValueKind::Object,
actual: elem.value().kind(),
},
);
return TypeExpectation::Invalid;
};
builders.push(build::empty(object.kind));
stack.push(Step::Close { slot });
let mut seen = vec![false; object.fields.len()];
for field in fields {
let key = field.key().as_unescaped_str();
let Ok(idx) = object.fields.binary_search_by_key(&key, |fd| fd.name) else {
warnings.insert(field.element(), Warning::UnexpectedField);
continue;
};
if let Some(flag) = seen.get_mut(idx) {
*flag = true;
}
if let Some(fd) = object.fields.get(idx) {
stack.push(Step::Visit {
elem: field.element(),
schema: &fd.schema,
slot: Slot::Field { name: fd.name },
});
}
}
for (field, &present) in object.fields.iter().zip(seen.iter()) {
if present {
continue;
}
if let Presence::Required = field.presence {
warnings.insert(elem, Warning::MissingField { name: field.name });
}
build::set_top_field(builders, field.name, Integrity::Missing);
}
TypeExpectation::Satisfied
}
fn open_array<'a, 'buf>(
stack: &mut Vec<Step<'a, 'buf>>,
builders: &mut Vec<build::Node<'buf>>,
warnings: &mut warning::Set<Warning>,
elem: &'a json::Element<'buf>,
item: &'a Schema,
cardinality: Cardinality,
slot: Slot,
) -> TypeExpectation {
let json::Value::Array(items) = elem.value() else {
warnings.insert(
elem,
Warning::TypeMismatch {
expected: json::ValueKind::Array,
actual: elem.value().kind(),
},
);
return TypeExpectation::Invalid;
};
if cardinality == Cardinality::OneOrMore && items.is_empty() {
warnings.insert(
elem,
Warning::Cardinality {
expected: cardinality,
len: 0,
},
);
}
builders.push(build::Node::Array(Vec::with_capacity(items.len())));
stack.push(Step::Close { slot });
for child in items.iter().rev() {
stack.push(Step::Visit {
elem: child,
schema: item,
slot: Slot::Item,
});
}
TypeExpectation::Satisfied
}
fn enqueue_all_children<'a, 'buf>(stack: &mut Vec<Step<'a, 'buf>>, elem: &'a json::Element<'buf>) {
match elem.value() {
json::Value::Array(items) => {
for child in items.iter().rev() {
stack.push(Step::Visit {
elem: child,
schema: &ANY,
slot: Slot::Ignore,
});
}
}
json::Value::Object(fields) => {
for field in fields.iter().rev() {
stack.push(Step::Visit {
elem: field.element(),
schema: &ANY,
slot: Slot::Ignore,
});
}
}
json::Value::Null
| json::Value::True
| json::Value::False
| json::Value::String(_)
| json::Value::Number(_) => {}
}
}
#[derive(Clone, Debug)]
pub(crate) struct Str<'buf> {
elem: json::Element<'buf>,
}
impl<'buf> Str<'buf> {
pub(super) fn new(elem: json::Element<'buf>) -> Self {
Self { elem }
}
#[allow(dead_code, reason = "Will be used in FromSchema integration PR")]
pub fn element(&self) -> &json::Element<'buf> {
&self.elem
}
}
#[derive(Clone, Debug)]
pub(crate) enum Number<'buf> {
Number(json::Element<'buf>),
StringEncoded(json::Element<'buf>),
}
#[expect(dead_code, reason = "For use in future `FromSchema` trait")]
impl<'buf> Number<'buf> {
pub fn element(&self) -> &json::Element<'buf> {
match self {
Self::Number(elem) | Self::StringEncoded(elem) => elem,
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct Enum<'buf> {
elem: json::Element<'buf>,
canonical: &'static str,
}
#[expect(dead_code, reason = "For use in future `FromSchema` trait")]
impl<'buf> Enum<'buf> {
pub fn new(elem: json::Element<'buf>, canonical: &'static str) -> Self {
Self { elem, canonical }
}
pub fn element(&self) -> &json::Element<'buf> {
&self.elem
}
pub fn canonical(&self) -> &'static str {
self.canonical
}
}