Skip to main content

ocpi_tariffs/
schema.rs

1pub mod v211;
2pub mod v221;
3
4mod build;
5mod leaf;
6
7#[cfg(test)]
8mod tests;
9
10use std::collections::BTreeSet;
11
12use crate::{
13    json,
14    warning::{self, IntoCaveat as _},
15    Caveat,
16};
17
18/// Describes the expected structure of a JSON value.
19#[derive(Clone, Copy)]
20enum Schema {
21    /// A scalar value of a known JSON kind (see [`Scalar`]).
22    Scalar(Scalar),
23    /// A JSON object with a known set of fields.
24    Object(&'static Object),
25    /// A homogeneous JSON array; each element validated against `item`, with a
26    /// minimum element count given by `cardinality`.
27    Array {
28        item: &'static Schema,
29        cardinality: Cardinality,
30    },
31}
32
33/// The minimum number of elements an array must contain.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum Cardinality {
36    /// An array with zero or more element is expected. An empty array is valid.
37    ZeroOrMore,
38    /// An array with one or more elements is expected. An empty array is a violation.
39    OneOrMore,
40}
41
42impl std::fmt::Display for Cardinality {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Cardinality::ZeroOrMore => f.write_str("zero or more"),
46            Cardinality::OneOrMore => f.write_str("one or more"),
47        }
48    }
49}
50
51/// The expected JSON kind of a scalar field.
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53enum Scalar {
54    /// A JSON string with no length bound. Covers OCPI `DateTime`, `date`, and
55    /// `time` (which are format-constrained, not length-constrained) and any
56    /// string the spec defines without a declared length. Strings the spec
57    /// declares as `string(n)` / `CiString(n)` use [`Scalar::StringMax`]; enum
58    /// types use [`Scalar::Enum`].
59    String,
60    /// A JSON string with a maximum character length, per the OCPI `string(n)`
61    /// or `CiString(n)` declaration. The value is checked to be a string and
62    /// then its decoded character count is compared against the length bound.
63    StringMax(usize),
64    /// A JSON string constrained to a fixed set of enum variants as defined
65    /// in the OCPI spec. Every OCPI enum serializes as a string. This table
66    /// pairs each permitted enum variant (the spec requires uppercase) with
67    /// the [`EnumValue`] it denotes. The value is matched case-insensitively
68    /// and the matched [`EnumValue`] is stored in [`Enum`].
69    Enum(&'static [(&'static str, EnumValue)]),
70    /// A JSON number. Covers OCPI `number`, `int`, and `decimal`.
71    Number,
72    /// A JSON boolean.
73    Boolean,
74    /// Any value; the JSON kind is not constrained. Used for fields whose value
75    /// is a nested object or array this schema layer deliberately does not
76    /// model (e.g. `BusinessDetails`, `Hours`).
77    Any,
78}
79
80/// The integrity of a field. Building an IR value is infallible.
81/// Every field ends in one of these states rather than aborting the build.
82/// The detail behind `Err` (the kind mismatch, the invalid value) is recorded
83/// in the accompanying [`warning::Set`].
84///
85/// A field the OCPI spec defines as optional is typed `Integrity<Option<T>>`: an
86/// absent optional field is `Ok(None)`, not [`Integrity::Missing`].
87/// [`Integrity::Missing`] therefore only ever describes an absent (or `null`)
88/// *required* field, which is also reported as a [`Warning::MissingField`].
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum Integrity<T> {
91    /// The field was present and built successfully.
92    Ok(T),
93    /// A required field was absent or `null`. This is also reported as a
94    /// [`Warning::MissingField`].
95    Missing,
96    /// The field was present but could not be built (wrong JSON kind, or an
97    /// otherwise invalid value).
98    Err,
99}
100
101// A manual impl, rather than `#[derive(Default)]`, so that `Integrity<T>::default()`
102// holds for any `T`. The derive would add an unwanted `T: Default` bound (the IR structs
103// store fields like `Integrity<Str>`, where `Str` is not `Default`).
104#[allow(
105    clippy::derivable_impls,
106    reason = "derive would add an unwanted T: Default bound"
107)]
108impl<T> Default for Integrity<T> {
109    fn default() -> Self {
110        Self::Missing
111    }
112}
113
114impl<T> Integrity<T> {
115    /// Map the contained value, leaving `Missing`/`Err` unchanged.
116    pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Integrity<U> {
117        match self {
118            Integrity::Ok(value) => Integrity::Ok(op(value)),
119            Integrity::Missing => Integrity::Missing,
120            Integrity::Err => Integrity::Err,
121        }
122    }
123
124    /// Borrow the contained value.
125    pub fn as_ref(&self) -> Integrity<&T> {
126        match self {
127            Integrity::Ok(value) => Integrity::Ok(value),
128            Integrity::Missing => Integrity::Missing,
129            Integrity::Err => Integrity::Err,
130        }
131    }
132
133    /// The contained value, if `Ok`.
134    pub fn ok(self) -> Option<T> {
135        match self {
136            Integrity::Ok(value) => Some(value),
137            Integrity::Missing | Integrity::Err => None,
138        }
139    }
140}
141
142/// Identifies which schema intermediate-representation (IR) value an [`Object`]
143/// should be mapped to during the [`walk`].
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145enum BuilderKind {
146    /// The object is validated for warnings but not built into any IR value.
147    Ignore,
148    V221Tariff,
149    V221Element,
150    V221PriceComponent,
151    V221Restrictions,
152    V221Price,
153    V221Cdr,
154    V221ChargingPeriod,
155    V221CdrDimension,
156    V211Tariff,
157    V211Element,
158    V211PriceComponent,
159    V211Restrictions,
160    V211Cdr,
161    V211ChargingPeriod,
162    V211CdrDimension,
163}
164
165/// The expected fields of a JSON object.
166#[derive(Clone, Copy)]
167struct Object {
168    fields: &'static [Field],
169    /// The IR value this object is built into during the [`walk`].
170    kind: BuilderKind,
171}
172
173/// One field expected in a JSON object.
174#[derive(Clone, Copy)]
175struct Field {
176    /// JSON key name.
177    ///
178    /// This value is hardcoded and will never contain escapes.
179    name: &'static str,
180    /// Whether the field must be present.
181    presence: Presence,
182    /// Expected substructure of the field value.
183    schema: Schema,
184}
185
186impl Field {
187    /// Define a required scalar of the given JSON kind.
188    const fn required(name: &'static str, scalar: Scalar) -> Self {
189        Self {
190            name,
191            presence: Presence::Required,
192            schema: Schema::Scalar(scalar),
193        }
194    }
195
196    /// Define a required array (OCPI `+`: present and nonempty).
197    const fn required_array(name: &'static str, item: &'static Schema) -> Self {
198        Self {
199            name,
200            presence: Presence::Required,
201            schema: Schema::Array {
202                item,
203                cardinality: Cardinality::OneOrMore,
204            },
205        }
206    }
207
208    /// Define a required object.
209    const fn required_object(name: &'static str, schema: &'static Object) -> Self {
210        Self {
211            name,
212            presence: Presence::Required,
213            schema: Schema::Object(schema),
214        }
215    }
216
217    /// Define an optional scalar of the given JSON kind.
218    const fn optional(name: &'static str, scalar: Scalar) -> Self {
219        Self {
220            name,
221            presence: Presence::Optional,
222            schema: Schema::Scalar(scalar),
223        }
224    }
225
226    /// Define an optional array (OCPI `*`: may be absent or empty).
227    const fn optional_array(name: &'static str, item: &'static Schema) -> Self {
228        Self {
229            name,
230            presence: Presence::Optional,
231            schema: Schema::Array {
232                item,
233                cardinality: Cardinality::ZeroOrMore,
234            },
235        }
236    }
237
238    /// Define an optional object.
239    const fn optional_object(name: &'static str, schema: &'static Object) -> Self {
240        Self {
241            name,
242            presence: Presence::Optional,
243            schema: Schema::Object(schema),
244        }
245    }
246}
247
248/// Whether a field must be present in its containing object.
249#[derive(Clone, Copy, Debug, PartialEq, Eq)]
250pub enum Presence {
251    /// The schema requires the field. Its absence is a violation (also reported as
252    /// [`Warning::MissingField`]).
253    Required,
254    /// The schema permits the field to be absent.
255    Optional,
256}
257
258/// A structural problem found while validating a JSON document against a [`Schema`].
259#[derive(Clone, Debug, PartialEq, Eq)]
260pub enum Warning {
261    /// A field present in the JSON that the schema does not list.
262    UnexpectedField,
263    /// A required field absent from its containing object.
264    MissingField {
265        /// The field name the schema expected.
266        name: &'static str,
267    },
268    /// A field whose value is JSON `null`. `null` fields can simply be omitted.
269    NullField,
270    /// A value whose JSON kind does not match the schema.
271    TypeMismatch {
272        /// The JSON kind the schema expects.
273        expected: json::ValueKind,
274        /// The JSON kind encountered.
275        actual: json::ValueKind,
276    },
277    /// A string longer than the maximum length the schema permits.
278    StringTooLong {
279        /// The maximum character length the schema allows.
280        max: usize,
281        /// The character length actually encountered.
282        len: usize,
283    },
284    /// A string value that is not one of an enum field's permitted variants.
285    FieldInvalidValue {
286        /// The permitted variants - the spec requires uppercase - paired with the [`EnumValue`] it denotes.
287        expected: &'static [(&'static str, EnumValue)],
288        /// The value encountered, as written in the JSON (escapes not decoded).
289        actual: String,
290    },
291    /// An array holding fewer elements than its declared [`Cardinality`] requires.
292    Cardinality {
293        /// The cardinality the schema requires.
294        expected: Cardinality,
295        /// The number of elements actually present.
296        len: usize,
297    },
298}
299
300impl crate::Warning for Warning {
301    fn id(&self) -> warning::Id {
302        match self {
303            Self::UnexpectedField => warning::Id::from_static("unexpected_field"),
304            Self::MissingField { name } => {
305                warning::Id::from_string(format!("missing_field({name})"))
306            }
307            Self::NullField => warning::Id::from_static("null_field"),
308            Self::TypeMismatch { actual, .. } => {
309                warning::Id::from_string(format!("invalid_type({actual})"))
310            }
311            Self::StringTooLong { .. } => warning::Id::from_static("string_too_long"),
312            Self::FieldInvalidValue { actual, .. } => {
313                warning::Id::from_string(format!("field_invalid_value({actual})"))
314            }
315            Self::Cardinality { expected, .. } => {
316                warning::Id::from_string(format!("cardinality({expected})"))
317            }
318        }
319    }
320}
321
322impl std::fmt::Display for Warning {
323    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324        match self {
325            Self::UnexpectedField => f.write_str("field is not part of the schema"),
326            Self::MissingField { name } => write!(f, "required field `{name}` is missing"),
327            Self::NullField => f.write_str(
328                "field is `null`. `null` fields have no semantic meaning for OCPI objects",
329            ),
330            Self::TypeMismatch { expected, actual } => {
331                write!(f, "expected {expected} found {actual}")
332            }
333            Self::StringTooLong { max, len } => {
334                write!(
335                    f,
336                    "string is `{len}` characters, but the maximum allowed is `{max}`"
337                )
338            }
339            Self::FieldInvalidValue { expected, actual } => {
340                let variants: Vec<&str> = expected.iter().map(|(variant, _)| *variant).collect();
341                write!(
342                    f,
343                    "value `{actual}` is not one of the permitted values: {}",
344                    variants.join(", ")
345                )
346            }
347            Self::Cardinality { expected, len } => {
348                write!(f, "expected {expected} elements, found {len}")
349            }
350        }
351    }
352}
353
354impl warning::Set<Warning> {
355    /// Collect the field paths of all [`Warning::UnexpectedField`] warnings into a set of `json::Path`s.
356    pub fn unexpected_fields(&self) -> json::PathSet<'_> {
357        let mut paths = BTreeSet::new();
358
359        for group in self {
360            let (element, group_warnings) = group.to_parts();
361
362            let has_unexpected_field = group_warnings
363                .iter()
364                .any(|warning| matches!(warning, Warning::UnexpectedField));
365
366            if has_unexpected_field {
367                paths.insert(&element.path);
368            }
369        }
370
371        json::PathSet::new(paths)
372    }
373
374    /// Collect the field paths of all [`Warning::MissingField`] warnings into a set of `json::Path`s.
375    pub fn missing_fields(&self) -> json::PathSet<'_> {
376        let mut paths = BTreeSet::new();
377
378        for group in self {
379            let (element, group_warnings) = group.to_parts();
380
381            let has_missing_field = group_warnings
382                .iter()
383                .any(|warning| matches!(warning, Warning::MissingField { .. }));
384
385            if has_missing_field {
386                paths.insert(&element.path);
387            }
388        }
389
390        json::PathSet::new(paths)
391    }
392
393    /// Remove all [`Warning::UnexpectedField`] warnings from the set.
394    pub fn remove_unexpected_fields(&mut self) {
395        self.retain(|warning| !matches!(warning, Warning::UnexpectedField));
396    }
397
398    /// Remove all [`Warning::MissingField`] warnings from the set.
399    pub fn remove_missing_fields(&mut self) {
400        self.retain(|warning| !matches!(warning, Warning::MissingField { .. }));
401    }
402
403    /// Remove all [`Warning::TypeMismatch`] warnings from the set.
404    pub fn remove_type_mismatches(&mut self) {
405        self.retain(|warning| !matches!(warning, Warning::TypeMismatch { .. }));
406    }
407
408    /// Remove all [`Warning::NullField`] warnings from the set.
409    pub fn remove_null_fields(&mut self) {
410        self.retain(|warning| !matches!(warning, Warning::NullField));
411    }
412
413    /// Remove all [`Warning::Cardinality`] warnings from the set.
414    pub fn remove_cardinalities(&mut self) {
415        self.retain(|warning| !matches!(warning, Warning::Cardinality { .. }));
416    }
417
418    /// Remove all [`Warning::StringTooLong`] warnings from the set.
419    pub fn remove_string_too_longs(&mut self) {
420        self.retain(|warning| !matches!(warning, Warning::StringTooLong { .. }));
421    }
422}
423
424/// Opaque-subtree marker: a value the schema does not model. The subtree is still
425/// walked so nested `null`s are reported.
426static ANY: Schema = Schema::Scalar(Scalar::Any);
427
428/// A step in the [`walk`]'s work stack.
429enum Step<'a, 'buf> {
430    /// Visit a node: record its warnings and build its leaf, or open its
431    /// object/array builder.
432    Visit {
433        elem: &'a json::Element<'buf>,
434        schema: &'a Schema,
435        slot: Slot,
436    },
437    /// Finalize the builder on top of the builder stack and route it to its parent.
438    Close { slot: Slot },
439}
440
441/// Where a built [`build::Node`] attaches within its parent.
442#[derive(Clone, Copy)]
443enum Slot {
444    /// The root value of the walk.
445    Root,
446    /// A named field of the parent object.
447    Field { name: &'static str },
448    /// An item of the parent array.
449    Item,
450    /// A value that is discarded (an unmodeled [`Scalar::Any`] subtree).
451    Ignore,
452}
453
454/// Validate `doc` against `schema` and build its intermediate representation (IR) in a
455/// single pass.
456///
457/// The returned [`build::Node`] is the value of the root [`Object`]'s [`BuilderKind`].
458/// When used through the public API the returned object will be one of the CDR or tariffs
459/// root types.
460///
461/// Building is infallible. Problems with a field emit a [`Warning`] and are stored as
462/// an [`Integrity::Err`] or [`Integrity::Missing`] on the IR object's field.
463///
464/// NOTE: A value whose type is invalid (a type mismatch) or whose key is
465/// unexpected is recorded but not descended into. Its substructure cannot
466/// be compared to the schema. Opaque [`Scalar::Any`] values are still
467/// walked, so nested `null`s are still reported for the inner JSON.
468fn walk<'a, 'buf>(
469    doc: &'a json::Document<'buf>,
470    schema: &'a Schema,
471) -> Caveat<build::Node<'buf>, Warning> {
472    let mut warnings = warning::Set::new();
473    let mut builders: Vec<build::Node<'buf>> = Vec::new();
474    let mut root = build::Node::Ignore;
475
476    // Iteration order: an object's own problems are recorded before its descendants'
477    // because its fields are scanned (emitting unexpected/missing warnings) when the
478    // object is opened, before the field `Visit`s pushed here are popped.
479    let mut stack = vec![Step::Visit {
480        elem: doc.root(),
481        schema,
482        slot: Slot::Root,
483    }];
484
485    while let Some(step) = stack.pop() {
486        match step {
487            Step::Visit { elem, schema, slot } => {
488                if let json::Value::Null = elem.value() {
489                    warnings.insert(elem, Warning::NullField);
490                    root.route_to_parent(&mut builders, slot, Integrity::Missing);
491                    continue;
492                }
493                match schema {
494                    // An unmodeled subtree: walk children only to report nested nulls.
495                    Schema::Scalar(Scalar::Any) => {
496                        enqueue_all_children(&mut stack, elem);
497                        root.route_to_parent(&mut builders, slot, Integrity::Missing);
498                    }
499                    Schema::Scalar(scalar) => {
500                        let built = check_scalar(&mut warnings, elem, *scalar);
501                        root.route_to_parent(&mut builders, slot, built);
502                    }
503                    Schema::Array { item, cardinality } => {
504                        let type_expectation = open_array(
505                            &mut stack,
506                            &mut builders,
507                            &mut warnings,
508                            elem,
509                            item,
510                            *cardinality,
511                            slot,
512                        );
513                        if type_expectation.is_type_invalid() {
514                            root.route_to_parent(&mut builders, slot, Integrity::Err);
515                        }
516                    }
517                    Schema::Object(object) => {
518                        let type_expectation = open_object(
519                            &mut stack,
520                            &mut builders,
521                            &mut warnings,
522                            elem,
523                            object,
524                            slot,
525                        );
526                        if type_expectation.is_type_invalid() {
527                            root.route_to_parent(&mut builders, slot, Integrity::Err);
528                        }
529                    }
530                }
531            }
532            Step::Close { slot } => {
533                if let Some(node) = builders.pop() {
534                    root.route_to_parent(&mut builders, slot, Integrity::Ok(node));
535                }
536            }
537        }
538    }
539
540    root.into_caveat(warnings)
541}
542
543/// Build the leaf [`build::Node`] for a scalar, recording any kind, length, enum, or
544/// string-encoded-number warning. Returns [`Integrity::Err`] for a wrong-kind value.
545fn check_scalar<'buf>(
546    warnings: &mut warning::Set<Warning>,
547    elem: &json::Element<'buf>,
548    scalar: Scalar,
549) -> Integrity<build::Node<'buf>> {
550    let expected = match scalar {
551        // Enums serialize as JSON strings; their kind check is the same as a plain
552        // string, with the value-membership check applied below.
553        Scalar::String | Scalar::StringMax(_) | Scalar::Enum(_) => json::ValueKind::String,
554        Scalar::Number => json::ValueKind::Number,
555        Scalar::Boolean => json::ValueKind::Bool,
556        // `Any` is handled by the caller; never built here.
557        Scalar::Any => return Integrity::Missing,
558    };
559
560    let actual = elem.value().kind();
561
562    // A `number` may be encoded as a JSON string; that is accepted but flagged below.
563    let string_encoded_number =
564        expected == json::ValueKind::Number && actual == json::ValueKind::String;
565    if actual != expected && !string_encoded_number {
566        warnings.insert(elem, Warning::TypeMismatch { expected, actual });
567        return Integrity::Err;
568    }
569
570    match scalar {
571        Scalar::String => Integrity::Ok(build::Node::Str(Str::new(elem.clone()))),
572        Scalar::StringMax(max) => {
573            if let Some(value) = elem.value().to_raw_str() {
574                let len = value.decode_escapes().ignore_warnings().chars().count();
575                if len > max {
576                    warnings.insert(elem, Warning::StringTooLong { max, len });
577                }
578            }
579            Integrity::Ok(build::Node::Str(Str::new(elem.clone())))
580        }
581        Scalar::Enum(variants) => {
582            let Some(value) = elem.value().to_raw_str() else {
583                return Integrity::Err;
584            };
585            let matched = variants
586                .iter()
587                .copied()
588                .find(|&(s, _)| value.eq_any_escape_aware_ignore_ascii_case(&[s]));
589            let Some((_, enum_value)) = matched else {
590                warnings.insert(
591                    elem,
592                    Warning::FieldInvalidValue {
593                        expected: variants,
594                        actual: value.as_unescaped_str().to_owned(),
595                    },
596                );
597                return Integrity::Err;
598            };
599            Integrity::Ok(build::Node::Enum(Enum::new(elem.clone(), enum_value)))
600        }
601        Scalar::Number => {
602            // OCPI permits a number to be encoded as a JSON string. The value is accepted
603            // either way; the linter can choose to flag the string-encoded form later.
604            if string_encoded_number {
605                Integrity::Ok(build::Node::Number(Number::StringEncoded(elem.clone())))
606            } else {
607                Integrity::Ok(build::Node::Number(Number::Number(elem.clone())))
608            }
609        }
610        Scalar::Boolean => Integrity::Ok(build::Node::Bool),
611        // Unreachable: handled above.
612        Scalar::Any => Integrity::Missing,
613    }
614}
615
616/// The [`open_array`] and [`open_object`] return whether the type they expected is
617/// the type they encountered.
618#[derive(Copy, Clone)]
619enum TypeExpectation {
620    Satisfied,
621    Invalid,
622}
623
624impl TypeExpectation {
625    fn is_type_invalid(self) -> bool {
626        matches!(self, Self::Invalid)
627    }
628}
629
630/// Open an object: push its builder and a [`Step::Close`], then queue its
631/// schema-matched fields. Records unexpected and missing-required-field warnings.
632/// Returns `false` (and opens nothing) if `elem` is not a JSON object.
633fn open_object<'a, 'buf>(
634    stack: &mut Vec<Step<'a, 'buf>>,
635    builders: &mut Vec<build::Node<'buf>>,
636    warnings: &mut warning::Set<Warning>,
637    elem: &'a json::Element<'buf>,
638    object: &'a Object,
639    slot: Slot,
640) -> TypeExpectation {
641    // `fields` are sorted alphabetically by `Field::name` so the `binary_search_by_key`
642    // below is valid; the `debug_assert` guards that against an out-of-order schema.
643    debug_assert!(
644        object
645            .fields
646            .windows(2)
647            .all(|pair| matches!(pair, [a, b] if a.name <= b.name)),
648        "Object::fields must be sorted alphabetically by name"
649    );
650    let json::Value::Object(fields) = elem.value() else {
651        warnings.insert(
652            elem,
653            Warning::TypeMismatch {
654                expected: json::ValueKind::Object,
655                actual: elem.value().kind(),
656            },
657        );
658        return TypeExpectation::Invalid;
659    };
660
661    builders.push(build::empty(object.kind));
662    stack.push(Step::Close { slot });
663
664    // Mark, by schema-field position, which fields the document supplies. Reusing each
665    // binary-search hit here lets the missing-field scan below be a single indexed pass
666    // instead of a linear `contains` per schema field.
667    let mut seen = vec![false; object.fields.len()];
668    for field in fields {
669        let key = field.key().as_unescaped_str();
670        let Ok(idx) = object.fields.binary_search_by_key(&key, |fd| fd.name) else {
671            // Not in the schema: record it and do not walk its subtree.
672            warnings.insert(field.element(), Warning::UnexpectedField);
673            continue;
674        };
675        if let Some(flag) = seen.get_mut(idx) {
676            *flag = true;
677        }
678        if let Some(fd) = object.fields.get(idx) {
679            stack.push(Step::Visit {
680                elem: field.element(),
681                schema: &fd.schema,
682                slot: Slot::Field { name: fd.name },
683            });
684        }
685    }
686
687    // An absent field has no element of its own to `Visit`, so it is recorded here.
688    // Every absent field is set to `Integrity::Missing`; the field's extractor then
689    // interprets that per the field's optionality (an optional field becomes
690    // `Integrity::Ok(None)`, a required field stays `Integrity::Missing`). A required
691    // field additionally records a `MissingField` warning against the parent, so its
692    // absence is visible in both the IR and the warnings.
693    for (field, &present) in object.fields.iter().zip(seen.iter()) {
694        if present {
695            continue;
696        }
697
698        if let Presence::Required = field.presence {
699            warnings.insert(elem, Warning::MissingField { name: field.name });
700        }
701        build::set_top_field(builders, field.name, Integrity::Missing);
702    }
703
704    TypeExpectation::Satisfied
705}
706
707/// Open an array: push its accumulator builder and a [`Step::Close`], then queue its
708/// items in document order. Records a cardinality warning for an empty `OneOrMore`
709/// array.
710///
711/// Returns `false` (and opens nothing) if `elem` is not a JSON array.
712fn open_array<'a, 'buf>(
713    stack: &mut Vec<Step<'a, 'buf>>,
714    builders: &mut Vec<build::Node<'buf>>,
715    warnings: &mut warning::Set<Warning>,
716    elem: &'a json::Element<'buf>,
717    item: &'a Schema,
718    cardinality: Cardinality,
719    slot: Slot,
720) -> TypeExpectation {
721    let json::Value::Array(items) = elem.value() else {
722        warnings.insert(
723            elem,
724            Warning::TypeMismatch {
725                expected: json::ValueKind::Array,
726                actual: elem.value().kind(),
727            },
728        );
729        return TypeExpectation::Invalid;
730    };
731
732    if cardinality == Cardinality::OneOrMore && items.is_empty() {
733        warnings.insert(
734            elem,
735            Warning::Cardinality {
736                expected: cardinality,
737                len: 0,
738            },
739        );
740    }
741
742    builders.push(build::Node::Array(Vec::with_capacity(items.len())));
743    stack.push(Step::Close { slot });
744
745    // Push in reverse so items are visited, and accumulated, in document order.
746    for child in items.iter().rev() {
747        stack.push(Step::Visit {
748            elem: child,
749            schema: item,
750            slot: Slot::Item,
751        });
752    }
753
754    TypeExpectation::Satisfied
755}
756
757/// Queue the children of an opaque [`Scalar::Any`] element so nested `null`s are
758/// still reported. Their values are discarded.
759fn enqueue_all_children<'a, 'buf>(stack: &mut Vec<Step<'a, 'buf>>, elem: &'a json::Element<'buf>) {
760    match elem.value() {
761        json::Value::Array(items) => {
762            for child in items.iter().rev() {
763                stack.push(Step::Visit {
764                    elem: child,
765                    schema: &ANY,
766                    slot: Slot::Ignore,
767                });
768            }
769        }
770        json::Value::Object(fields) => {
771            for field in fields.iter().rev() {
772                stack.push(Step::Visit {
773                    elem: field.element(),
774                    schema: &ANY,
775                    slot: Slot::Ignore,
776                });
777            }
778        }
779        json::Value::Null
780        | json::Value::True
781        | json::Value::False
782        | json::Value::String(_)
783        | json::Value::Number(_) => {}
784    }
785}
786
787// Constrained leaf types for the schema intermediate representation (IR).
788//
789// A leaf wraps a [`json::Element`] that the IR builder has already confirmed to be
790// the right JSON kind. Downstream lowering (the `FromSchema` impls) therefore does
791// not repeat the kind check; it only does semantic interpretation (parsing a number
792// into a `Decimal`, validating an ISO currency code, and so on).
793//
794// The leaves keep a (cheap, reference-counted) clone of their [`json::Element`] so
795// the lowering step can still attach its semantic warnings to the right path.
796
797/// A JSON value the builder confirmed to be a string.
798///
799/// Length and other lexical checks are applied by the builder when the leaf is
800/// constructed; see [`crate::schema::build`].
801#[derive(Clone, Debug)]
802pub(crate) struct Str<'buf> {
803    elem: json::Element<'buf>,
804}
805
806impl<'buf> Str<'buf> {
807    pub(super) fn new(elem: json::Element<'buf>) -> Self {
808        Self { elem }
809    }
810
811    /// The element this leaf was built from; the lowering step parses its value and
812    /// attaches any semantic warnings to it.
813    #[allow(dead_code, reason = "Will be used in FromSchema integration PR")]
814    pub fn element(&self) -> &json::Element<'buf> {
815        &self.elem
816    }
817}
818
819/// A JSON value the builder confirmed to be a number, remembering whether it was
820/// written as a JSON number or encoded as a JSON string.
821///
822/// OCPI allows a `number` to be encoded as a string; the [`Number::StringEncoded`]
823/// variant records that so the builder can flag it and the lowering step can still
824/// read the digits.
825#[derive(Clone, Debug)]
826pub(crate) enum Number<'buf> {
827    /// A JSON number literal.
828    Number(json::Element<'buf>),
829    /// A number encoded as a JSON string.
830    StringEncoded(json::Element<'buf>),
831}
832
833#[expect(dead_code, reason = "For use in future `FromSchema` trait")]
834impl<'buf> Number<'buf> {
835    /// The element this leaf was built from; the lowering step parses its value and
836    /// attaches any semantic warnings to it.
837    pub fn element(&self) -> &json::Element<'buf> {
838        match self {
839            Self::Number(elem) | Self::StringEncoded(elem) => elem,
840        }
841    }
842}
843
844/// A JSON string the builder confirmed to be one of an enum's permitted variants.
845///
846/// The builder resolves the string to the [`EnumValue`] it denotes while walking,
847/// so the lowering step reads a typed Rust enum directly and never re-parses the
848/// string or repeats the membership check.
849#[derive(Clone, Debug)]
850pub(crate) struct Enum<'buf> {
851    elem: json::Element<'buf>,
852    value: EnumValue,
853}
854
855#[expect(dead_code, reason = "For use in future `FromSchema` trait")]
856impl<'buf> Enum<'buf> {
857    pub fn new(elem: json::Element<'buf>, value: EnumValue) -> Self {
858        Self { elem, value }
859    }
860
861    /// The element this leaf was built from. Used to attach semantic warnings.
862    pub fn element(&self) -> &json::Element<'buf> {
863        &self.elem
864    }
865
866    /// The typed OCPI enum the value resolved to.
867    pub fn value(&self) -> EnumValue {
868        self.value
869    }
870
871    /// The spec value of the wrapped variant.
872    pub fn canonical(&self) -> &'static str {
873        self.value.canonical()
874    }
875}
876
877/// The typed value of any OCPI enum that appears in the CDR/Tariff graph, across
878/// both supported spec versions.
879///
880/// Each variant wraps the version-specific Rust enum (the variants, and in some
881/// cases the variant set, differ between 2.1.1 and 2.2.1).
882#[derive(Clone, Copy, Debug, PartialEq, Eq)]
883pub enum EnumValue {
884    V211Capability(v211::Capability),
885    V211ConnectorFormat(v211::ConnectorFormat),
886    V211PowerType(v211::PowerType),
887    V211ConnectorType(v211::ConnectorType),
888    V211ParkingRestriction(v211::ParkingRestriction),
889    V211Status(v211::Status),
890    V211Facility(v211::Facility),
891    V211LocationType(v211::LocationType),
892    V211AuthMethod(v211::AuthMethod),
893    V211CdrDimensionType(v211::CdrDimensionType),
894    V211TariffDimensionType(v211::TariffDimensionType),
895    V211DayOfWeek(v211::DayOfWeek),
896    V211EnergySourceCategory(v211::EnergySourceCategory),
897    V211EnvironmentalImpactCategory(v211::EnvironmentalImpactCategory),
898    V221TokenType(v221::TokenType),
899    V221ConnectorFormat(v221::ConnectorFormat),
900    V221PowerType(v221::PowerType),
901    V221ConnectorType(v221::ConnectorType),
902    V221AuthMethod(v221::AuthMethod),
903    V221CdrDimensionType(v221::CdrDimensionType),
904    V221TariffDimensionType(v221::TariffDimensionType),
905    V221DayOfWeek(v221::DayOfWeek),
906    V221ReservationRestrictionType(v221::ReservationRestrictionType),
907    V221TariffType(v221::TariffType),
908    V221EnergySourceCategory(v221::EnergySourceCategory),
909    V221EnvironmentalImpactCategory(v221::EnvironmentalImpactCategory),
910}
911
912impl EnumValue {
913    /// The spec value of the wrapped variant.
914    #[allow(dead_code, reason = "For use in future `FromSchema` trait")]
915    fn canonical(self) -> &'static str {
916        match self {
917            Self::V211Capability(value) => value.canonical(),
918            Self::V211ConnectorFormat(value) => value.canonical(),
919            Self::V211PowerType(value) => value.canonical(),
920            Self::V211ConnectorType(value) => value.canonical(),
921            Self::V211ParkingRestriction(value) => value.canonical(),
922            Self::V211Status(value) => value.canonical(),
923            Self::V211Facility(value) => value.canonical(),
924            Self::V211LocationType(value) => value.canonical(),
925            Self::V211AuthMethod(value) => value.canonical(),
926            Self::V211CdrDimensionType(value) => value.canonical(),
927            Self::V211TariffDimensionType(value) => value.canonical(),
928            Self::V211DayOfWeek(value) => value.canonical(),
929            Self::V211EnergySourceCategory(value) => value.canonical(),
930            Self::V211EnvironmentalImpactCategory(value) => value.canonical(),
931            Self::V221TokenType(value) => value.canonical(),
932            Self::V221ConnectorFormat(value) => value.canonical(),
933            Self::V221PowerType(value) => value.canonical(),
934            Self::V221ConnectorType(value) => value.canonical(),
935            Self::V221AuthMethod(value) => value.canonical(),
936            Self::V221CdrDimensionType(value) => value.canonical(),
937            Self::V221TariffDimensionType(value) => value.canonical(),
938            Self::V221DayOfWeek(value) => value.canonical(),
939            Self::V221ReservationRestrictionType(value) => value.canonical(),
940            Self::V221TariffType(value) => value.canonical(),
941            Self::V221EnergySourceCategory(value) => value.canonical(),
942            Self::V221EnvironmentalImpactCategory(value) => value.canonical(),
943        }
944    }
945}
946
947/// Define an OCPI enum: its Rust type, the variant table used by [`Scalar::Enum`],
948/// and the spec value of each variant.
949///
950/// The first argument is the [`EnumValue`] variant that wraps this type in the
951/// umbrella value. The body lists each Rust variant with the exact spec value it
952/// serializes to. `VARIANTS` (the value-to-[`EnumValue`] table) and `canonical`
953/// ([`EnumValue`]-to-value) are generated from that single list, so the two
954/// mappings cannot drift.
955macro_rules! ocpi_enum {
956    ($wrapper:ident, $kind:ident { $($variant:ident = $value:literal),+ $(,)? }) => {
957        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
958        pub enum $kind {
959            $($variant),+
960        }
961
962        impl $kind {
963            /// The permitted variants - the spec requires uppercase - paired with
964            /// the `EnumValue` each denotes. Used as the `Scalar::Enum` table.
965            const VARIANTS: &'static [(&'static str, super::EnumValue)] = &[
966                $(($value, super::EnumValue::$wrapper(Self::$variant))),+
967            ];
968
969            /// The spec variant in uppercase.
970            #[allow(dead_code, reason = "For use in future `FromSchema` trait")]
971            pub(crate) fn canonical(self) -> &'static str {
972                match self {
973                    $(Self::$variant => $value),+
974                }
975            }
976        }
977    };
978}
979pub(crate) use ocpi_enum;