ocpi_tariffs/schema.rs
1//! Validate a [`json::Document`] against the OCPI schema for a given version.
2//!
3//! The walk checks the JSON kind, string length, array cardinality, and enum variant of
4//! every field the spec defines, and reports each violation as a [`Warning`]. It also
5//! builds the intermediate representation the pricing, linting, and explain features read.
6//!
7//! [`Warning`] is the only public item here. The intermediate representation and the
8//! per-version schema tables are an implementation detail: callers reach the validated
9//! data through [`cdr::from_json`](crate::cdr::from_json) and
10//! [`tariff::from_json`](crate::tariff::from_json), which return the [`Warning`]s this
11//! module produced alongside the object they built.
12
13pub(crate) mod v211;
14pub(crate) mod v221;
15
16mod build;
17
18#[cfg(test)]
19mod test_walk;
20
21use std::collections::BTreeSet;
22
23use crate::{
24 json,
25 warning::{self, IntoCaveat as _},
26 Caveat, Verdict,
27};
28
29/// Lower a borrowed schema IR object `Source` into a domain type.
30///
31/// The schema has already validated the kind, length, cardinality, and enum
32/// variants. The `FromSchema` only needs to perform semantic interpretation.
33pub(crate) trait FromSchema<'buf, Source>: Sized {
34 /// Warning type emitted for semantic issues found while lowering.
35 type Warning: warning::Warning;
36
37 /// Convert `source` to `Self`, collecting any semantic issues as warnings.
38 fn from_schema(source: &Source) -> Verdict<Self, Self::Warning>;
39}
40
41/// A schema-IR value that carries the [`json::Element`] it was built from.
42///
43/// Every leaf ([`Str`], [`Number`], [`Enum`]) and every object IR value that retains its
44/// element implements this. It gives the lowering step a uniform way to reach a value's
45/// element without naming the concrete type.
46///
47/// See [`warning::Set::ok_or_bail`].
48pub(crate) trait HasElement<'buf> {
49 /// The element this value was built from.
50 fn element(&self) -> &json::Element<'buf>;
51}
52
53/// Describes the expected structure of a JSON value.
54#[derive(Clone, Copy)]
55enum Schema {
56 /// A scalar value of a known JSON kind (see [`Scalar`]).
57 Scalar(Scalar),
58 /// A JSON object with a known set of fields.
59 Object(&'static Object),
60 /// A JSON object the spec for this version does not define, but which this layer reads
61 /// anyway (see [`Presence::NonSpec`]).
62 ///
63 /// A field the object does not list is not reported: the object itself is already
64 /// flagged as non-spec, so listing what it contains adds noise rather than
65 /// information.
66 NonSpecObject(&'static Object),
67 /// A `Price` value, which may be either a JSON object or a bare JSON number.
68 ///
69 /// OCPI 2.1.1 wrote a price as a bare `number`; 2.2.1 made it a `Price` object.
70 /// A JSON object is validated against the wrapped [`Object`] as usual. A bare JSON
71 /// number is accepted (and flagged as a type mismatch) and lowered to a `Price`
72 /// whose `excl_vat` is that number, leaving `incl_vat` absent. Any other kind is a
73 /// type error.
74 Price(&'static Object),
75 /// A homogeneous JSON array; each element validated against `item`, with a
76 /// minimum element count given by `cardinality`.
77 Array {
78 item: &'static Schema,
79 cardinality: Cardinality,
80 },
81}
82
83/// The minimum number of elements an array must contain.
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub(crate) enum Cardinality {
86 /// An array with zero or more element is expected. An empty array is valid.
87 ZeroOrMore,
88 /// An array with one or more elements is expected. An empty array is a violation.
89 OneOrMore,
90}
91
92impl std::fmt::Display for Cardinality {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 match self {
95 Cardinality::ZeroOrMore => f.write_str("zero or more"),
96 Cardinality::OneOrMore => f.write_str("one or more"),
97 }
98 }
99}
100
101/// The expected JSON kind of a scalar field.
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103enum Scalar {
104 /// A JSON string with no length bound. Covers OCPI `DateTime`, `date`, and
105 /// `time` (which are format-constrained, not length-constrained) and any
106 /// string the spec defines without a declared length. Strings the spec
107 /// declares as `string(n)` / `CiString(n)` use [`Scalar::StringMax`]; enum
108 /// types use [`Scalar::Enum`].
109 String,
110 /// A JSON string with a maximum character length, per the OCPI `string(n)`
111 /// or `CiString(n)` declaration. The value is checked to be a string and
112 /// then its decoded character count is compared against the length bound.
113 StringMax(usize),
114 /// A JSON string constrained to a fixed set of enum variants as defined
115 /// in the OCPI spec. Every OCPI enum serializes as a string. This table
116 /// lists each permitted spec value (the spec requires uppercase). The value
117 /// is matched case-insensitively and the matched spec value is stored in
118 /// [`Enum`], to be resolved to a typed variant during extraction.
119 Enum(&'static [&'static str]),
120 /// A JSON number. Covers OCPI `number`, `int`, and `decimal`.
121 Number,
122 /// A JSON boolean.
123 Boolean,
124 /// Any value; the JSON kind is not constrained. Used for fields whose value
125 /// is a nested object or array this schema layer deliberately does not
126 /// model (e.g. `BusinessDetails`, `Hours`).
127 Any,
128}
129
130/// The integrity of a field. Building an IR value is infallible.
131/// Every field ends in one of these states rather than aborting the build.
132/// The detail behind `Err` (the kind mismatch, the invalid value) is recorded
133/// in the accompanying [`warning::Set`].
134///
135/// A field the OCPI spec defines as optional is typed `Integrity<Option<T>>`: an
136/// absent optional field is `Ok(None)`, not [`Integrity::Missing`].
137/// [`Integrity::Missing`] therefore only ever describes an absent (or `null`)
138/// *required* field, which is also reported as a [`Warning::MissingField`].
139#[derive(Clone, Debug, PartialEq, Eq)]
140pub(crate) enum Integrity<T> {
141 /// The field was present and built successfully.
142 Ok(T),
143 /// A required field was absent or `null`. This is also reported as a
144 /// [`Warning::MissingField`]. The location is the containing object's, since an
145 /// absent field has no element of its own.
146 Missing(warning::Element),
147 /// The field was present but could not be built (wrong JSON kind, or an
148 /// otherwise invalid value). The location is the field's own.
149 Err(warning::Element),
150}
151
152impl<T> Integrity<Option<T>> {
153 /// Map the contained `Option<T>` to `Option<U>`.
154 /// Return `Some(U)` if `Ok(Some(T))`.
155 /// Otherwise, return `None` if `Ok(None)`, `Missing`, or `Err`.
156 pub(crate) fn map_some<U, F: FnOnce(&T) -> U>(&self, op: F) -> Option<U> {
157 match self {
158 Integrity::Ok(Some(v)) => Some(op(v)),
159 Integrity::Ok(None) | Integrity::Missing(_) | Integrity::Err(_) => None,
160 }
161 }
162}
163
164/// Generate an all-[`Integrity::Missing`] `new` constructor for an IR object struct.
165macro_rules! ir_object {
166 ($ty:ident { $($field:ident),* $(,)? }) => {
167 impl<'buf> $ty<'buf> {
168 /// An empty builder for the object at `elem`: every field starts `Missing`,
169 /// anchored to `elem`, and is filled by the walk.
170 pub(super) fn new(elem: &json::Element<'buf>) -> Self {
171 Self {
172 $($field: super::Integrity::Missing(
173 $crate::warning::Element::from_json(elem),
174 ),)*
175 }
176 }
177 }
178 };
179 (@keep_element $ty:ident { $($field:ident),* $(,)? }) => {
180 impl<'buf> $ty<'buf> {
181 /// An empty builder for the JSON object at `elem`.
182 ///
183 /// Most IR objects don't need to save the JSON element they were created from.
184 /// But some objects are removed when empty. An element's byte span is needed
185 /// for the removal. Every field starts `Missing` with an associated `elem`.
186 /// Each field is then set by the IR walk.
187 pub(super) fn new(elem: &json::Element<'buf>) -> Self {
188 Self {
189 elem: elem.clone(),
190 $($field: super::Integrity::Missing(
191 $crate::warning::Element::from_json(elem),
192 ),)*
193 }
194 }
195 }
196
197 impl<'buf> super::HasElement<'buf> for $ty<'buf> {
198 fn element(&self) -> &json::Element<'buf> {
199 &self.elem
200 }
201 }
202 };
203}
204pub(crate) use ir_object;
205
206/// Identifies which schema intermediate-representation (IR) value an [`Object`]
207/// should be mapped to during the [`walk`].
208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
209enum BuilderKind {
210 /// The object is validated for warnings but not built into any IR value.
211 Ignore,
212 V221Tariff,
213 V221Element,
214 V221PriceComponent,
215 V221Restrictions,
216 V221Price,
217 V221Cdr,
218 V221CdrLocation,
219 V221ChargingPeriod,
220 V221CdrDimension,
221 V211Tariff,
222 V211Element,
223 V211PriceComponent,
224 V211Restrictions,
225 V211Cdr,
226 V211Location,
227 V211ChargingPeriod,
228 V211CdrDimension,
229}
230
231/// The expected fields of a JSON object.
232#[derive(Clone, Copy)]
233struct Object {
234 fields: &'static [Field],
235 /// The IR value this object is built into during the [`walk`].
236 kind: BuilderKind,
237}
238
239/// One field expected in a JSON object.
240#[derive(Clone, Copy)]
241struct Field {
242 /// JSON key name.
243 ///
244 /// This value is hardcoded and will never contain escapes.
245 name: &'static str,
246 /// Whether the field must be present.
247 presence: Presence,
248 /// Expected substructure of the field value.
249 schema: Schema,
250}
251
252impl Field {
253 /// Define a required scalar of the given JSON kind.
254 const fn required(name: &'static str, scalar: Scalar) -> Self {
255 Self {
256 name,
257 presence: Presence::Required,
258 schema: Schema::Scalar(scalar),
259 }
260 }
261
262 /// Define a required array (OCPI `+`: present and nonempty).
263 const fn required_array(name: &'static str, item: &'static Schema) -> Self {
264 Self {
265 name,
266 presence: Presence::Required,
267 schema: Schema::Array {
268 item,
269 cardinality: Cardinality::OneOrMore,
270 },
271 }
272 }
273
274 /// Define a required object.
275 const fn required_object(name: &'static str, schema: &'static Object) -> Self {
276 Self {
277 name,
278 presence: Presence::Required,
279 schema: Schema::Object(schema),
280 }
281 }
282
283 /// Define a required `Price` field, which per OCPI's 2.1.1-to-2.2.1 evolution
284 /// accepts either a `Price` object or a bare number.
285 const fn required_price(name: &'static str, schema: &'static Object) -> Self {
286 Self {
287 name,
288 presence: Presence::Required,
289 schema: Schema::Price(schema),
290 }
291 }
292
293 /// Define an optional scalar of the given JSON kind.
294 const fn optional(name: &'static str, scalar: Scalar) -> Self {
295 Self {
296 name,
297 presence: Presence::Optional,
298 schema: Schema::Scalar(scalar),
299 }
300 }
301
302 /// Define an optional array (OCPI `*`: may be absent or empty).
303 const fn optional_array(name: &'static str, item: &'static Schema) -> Self {
304 Self {
305 name,
306 presence: Presence::Optional,
307 schema: Schema::Array {
308 item,
309 cardinality: Cardinality::ZeroOrMore,
310 },
311 }
312 }
313
314 /// Define an optional object.
315 const fn optional_object(name: &'static str, schema: &'static Object) -> Self {
316 Self {
317 name,
318 presence: Presence::Optional,
319 schema: Schema::Object(schema),
320 }
321 }
322
323 /// Define an optional `Price` field, which per OCPI's 2.1.1-to-2.2.1 evolution
324 /// accepts either a `Price` object or a bare number.
325 const fn optional_price(name: &'static str, schema: &'static Object) -> Self {
326 Self {
327 name,
328 presence: Presence::Optional,
329 schema: Schema::Price(schema),
330 }
331 }
332
333 /// Define a scalar the spec for this version does not define, but which this layer
334 /// reads anyway (see [`Presence::NonSpec`]).
335 const fn non_spec(name: &'static str, scalar: Scalar) -> Self {
336 Self {
337 name,
338 presence: Presence::NonSpec,
339 schema: Schema::Scalar(scalar),
340 }
341 }
342
343 /// Define an object the spec for this version does not define, but which this layer
344 /// reads anyway (see [`Presence::NonSpec`] and [`Schema::NonSpecObject`]).
345 const fn non_spec_object(name: &'static str, schema: &'static Object) -> Self {
346 Self {
347 name,
348 presence: Presence::NonSpec,
349 schema: Schema::NonSpecObject(schema),
350 }
351 }
352}
353
354/// Whether a field must be present in its containing object.
355#[derive(Clone, Copy, Debug, PartialEq, Eq)]
356pub(crate) enum Presence {
357 /// The schema requires the field. Its absence is a violation (also reported as
358 /// [`Warning::MissingField`]).
359 Required,
360 /// The schema permits the field to be absent.
361 Optional,
362 /// The spec for this version does not define the field, but this layer reads it when a
363 /// document supplies it, because real-world documents carry it.
364 ///
365 /// Its absence is not a violation; its presence is reported as
366 /// [`Warning::NonSpecField`] so the caller still learns the document is off-spec.
367 NonSpec,
368}
369
370/// A structural problem found while validating a JSON document against the OCPI schema.
371#[derive(Clone, Debug, PartialEq, Eq)]
372pub enum Warning {
373 /// A field present in the JSON that the schema does not list.
374 UnexpectedField,
375 /// A field the spec for this version does not define, which this layer reads anyway
376 /// because real-world documents carry it. The value is still validated and retained.
377 NonSpecField,
378 /// A required field absent from its containing object.
379 MissingField {
380 /// The field name the schema expected.
381 name: &'static str,
382 },
383 /// A field whose value is JSON `null`. `null` fields can simply be omitted.
384 NullField,
385 /// A value whose JSON kind does not match the schema.
386 InvalidType {
387 /// The JSON kind the schema expects.
388 expected: json::ValueKind,
389 /// The JSON kind encountered.
390 actual: json::ValueKind,
391 },
392 /// A string longer than the maximum length the schema permits.
393 StringTooLong {
394 /// The maximum character length the schema allows.
395 max: usize,
396 /// The character length actually encountered.
397 len: usize,
398 },
399 /// A string value that is not one of an enum field's permitted variants, read without
400 /// regard to case.
401 InvalidValue {
402 /// The permitted spec values, in the spec's defined case.
403 expected: &'static [&'static str],
404 /// The value encountered, as written in the JSON (escapes not decoded).
405 actual: String,
406 },
407
408 /// A string value that names one of an enum field's variants, but not in the spec's case.
409 ///
410 /// Both OCPI versions state that strings in messages and enumerations are case-sensitive,
411 /// so the value is off-spec rather than merely unconventional. This crate reads it anyway,
412 /// and says which variant it read.
413 IncorrectCase {
414 /// The variant the value named, in the spec's defined case.
415 expected: &'static str,
416 /// The value encountered, as written in the JSON (escapes not decoded).
417 actual: String,
418 },
419 /// An empty array for a field whose schema requires one or more elements.
420 ///
421 /// The only cardinality the schema can violate is "one or more", and the only way to
422 /// violate it is to hold nothing, so this variant carries no data.
423 Cardinality,
424}
425
426impl crate::Warning for Warning {
427 fn id(&self) -> warning::Id {
428 match self {
429 Self::UnexpectedField => warning::Id::from_static("unexpected_field"),
430 Self::NonSpecField => warning::Id::from_static("non_spec_field"),
431 Self::MissingField { name } => {
432 warning::Id::from_string(format!("missing_field({name})"))
433 }
434 Self::NullField => warning::Id::from_static("null_field"),
435 Self::InvalidType { actual, .. } => {
436 warning::Id::from_string(format!("invalid_type({actual})"))
437 }
438 Self::StringTooLong { .. } => warning::Id::from_static("string_too_long"),
439 Self::InvalidValue { actual, .. } => {
440 warning::Id::from_string(format!("invalid_value({actual})"))
441 }
442 Self::IncorrectCase { .. } => warning::Id::from_static("incorrect_case"),
443 Self::Cardinality => warning::Id::from_static("cardinality(one or more)"),
444 }
445 }
446}
447
448impl std::fmt::Display for Warning {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 match self {
451 Self::UnexpectedField => f.write_str("field is not part of the schema"),
452 Self::NonSpecField => f.write_str(
453 "field is not defined by the OCPI version of this document; it is read anyway",
454 ),
455 Self::MissingField { name } => write!(f, "required field `{name}` is missing"),
456 Self::NullField => f.write_str(
457 "field is `null`. `null` fields have no semantic meaning for OCPI objects",
458 ),
459 Self::InvalidType { expected, actual } => {
460 write!(f, "expected {expected} found {actual}")
461 }
462 Self::StringTooLong { max, len } => {
463 write!(
464 f,
465 "string is `{len}` characters, but the maximum allowed is `{max}`"
466 )
467 }
468 Self::InvalidValue { expected, actual } => {
469 write!(
470 f,
471 "value `{actual}` is not one of the permitted values: {}",
472 expected.join(", ")
473 )
474 }
475 Self::IncorrectCase { expected, actual } => {
476 write!(
477 f,
478 "value `{actual}` is not written in the case the spec uses: `{expected}`"
479 )
480 }
481 Self::Cardinality => f.write_str("expected one or more elements, found 0"),
482 }
483 }
484}
485
486impl warning::Set<Warning> {
487 /// Collect the field paths of all [`Warning::UnexpectedField`] warnings into a set of `json::Path`s.
488 pub fn unexpected_fields(&self) -> json::PathSet<'_> {
489 let mut paths = BTreeSet::new();
490
491 for group in self {
492 let (element, group_warnings) = group.to_parts();
493
494 let has_unexpected_field = group_warnings
495 .iter()
496 .any(|warning| matches!(warning, Warning::UnexpectedField));
497
498 if has_unexpected_field {
499 paths.insert(&element.path);
500 }
501 }
502
503 json::PathSet::new(paths)
504 }
505
506 /// Collect the field paths of all [`Warning::NonSpecField`] warnings into a set of `json::Path`s.
507 pub fn non_spec_fields(&self) -> json::PathSet<'_> {
508 let mut paths = BTreeSet::new();
509
510 for group in self {
511 let (element, group_warnings) = group.to_parts();
512
513 let has_non_spec_field = group_warnings
514 .iter()
515 .any(|warning| matches!(warning, Warning::NonSpecField));
516
517 if has_non_spec_field {
518 paths.insert(&element.path);
519 }
520 }
521
522 json::PathSet::new(paths)
523 }
524
525 /// Collect the field paths of all [`Warning::MissingField`] warnings into a set of `json::Path`s.
526 pub fn missing_fields(&self) -> json::PathSet<'_> {
527 let mut paths = BTreeSet::new();
528
529 for group in self {
530 let (element, group_warnings) = group.to_parts();
531
532 let has_missing_field = group_warnings
533 .iter()
534 .any(|warning| matches!(warning, Warning::MissingField { .. }));
535
536 if has_missing_field {
537 paths.insert(&element.path);
538 }
539 }
540
541 json::PathSet::new(paths)
542 }
543
544 /// Remove all [`Warning::UnexpectedField`] warnings from the set.
545 pub fn remove_unexpected_fields(&mut self) {
546 self.retain(|warning| !matches!(warning, Warning::UnexpectedField));
547 }
548
549 /// Remove all [`Warning::MissingField`] warnings from the set.
550 pub fn remove_missing_fields(&mut self) {
551 self.retain(|warning| !matches!(warning, Warning::MissingField { .. }));
552 }
553
554 /// Remove all [`Warning::InvalidType`] warnings from the set.
555 pub fn remove_invalid_types(&mut self) {
556 self.retain(|warning| !matches!(warning, Warning::InvalidType { .. }));
557 }
558
559 /// Remove all [`Warning::NullField`] warnings from the set.
560 pub fn remove_null_fields(&mut self) {
561 self.retain(|warning| !matches!(warning, Warning::NullField));
562 }
563
564 /// Remove all [`Warning::Cardinality`] warnings from the set.
565 pub fn remove_cardinalities(&mut self) {
566 self.retain(|warning| !matches!(warning, Warning::Cardinality));
567 }
568
569 /// Remove all [`Warning::StringTooLong`] warnings from the set.
570 pub fn remove_string_too_longs(&mut self) {
571 self.retain(|warning| !matches!(warning, Warning::StringTooLong { .. }));
572 }
573}
574
575/// Opaque-subtree marker: a value the schema does not model. The subtree is still
576/// walked so nested `null`s are reported.
577static ANY: Schema = Schema::Scalar(Scalar::Any);
578
579/// A step in the [`walk`]'s work stack.
580enum Step<'a, 'buf> {
581 /// Visit a node: record its warnings and build its leaf, or open its
582 /// object/array builder.
583 Visit {
584 elem: &'a json::Element<'buf>,
585 schema: &'a Schema,
586 slot: Slot,
587 },
588 /// Finalize the builder on top of the builder stack and route it to its parent.
589 Close { slot: Slot },
590}
591
592/// Where a built [`build::Node`] attaches within its parent.
593#[derive(Clone, Copy)]
594enum Slot {
595 /// The root value of the walk.
596 Root,
597 /// A named field of the parent object.
598 Field { name: &'static str },
599 /// An item of the parent array.
600 Item,
601 /// A value that is discarded (an unmodeled [`Scalar::Any`] subtree).
602 Ignore,
603}
604
605/// Validate `doc` against `schema` and build its intermediate representation (IR) in a
606/// single pass.
607///
608/// The returned [`build::Node`] is the value of the root [`Object`]'s [`BuilderKind`].
609/// When used through the public API the returned object will be one of the CDR or tariffs
610/// root types.
611///
612/// Building is infallible. Problems with a field emit a [`Warning`] and are stored as
613/// an [`Integrity::Err`] or [`Integrity::Missing`] on the IR object's field.
614///
615/// NOTE: A value whose type is invalid (a type mismatch) or whose key is
616/// unexpected is recorded but not descended into. Its substructure cannot
617/// be compared to the schema. Opaque [`Scalar::Any`] values are still
618/// walked, so nested `null`s are still reported for the inner JSON.
619fn walk<'a, 'buf>(
620 doc: &'a json::Document<'buf>,
621 schema: &'a Schema,
622) -> Caveat<build::Node<'buf>, Warning> {
623 let mut warnings = warning::Set::new();
624 let mut builders: Vec<build::Node<'buf>> = Vec::new();
625 let mut root = build::Node::Ignore;
626
627 // Iteration order: an object's own problems are recorded before its descendants'
628 // because its fields are scanned (emitting unexpected/missing warnings) when the
629 // object is opened, before the field `Visit`s pushed here are popped.
630 let mut stack = vec![Step::Visit {
631 elem: doc.root(),
632 schema,
633 slot: Slot::Root,
634 }];
635
636 while let Some(step) = stack.pop() {
637 match step {
638 Step::Visit { elem, schema, slot } => {
639 if let json::Value::Null = elem.value() {
640 warnings.insert(elem, Warning::NullField);
641 root.route_to_parent(
642 &mut builders,
643 slot,
644 Integrity::Missing(warning::Element::from_json(elem)),
645 );
646 continue;
647 }
648 match schema {
649 // An unmodeled subtree: walk children only to report nested nulls.
650 Schema::Scalar(Scalar::Any) => {
651 enqueue_all_children(&mut stack, elem);
652 root.route_to_parent(
653 &mut builders,
654 slot,
655 Integrity::Missing(warning::Element::from_json(elem)),
656 );
657 }
658 Schema::Scalar(scalar) => {
659 let built = check_scalar(&mut warnings, elem, *scalar);
660 root.route_to_parent(&mut builders, slot, built);
661 }
662 Schema::Array { item, cardinality } => {
663 let type_expectation = open_array(
664 &mut stack,
665 &mut builders,
666 &mut warnings,
667 elem,
668 item,
669 *cardinality,
670 slot,
671 );
672 if type_expectation.is_type_invalid() {
673 root.route_to_parent(
674 &mut builders,
675 slot,
676 Integrity::Err(warning::Element::from_json(elem)),
677 );
678 }
679 }
680 Schema::Object(object) => {
681 let type_expectation = open_object(
682 &mut stack,
683 &mut builders,
684 &mut warnings,
685 elem,
686 object,
687 slot,
688 Unlisted::Report,
689 );
690 if type_expectation.is_type_invalid() {
691 root.route_to_parent(
692 &mut builders,
693 slot,
694 Integrity::Err(warning::Element::from_json(elem)),
695 );
696 }
697 }
698 Schema::NonSpecObject(object) => {
699 let type_expectation = open_object(
700 &mut stack,
701 &mut builders,
702 &mut warnings,
703 elem,
704 object,
705 slot,
706 Unlisted::Ignore,
707 );
708 if type_expectation.is_type_invalid() {
709 root.route_to_parent(
710 &mut builders,
711 slot,
712 Integrity::Err(warning::Element::from_json(elem)),
713 );
714 }
715 }
716 Schema::Price(object) => {
717 // A bare number is the 2.1.1 price shape; accept it directly.
718 // Any other kind (including an object) is validated as an object.
719 if let Some(node) = price_from_number(&mut warnings, elem) {
720 root.route_to_parent(&mut builders, slot, Integrity::Ok(node));
721 } else {
722 let type_expectation = open_object(
723 &mut stack,
724 &mut builders,
725 &mut warnings,
726 elem,
727 object,
728 slot,
729 Unlisted::Report,
730 );
731 if type_expectation.is_type_invalid() {
732 root.route_to_parent(
733 &mut builders,
734 slot,
735 Integrity::Err(warning::Element::from_json(elem)),
736 );
737 }
738 }
739 }
740 }
741 }
742 Step::Close { slot } => {
743 if let Some(node) = builders.pop() {
744 root.route_to_parent(&mut builders, slot, Integrity::Ok(node));
745 }
746 }
747 }
748 }
749
750 root.into_caveat(warnings)
751}
752
753/// Build the leaf [`build::Node`] for a scalar, recording any kind, length, enum, or
754/// string-encoded-number warning. Returns [`Integrity::Err`] for a wrong-kind value.
755fn check_scalar<'buf>(
756 warnings: &mut warning::Set<Warning>,
757 elem: &json::Element<'buf>,
758 scalar: Scalar,
759) -> Integrity<build::Node<'buf>> {
760 let expected = match scalar {
761 // Enums serialize as JSON strings; their kind check is the same as a plain
762 // string, with the value-membership check applied below.
763 Scalar::String | Scalar::StringMax(_) | Scalar::Enum(_) => json::ValueKind::String,
764 Scalar::Number => json::ValueKind::Number,
765 Scalar::Boolean => json::ValueKind::Bool,
766 // `Any` is handled by the caller; never built here.
767 Scalar::Any => return Integrity::Missing(warning::Element::from_json(elem)),
768 };
769
770 let actual = elem.value().kind();
771
772 // A `number` may be encoded as a JSON string; that is accepted but flagged below.
773 let string_encoded_number =
774 expected == json::ValueKind::Number && actual == json::ValueKind::String;
775 if actual != expected && !string_encoded_number {
776 warnings.insert(elem, Warning::InvalidType { expected, actual });
777 return Integrity::Err(warning::Element::from_json(elem));
778 }
779
780 match scalar {
781 Scalar::String => {
782 // The kind gate above guarantees a string.
783 let json::Value::String(text) = elem.value() else {
784 unreachable!("kind gate guarantees a string");
785 };
786 Integrity::Ok(build::Node::Str(Str::new(elem.clone(), *text)))
787 }
788 Scalar::StringMax(max) => {
789 // The kind gate above guarantees a string.
790 let json::Value::String(text) = elem.value() else {
791 unreachable!("kind gate guarantees a string");
792 };
793 let len = text.decode_escapes().ignore_warnings().chars().count();
794 if len > max {
795 warnings.insert(elem, Warning::StringTooLong { max, len });
796 }
797 Integrity::Ok(build::Node::Str(Str::new(elem.clone(), *text)))
798 }
799 Scalar::Enum(variants) => {
800 let Some(value) = elem.value().to_raw_str() else {
801 return Integrity::Err(warning::Element::from_json(elem));
802 };
803 let matched = variants
804 .iter()
805 .copied()
806 .find(|&s| value.eq_any_escape_aware_ignore_ascii_case(&[s]));
807 let Some(canonical) = matched else {
808 warnings.insert(
809 elem,
810 Warning::InvalidValue {
811 expected: variants,
812 actual: value.as_unescaped_str().to_owned(),
813 },
814 );
815 return Integrity::Err(warning::Element::from_json(elem));
816 };
817
818 // The match above ignores case and decodes escapes, so a value that reaches here
819 // and still differs from the variant differs in case alone.
820 if !value.eq_any_escape_aware(&[canonical]) {
821 warnings.insert(
822 elem,
823 Warning::IncorrectCase {
824 expected: canonical,
825 actual: value.as_unescaped_str().to_owned(),
826 },
827 );
828 }
829
830 Integrity::Ok(build::Node::Enum(elem.clone(), canonical))
831 }
832 Scalar::Number => {
833 // OCPI permits a number to be encoded as a JSON string. The value is accepted
834 // either way; the linter can choose to flag the string-encoded form later. The
835 // match is exhaustive in practice: the kind gate above already rejected any
836 // value that is neither a JSON number nor a JSON string.
837 match elem.value() {
838 json::Value::Number(digits) => Integrity::Ok(build::Node::Number(Number::Number {
839 elem: elem.clone(),
840 digits,
841 })),
842 json::Value::String(text) => {
843 Integrity::Ok(build::Node::Number(Number::StringEncoded {
844 elem: elem.clone(),
845 value: *text,
846 }))
847 }
848 json::Value::Null
849 | json::Value::True
850 | json::Value::False
851 | json::Value::Array(_)
852 | json::Value::Object(_) => unreachable!(
853 "kind gate rejects any value that is neither a number nor a string"
854 ),
855 }
856 }
857 Scalar::Boolean => Integrity::Ok(build::Node::Bool),
858 // Unreachable: handled above.
859 Scalar::Any => Integrity::Missing(warning::Element::from_json(elem)),
860 }
861}
862
863/// If `elem` is a bare JSON number, build the [`v221::Price`] node it stands for: the
864/// number becomes `excl_vat` and `incl_vat` is left absent. The bare-number form is
865/// flagged as a type mismatch (an object is the 2.2.1 shape) but still accepted, per
866/// OCPI's evolution from a `number` price in 2.1.1. Returns `None` for any other kind,
867/// which the caller then validates as an object.
868fn price_from_number<'buf>(
869 warnings: &mut warning::Set<Warning>,
870 elem: &json::Element<'buf>,
871) -> Option<build::Node<'buf>> {
872 let json::Value::Number(digits) = elem.value() else {
873 return None;
874 };
875
876 warnings.insert(
877 elem,
878 Warning::InvalidType {
879 expected: json::ValueKind::Object,
880 actual: json::ValueKind::Number,
881 },
882 );
883
884 Some(build::Node::Price(v221::Price::from_number(
885 elem.clone(),
886 digits,
887 )))
888}
889
890/// The [`open_array`] and [`open_object`] return whether the type they expected is
891/// the type they encountered.
892#[derive(Copy, Clone)]
893enum TypeExpectation {
894 Satisfied,
895 Invalid,
896}
897
898impl TypeExpectation {
899 fn is_type_invalid(self) -> bool {
900 matches!(self, Self::Invalid)
901 }
902}
903
904/// Whether [`open_object`] reports a field the object's schema does not list.
905#[derive(Copy, Clone)]
906enum Unlisted {
907 /// Report it as a [`Warning::UnexpectedField`].
908 Report,
909 /// Say nothing. Used for a [`Schema::NonSpecObject`], which is already reported as a
910 /// whole.
911 Ignore,
912}
913
914/// Open an object: push its builder and a [`Step::Close`], then queue its
915/// schema-matched fields. Records unexpected and missing-required-field warnings.
916/// Returns `false` (and opens nothing) if `elem` is not a JSON object.
917fn open_object<'a, 'buf>(
918 stack: &mut Vec<Step<'a, 'buf>>,
919 builders: &mut Vec<build::Node<'buf>>,
920 warnings: &mut warning::Set<Warning>,
921 elem: &'a json::Element<'buf>,
922 object: &'a Object,
923 slot: Slot,
924 unlisted: Unlisted,
925) -> TypeExpectation {
926 // `fields` are sorted alphabetically by `Field::name` so the `binary_search_by_key`
927 // below is valid; the `debug_assert` guards that against an out-of-order schema.
928 debug_assert!(
929 object
930 .fields
931 .windows(2)
932 .all(|pair| matches!(pair, [a, b] if a.name <= b.name)),
933 "Object::fields must be sorted alphabetically by name"
934 );
935 let json::Value::Object(fields) = elem.value() else {
936 warnings.insert(
937 elem,
938 Warning::InvalidType {
939 expected: json::ValueKind::Object,
940 actual: elem.value().kind(),
941 },
942 );
943 return TypeExpectation::Invalid;
944 };
945
946 builders.push(build::empty(object.kind, elem));
947 stack.push(Step::Close { slot });
948
949 // Mark, by schema-field position, which fields the document supplies. Reusing each
950 // binary-search hit here lets the missing-field scan below be a single indexed pass
951 // instead of a linear `contains` per schema field.
952 let mut seen = vec![false; object.fields.len()];
953 for field in fields {
954 let key = field.key().as_unescaped_str();
955 let Ok(idx) = object.fields.binary_search_by_key(&key, |fd| fd.name) else {
956 // Not in the schema: record it and do not walk its subtree.
957 if let Unlisted::Report = unlisted {
958 warnings.insert(field.element(), Warning::UnexpectedField);
959 }
960 continue;
961 };
962 if let Some(flag) = seen.get_mut(idx) {
963 *flag = true;
964 }
965 if let Some(fd) = object.fields.get(idx) {
966 // A field the spec does not define is read anyway, but the document is still
967 // off-spec for supplying it.
968 if let Presence::NonSpec = fd.presence {
969 warnings.insert(field.element(), Warning::NonSpecField);
970 }
971 stack.push(Step::Visit {
972 elem: field.element(),
973 schema: &fd.schema,
974 slot: Slot::Field { name: fd.name },
975 });
976 }
977 }
978
979 // An absent field has no element of its own to `Visit`, so it is recorded here.
980 // Every absent field is set to `Integrity::Missing`; the field's extractor then
981 // interprets that per the field's optionality (an optional field becomes
982 // `Integrity::Ok(None)`, a required field stays `Integrity::Missing`). A required
983 // field additionally records a `MissingField` warning against the parent, so its
984 // absence is visible in both the IR and the warnings.
985 for (field, &present) in object.fields.iter().zip(seen.iter()) {
986 if present {
987 continue;
988 }
989
990 // An absent `NonSpec` field is not a violation; the spec does not define it.
991 if let Presence::Required = field.presence {
992 warnings.insert(elem, Warning::MissingField { name: field.name });
993 }
994 build::set_top_field(
995 builders,
996 field.name,
997 Integrity::Missing(warning::Element::from_json(elem)),
998 );
999 }
1000
1001 TypeExpectation::Satisfied
1002}
1003
1004/// Open an array: push its accumulator builder and a [`Step::Close`], then queue its
1005/// items in document order. Records a cardinality warning for an empty `OneOrMore`
1006/// array.
1007///
1008/// Returns `false` (and opens nothing) if `elem` is not a JSON array.
1009fn open_array<'a, 'buf>(
1010 stack: &mut Vec<Step<'a, 'buf>>,
1011 builders: &mut Vec<build::Node<'buf>>,
1012 warnings: &mut warning::Set<Warning>,
1013 elem: &'a json::Element<'buf>,
1014 item: &'a Schema,
1015 cardinality: Cardinality,
1016 slot: Slot,
1017) -> TypeExpectation {
1018 let json::Value::Array(items) = elem.value() else {
1019 warnings.insert(
1020 elem,
1021 Warning::InvalidType {
1022 expected: json::ValueKind::Array,
1023 actual: elem.value().kind(),
1024 },
1025 );
1026 return TypeExpectation::Invalid;
1027 };
1028
1029 if cardinality == Cardinality::OneOrMore && items.is_empty() {
1030 warnings.insert(elem, Warning::Cardinality);
1031 }
1032
1033 builders.push(build::Node::Array(
1034 elem.clone(),
1035 Vec::with_capacity(items.len()),
1036 ));
1037 stack.push(Step::Close { slot });
1038
1039 // Push in reverse so items are visited, and accumulated, in document order.
1040 for child in items.iter().rev() {
1041 stack.push(Step::Visit {
1042 elem: child,
1043 schema: item,
1044 slot: Slot::Item,
1045 });
1046 }
1047
1048 TypeExpectation::Satisfied
1049}
1050
1051/// Queue the children of an opaque [`Scalar::Any`] element so nested `null`s are
1052/// still reported. Their values are discarded.
1053fn enqueue_all_children<'a, 'buf>(stack: &mut Vec<Step<'a, 'buf>>, elem: &'a json::Element<'buf>) {
1054 match elem.value() {
1055 json::Value::Array(items) => {
1056 for child in items.iter().rev() {
1057 stack.push(Step::Visit {
1058 elem: child,
1059 schema: &ANY,
1060 slot: Slot::Ignore,
1061 });
1062 }
1063 }
1064 json::Value::Object(fields) => {
1065 for field in fields.iter().rev() {
1066 stack.push(Step::Visit {
1067 elem: field.element(),
1068 schema: &ANY,
1069 slot: Slot::Ignore,
1070 });
1071 }
1072 }
1073 json::Value::Null
1074 | json::Value::True
1075 | json::Value::False
1076 | json::Value::String(_)
1077 | json::Value::Number(_) => {}
1078 }
1079}
1080
1081// Constrained leaf types for the schema intermediate representation (IR).
1082//
1083// A leaf wraps a [`json::Element`] that the IR builder has already confirmed to be
1084// the right JSON kind. Downstream lowering (the `FromSchema` impls) therefore does
1085// not repeat the kind check; it only does semantic interpretation (parsing a number
1086// into a `Decimal`, validating an ISO currency code, and so on).
1087//
1088// The leaves keep a (cheap, reference-counted) clone of their [`json::Element`] so
1089// the lowering step can still attach its semantic warnings to the right path.
1090
1091/// A JSON array the builder walked, retaining the element it was built from.
1092///
1093/// The items are kept as `Integrity` values so one unreadable entry does not cost the
1094/// others. The element is what a warning about the array *as a whole* anchors to - an empty
1095/// list, or a list whose contents are individually fine but collectively wrong - which no
1096/// item can stand in for.
1097#[derive(Clone, Debug, PartialEq, Eq)]
1098pub(crate) struct List<'buf, T> {
1099 elem: json::Element<'buf>,
1100 items: Vec<Integrity<T>>,
1101}
1102
1103impl<'buf, T> List<'buf, T> {
1104 pub(super) fn new(elem: json::Element<'buf>, items: Vec<Integrity<T>>) -> Self {
1105 Self { elem, items }
1106 }
1107
1108 /// The number of items in the array, readable or not.
1109 pub fn len(&self) -> usize {
1110 self.items.len()
1111 }
1112
1113 /// True if the array holds no items at all.
1114 pub fn is_empty(&self) -> bool {
1115 self.items.is_empty()
1116 }
1117}
1118
1119impl<'a, T> IntoIterator for &'a List<'_, T> {
1120 type Item = &'a Integrity<T>;
1121 type IntoIter = std::slice::Iter<'a, Integrity<T>>;
1122
1123 fn into_iter(self) -> Self::IntoIter {
1124 self.items.iter()
1125 }
1126}
1127
1128impl<T> std::ops::Index<usize> for List<'_, T> {
1129 type Output = Integrity<T>;
1130
1131 /// # Panics
1132 ///
1133 /// Panics if `index` is out of bounds, as every `Index` implementation does. Use
1134 /// `IntoIterator` to walk the items without naming a position.
1135 #[expect(
1136 clippy::indexing_slicing,
1137 reason = "an `Index` impl is a bounds-checked panic by definition"
1138 )]
1139 fn index(&self, index: usize) -> &Self::Output {
1140 &self.items[index]
1141 }
1142}
1143
1144impl<'buf, T> HasElement<'buf> for List<'buf, T> {
1145 fn element(&self) -> &json::Element<'buf> {
1146 &self.elem
1147 }
1148}
1149
1150/// A JSON value the builder confirmed to be a string.
1151///
1152/// `text` is the confirmed string content (escapes not yet decoded), borrowed from the
1153/// source buffer; the builder proved its kind, so the lowering step reads it without
1154/// rechecking. Length and other lexical checks are applied by the builder when the leaf
1155/// is constructed; see [`crate::schema::build`].
1156#[derive(Clone, Debug)]
1157pub(crate) struct Str<'buf> {
1158 elem: json::Element<'buf>,
1159 value: json::RawStr<'buf>,
1160}
1161
1162impl<'buf> Str<'buf> {
1163 pub(super) fn new(elem: json::Element<'buf>, value: json::RawStr<'buf>) -> Self {
1164 Self { elem, value }
1165 }
1166
1167 /// The confirmed string content (escapes not yet decoded).
1168 pub fn value(&self) -> json::RawStr<'buf> {
1169 self.value
1170 }
1171}
1172
1173impl<'buf> HasElement<'buf> for Str<'buf> {
1174 fn element(&self) -> &json::Element<'buf> {
1175 &self.elem
1176 }
1177}
1178
1179/// A JSON value the builder confirmed to be a number, remembering whether it was
1180/// written as a JSON number or encoded as a JSON string.
1181///
1182/// OCPI allows a `number` to be encoded as a string; the [`Number::StringEncoded`]
1183/// variant records that so the builder can flag it and the lowering step can still
1184/// read the digits.
1185#[derive(Clone, Debug)]
1186pub(crate) enum Number<'buf> {
1187 /// A syntactically valid RFC 8259 JSON number.
1188 ///
1189 /// `digits` is the validated number text, borrowed from the source buffer. The
1190 /// builder proved its shape, so the lowering step reads it without rechecking.
1191 Number {
1192 elem: json::Element<'buf>,
1193 digits: &'buf str,
1194 },
1195 /// A number encoded as a JSON string.
1196 ///
1197 /// There are no guarantees made about the contents of the string; `text` may, for
1198 /// example, contain escape sequences the lowering step must still decode.
1199 StringEncoded {
1200 elem: json::Element<'buf>,
1201 value: json::RawStr<'buf>,
1202 },
1203}
1204
1205impl<'buf> HasElement<'buf> for Number<'buf> {
1206 fn element(&self) -> &json::Element<'buf> {
1207 match self {
1208 Self::Number { elem, .. } | Self::StringEncoded { elem, .. } => elem,
1209 }
1210 }
1211}
1212
1213/// A JSON string the builder confirmed to be one of an enum's permitted variants,
1214/// carrying the typed OCPI enum `T` it resolved to.
1215///
1216/// The builder resolves the string to its typed variant during extraction (the
1217/// schema field's concrete `T` is known there), so the lowering step reads a typed
1218/// Rust enum directly and never re-parses the string or repeats the membership check.
1219///
1220/// Only the resolved variant is kept. The case a value was written in is the walk's
1221/// business, which reports on it from the element before an `Enum` exists.
1222#[derive(Clone, Debug)]
1223pub(crate) struct Enum<'buf, T> {
1224 elem: json::Element<'buf>,
1225 value: T,
1226}
1227
1228impl<'buf, T: OcpiEnum> Enum<'buf, T> {
1229 pub fn new(elem: json::Element<'buf>, value: T) -> Self {
1230 Self { elem, value }
1231 }
1232
1233 /// The typed OCPI enum the value resolved to.
1234 pub fn value(&self) -> T {
1235 self.value
1236 }
1237}
1238
1239impl<'buf, T> HasElement<'buf> for Enum<'buf, T> {
1240 fn element(&self) -> &json::Element<'buf> {
1241 &self.elem
1242 }
1243}
1244
1245/// A single OCPI enum, as modeled by the schema layer. Implemented (via the
1246/// [`ocpi_enum!`] macro) by each version-specific OCPI enum so a generic
1247/// [`Enum<T>`] can be resolved and rendered without naming the concrete type.
1248pub(crate) trait OcpiEnum: Copy {
1249 /// Resolve a canonical spec value (one of the schema's permitted variants) to
1250 /// its typed variant. Returns `None` for a value outside this enum's set.
1251 fn from_canonical(value: &str) -> Option<Self>;
1252}
1253
1254/// Define an OCPI enum: its Rust type, the variant table used by [`Scalar::Enum`],
1255/// and the [`OcpiEnum`] impl that maps between the typed variant and its spec value.
1256///
1257/// The body lists each Rust variant with the exact spec value it serializes to.
1258/// `VARIANTS` (the permitted spec values) and [`OcpiEnum::from_canonical`]
1259/// (value-to-variant) are both generated from that single list, so the two
1260/// cannot drift.
1261macro_rules! ocpi_enum {
1262 ($kind:ident { $($variant:ident = $value:literal),+ $(,)? }) => {
1263 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1264 pub(crate) enum $kind {
1265 $($variant),+
1266 }
1267
1268 impl $kind {
1269 /// The permitted spec values, in the case the spec writes them. Used as the
1270 /// `Scalar::Enum` table.
1271 const VARIANTS: &'static [&'static str] = &[$($value),+];
1272 }
1273
1274 impl super::OcpiEnum for $kind {
1275 fn from_canonical(value: &str) -> Option<Self> {
1276 match value {
1277 $($value => Some(Self::$variant),)+
1278 _ => None,
1279 }
1280 }
1281 }
1282 };
1283}
1284pub(crate) use ocpi_enum;