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}