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