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