Skip to main content

ocpi_tariffs/
schema.rs

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