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