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;