Skip to main content

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;