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#[derive(Clone, Copy)]
20enum Schema {
21 Scalar(Scalar),
23 Object(&'static Object),
25 Array {
28 item: &'static Schema,
29 cardinality: Cardinality,
30 },
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum Cardinality {
36 ZeroOrMore,
38 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53enum Scalar {
54 String,
60 StringMax(usize),
64 Enum(&'static [(&'static str, EnumValue)]),
70 Number,
72 Boolean,
74 Any,
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum Integrity<T> {
91 Ok(T),
93 Missing,
96 Err,
99}
100
101#[allow(
105 clippy::derivable_impls,
106 reason = "derive would add an unwanted T: Default bound"
107)]
108impl<T> Default for Integrity<T> {
109 fn default() -> Self {
110 Self::Missing
111 }
112}
113
114impl<T> Integrity<T> {
115 pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Integrity<U> {
117 match self {
118 Integrity::Ok(value) => Integrity::Ok(op(value)),
119 Integrity::Missing => Integrity::Missing,
120 Integrity::Err => Integrity::Err,
121 }
122 }
123
124 pub fn as_ref(&self) -> Integrity<&T> {
126 match self {
127 Integrity::Ok(value) => Integrity::Ok(value),
128 Integrity::Missing => Integrity::Missing,
129 Integrity::Err => Integrity::Err,
130 }
131 }
132
133 pub fn ok(self) -> Option<T> {
135 match self {
136 Integrity::Ok(value) => Some(value),
137 Integrity::Missing | Integrity::Err => None,
138 }
139 }
140}
141
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145enum BuilderKind {
146 Ignore,
148 V221Tariff,
149 V221Element,
150 V221PriceComponent,
151 V221Restrictions,
152 V221Price,
153 V221Cdr,
154 V221ChargingPeriod,
155 V221CdrDimension,
156 V211Tariff,
157 V211Element,
158 V211PriceComponent,
159 V211Restrictions,
160 V211Cdr,
161 V211ChargingPeriod,
162 V211CdrDimension,
163}
164
165#[derive(Clone, Copy)]
167struct Object {
168 fields: &'static [Field],
169 kind: BuilderKind,
171}
172
173#[derive(Clone, Copy)]
175struct Field {
176 name: &'static str,
180 presence: Presence,
182 schema: Schema,
184}
185
186impl Field {
187 const fn required(name: &'static str, scalar: Scalar) -> Self {
189 Self {
190 name,
191 presence: Presence::Required,
192 schema: Schema::Scalar(scalar),
193 }
194 }
195
196 const fn required_array(name: &'static str, item: &'static Schema) -> Self {
198 Self {
199 name,
200 presence: Presence::Required,
201 schema: Schema::Array {
202 item,
203 cardinality: Cardinality::OneOrMore,
204 },
205 }
206 }
207
208 const fn required_object(name: &'static str, schema: &'static Object) -> Self {
210 Self {
211 name,
212 presence: Presence::Required,
213 schema: Schema::Object(schema),
214 }
215 }
216
217 const fn optional(name: &'static str, scalar: Scalar) -> Self {
219 Self {
220 name,
221 presence: Presence::Optional,
222 schema: Schema::Scalar(scalar),
223 }
224 }
225
226 const fn optional_array(name: &'static str, item: &'static Schema) -> Self {
228 Self {
229 name,
230 presence: Presence::Optional,
231 schema: Schema::Array {
232 item,
233 cardinality: Cardinality::ZeroOrMore,
234 },
235 }
236 }
237
238 const fn optional_object(name: &'static str, schema: &'static Object) -> Self {
240 Self {
241 name,
242 presence: Presence::Optional,
243 schema: Schema::Object(schema),
244 }
245 }
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
250pub enum Presence {
251 Required,
254 Optional,
256}
257
258#[derive(Clone, Debug, PartialEq, Eq)]
260pub enum Warning {
261 UnexpectedField,
263 MissingField {
265 name: &'static str,
267 },
268 NullField,
270 TypeMismatch {
272 expected: json::ValueKind,
274 actual: json::ValueKind,
276 },
277 StringTooLong {
279 max: usize,
281 len: usize,
283 },
284 FieldInvalidValue {
286 expected: &'static [(&'static str, EnumValue)],
288 actual: String,
290 },
291 Cardinality {
293 expected: Cardinality,
295 len: usize,
297 },
298}
299
300impl crate::Warning for Warning {
301 fn id(&self) -> warning::Id {
302 match self {
303 Self::UnexpectedField => warning::Id::from_static("unexpected_field"),
304 Self::MissingField { name } => {
305 warning::Id::from_string(format!("missing_field({name})"))
306 }
307 Self::NullField => warning::Id::from_static("null_field"),
308 Self::TypeMismatch { actual, .. } => {
309 warning::Id::from_string(format!("invalid_type({actual})"))
310 }
311 Self::StringTooLong { .. } => warning::Id::from_static("string_too_long"),
312 Self::FieldInvalidValue { actual, .. } => {
313 warning::Id::from_string(format!("field_invalid_value({actual})"))
314 }
315 Self::Cardinality { expected, .. } => {
316 warning::Id::from_string(format!("cardinality({expected})"))
317 }
318 }
319 }
320}
321
322impl std::fmt::Display for Warning {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 match self {
325 Self::UnexpectedField => f.write_str("field is not part of the schema"),
326 Self::MissingField { name } => write!(f, "required field `{name}` is missing"),
327 Self::NullField => f.write_str(
328 "field is `null`. `null` fields have no semantic meaning for OCPI objects",
329 ),
330 Self::TypeMismatch { expected, actual } => {
331 write!(f, "expected {expected} found {actual}")
332 }
333 Self::StringTooLong { max, len } => {
334 write!(
335 f,
336 "string is `{len}` characters, but the maximum allowed is `{max}`"
337 )
338 }
339 Self::FieldInvalidValue { expected, actual } => {
340 let variants: Vec<&str> = expected.iter().map(|(variant, _)| *variant).collect();
341 write!(
342 f,
343 "value `{actual}` is not one of the permitted values: {}",
344 variants.join(", ")
345 )
346 }
347 Self::Cardinality { expected, len } => {
348 write!(f, "expected {expected} elements, found {len}")
349 }
350 }
351 }
352}
353
354impl warning::Set<Warning> {
355 pub fn unexpected_fields(&self) -> json::PathSet<'_> {
357 let mut paths = BTreeSet::new();
358
359 for group in self {
360 let (element, group_warnings) = group.to_parts();
361
362 let has_unexpected_field = group_warnings
363 .iter()
364 .any(|warning| matches!(warning, Warning::UnexpectedField));
365
366 if has_unexpected_field {
367 paths.insert(&element.path);
368 }
369 }
370
371 json::PathSet::new(paths)
372 }
373
374 pub fn missing_fields(&self) -> json::PathSet<'_> {
376 let mut paths = BTreeSet::new();
377
378 for group in self {
379 let (element, group_warnings) = group.to_parts();
380
381 let has_missing_field = group_warnings
382 .iter()
383 .any(|warning| matches!(warning, Warning::MissingField { .. }));
384
385 if has_missing_field {
386 paths.insert(&element.path);
387 }
388 }
389
390 json::PathSet::new(paths)
391 }
392
393 pub fn remove_unexpected_fields(&mut self) {
395 self.retain(|warning| !matches!(warning, Warning::UnexpectedField));
396 }
397
398 pub fn remove_missing_fields(&mut self) {
400 self.retain(|warning| !matches!(warning, Warning::MissingField { .. }));
401 }
402
403 pub fn remove_type_mismatches(&mut self) {
405 self.retain(|warning| !matches!(warning, Warning::TypeMismatch { .. }));
406 }
407
408 pub fn remove_null_fields(&mut self) {
410 self.retain(|warning| !matches!(warning, Warning::NullField));
411 }
412
413 pub fn remove_cardinalities(&mut self) {
415 self.retain(|warning| !matches!(warning, Warning::Cardinality { .. }));
416 }
417
418 pub fn remove_string_too_longs(&mut self) {
420 self.retain(|warning| !matches!(warning, Warning::StringTooLong { .. }));
421 }
422}
423
424static ANY: Schema = Schema::Scalar(Scalar::Any);
427
428enum Step<'a, 'buf> {
430 Visit {
433 elem: &'a json::Element<'buf>,
434 schema: &'a Schema,
435 slot: Slot,
436 },
437 Close { slot: Slot },
439}
440
441#[derive(Clone, Copy)]
443enum Slot {
444 Root,
446 Field { name: &'static str },
448 Item,
450 Ignore,
452}
453
454fn walk<'a, 'buf>(
469 doc: &'a json::Document<'buf>,
470 schema: &'a Schema,
471) -> Caveat<build::Node<'buf>, Warning> {
472 let mut warnings = warning::Set::new();
473 let mut builders: Vec<build::Node<'buf>> = Vec::new();
474 let mut root = build::Node::Ignore;
475
476 let mut stack = vec![Step::Visit {
480 elem: doc.root(),
481 schema,
482 slot: Slot::Root,
483 }];
484
485 while let Some(step) = stack.pop() {
486 match step {
487 Step::Visit { elem, schema, slot } => {
488 if let json::Value::Null = elem.value() {
489 warnings.insert(elem, Warning::NullField);
490 root.route_to_parent(&mut builders, slot, Integrity::Missing);
491 continue;
492 }
493 match schema {
494 Schema::Scalar(Scalar::Any) => {
496 enqueue_all_children(&mut stack, elem);
497 root.route_to_parent(&mut builders, slot, Integrity::Missing);
498 }
499 Schema::Scalar(scalar) => {
500 let built = check_scalar(&mut warnings, elem, *scalar);
501 root.route_to_parent(&mut builders, slot, built);
502 }
503 Schema::Array { item, cardinality } => {
504 let type_expectation = open_array(
505 &mut stack,
506 &mut builders,
507 &mut warnings,
508 elem,
509 item,
510 *cardinality,
511 slot,
512 );
513 if type_expectation.is_type_invalid() {
514 root.route_to_parent(&mut builders, slot, Integrity::Err);
515 }
516 }
517 Schema::Object(object) => {
518 let type_expectation = open_object(
519 &mut stack,
520 &mut builders,
521 &mut warnings,
522 elem,
523 object,
524 slot,
525 );
526 if type_expectation.is_type_invalid() {
527 root.route_to_parent(&mut builders, slot, Integrity::Err);
528 }
529 }
530 }
531 }
532 Step::Close { slot } => {
533 if let Some(node) = builders.pop() {
534 root.route_to_parent(&mut builders, slot, Integrity::Ok(node));
535 }
536 }
537 }
538 }
539
540 root.into_caveat(warnings)
541}
542
543fn check_scalar<'buf>(
546 warnings: &mut warning::Set<Warning>,
547 elem: &json::Element<'buf>,
548 scalar: Scalar,
549) -> Integrity<build::Node<'buf>> {
550 let expected = match scalar {
551 Scalar::String | Scalar::StringMax(_) | Scalar::Enum(_) => json::ValueKind::String,
554 Scalar::Number => json::ValueKind::Number,
555 Scalar::Boolean => json::ValueKind::Bool,
556 Scalar::Any => return Integrity::Missing,
558 };
559
560 let actual = elem.value().kind();
561
562 let string_encoded_number =
564 expected == json::ValueKind::Number && actual == json::ValueKind::String;
565 if actual != expected && !string_encoded_number {
566 warnings.insert(elem, Warning::TypeMismatch { expected, actual });
567 return Integrity::Err;
568 }
569
570 match scalar {
571 Scalar::String => Integrity::Ok(build::Node::Str(Str::new(elem.clone()))),
572 Scalar::StringMax(max) => {
573 if let Some(value) = elem.value().to_raw_str() {
574 let len = value.decode_escapes().ignore_warnings().chars().count();
575 if len > max {
576 warnings.insert(elem, Warning::StringTooLong { max, len });
577 }
578 }
579 Integrity::Ok(build::Node::Str(Str::new(elem.clone())))
580 }
581 Scalar::Enum(variants) => {
582 let Some(value) = elem.value().to_raw_str() else {
583 return Integrity::Err;
584 };
585 let matched = variants
586 .iter()
587 .copied()
588 .find(|&(s, _)| value.eq_any_escape_aware_ignore_ascii_case(&[s]));
589 let Some((_, enum_value)) = matched else {
590 warnings.insert(
591 elem,
592 Warning::FieldInvalidValue {
593 expected: variants,
594 actual: value.as_unescaped_str().to_owned(),
595 },
596 );
597 return Integrity::Err;
598 };
599 Integrity::Ok(build::Node::Enum(Enum::new(elem.clone(), enum_value)))
600 }
601 Scalar::Number => {
602 if string_encoded_number {
605 Integrity::Ok(build::Node::Number(Number::StringEncoded(elem.clone())))
606 } else {
607 Integrity::Ok(build::Node::Number(Number::Number(elem.clone())))
608 }
609 }
610 Scalar::Boolean => Integrity::Ok(build::Node::Bool),
611 Scalar::Any => Integrity::Missing,
613 }
614}
615
616#[derive(Copy, Clone)]
619enum TypeExpectation {
620 Satisfied,
621 Invalid,
622}
623
624impl TypeExpectation {
625 fn is_type_invalid(self) -> bool {
626 matches!(self, Self::Invalid)
627 }
628}
629
630fn open_object<'a, 'buf>(
634 stack: &mut Vec<Step<'a, 'buf>>,
635 builders: &mut Vec<build::Node<'buf>>,
636 warnings: &mut warning::Set<Warning>,
637 elem: &'a json::Element<'buf>,
638 object: &'a Object,
639 slot: Slot,
640) -> TypeExpectation {
641 debug_assert!(
644 object
645 .fields
646 .windows(2)
647 .all(|pair| matches!(pair, [a, b] if a.name <= b.name)),
648 "Object::fields must be sorted alphabetically by name"
649 );
650 let json::Value::Object(fields) = elem.value() else {
651 warnings.insert(
652 elem,
653 Warning::TypeMismatch {
654 expected: json::ValueKind::Object,
655 actual: elem.value().kind(),
656 },
657 );
658 return TypeExpectation::Invalid;
659 };
660
661 builders.push(build::empty(object.kind));
662 stack.push(Step::Close { slot });
663
664 let mut seen = vec![false; object.fields.len()];
668 for field in fields {
669 let key = field.key().as_unescaped_str();
670 let Ok(idx) = object.fields.binary_search_by_key(&key, |fd| fd.name) else {
671 warnings.insert(field.element(), Warning::UnexpectedField);
673 continue;
674 };
675 if let Some(flag) = seen.get_mut(idx) {
676 *flag = true;
677 }
678 if let Some(fd) = object.fields.get(idx) {
679 stack.push(Step::Visit {
680 elem: field.element(),
681 schema: &fd.schema,
682 slot: Slot::Field { name: fd.name },
683 });
684 }
685 }
686
687 for (field, &present) in object.fields.iter().zip(seen.iter()) {
694 if present {
695 continue;
696 }
697
698 if let Presence::Required = field.presence {
699 warnings.insert(elem, Warning::MissingField { name: field.name });
700 }
701 build::set_top_field(builders, field.name, Integrity::Missing);
702 }
703
704 TypeExpectation::Satisfied
705}
706
707fn open_array<'a, 'buf>(
713 stack: &mut Vec<Step<'a, 'buf>>,
714 builders: &mut Vec<build::Node<'buf>>,
715 warnings: &mut warning::Set<Warning>,
716 elem: &'a json::Element<'buf>,
717 item: &'a Schema,
718 cardinality: Cardinality,
719 slot: Slot,
720) -> TypeExpectation {
721 let json::Value::Array(items) = elem.value() else {
722 warnings.insert(
723 elem,
724 Warning::TypeMismatch {
725 expected: json::ValueKind::Array,
726 actual: elem.value().kind(),
727 },
728 );
729 return TypeExpectation::Invalid;
730 };
731
732 if cardinality == Cardinality::OneOrMore && items.is_empty() {
733 warnings.insert(
734 elem,
735 Warning::Cardinality {
736 expected: cardinality,
737 len: 0,
738 },
739 );
740 }
741
742 builders.push(build::Node::Array(Vec::with_capacity(items.len())));
743 stack.push(Step::Close { slot });
744
745 for child in items.iter().rev() {
747 stack.push(Step::Visit {
748 elem: child,
749 schema: item,
750 slot: Slot::Item,
751 });
752 }
753
754 TypeExpectation::Satisfied
755}
756
757fn enqueue_all_children<'a, 'buf>(stack: &mut Vec<Step<'a, 'buf>>, elem: &'a json::Element<'buf>) {
760 match elem.value() {
761 json::Value::Array(items) => {
762 for child in items.iter().rev() {
763 stack.push(Step::Visit {
764 elem: child,
765 schema: &ANY,
766 slot: Slot::Ignore,
767 });
768 }
769 }
770 json::Value::Object(fields) => {
771 for field in fields.iter().rev() {
772 stack.push(Step::Visit {
773 elem: field.element(),
774 schema: &ANY,
775 slot: Slot::Ignore,
776 });
777 }
778 }
779 json::Value::Null
780 | json::Value::True
781 | json::Value::False
782 | json::Value::String(_)
783 | json::Value::Number(_) => {}
784 }
785}
786
787#[derive(Clone, Debug)]
802pub(crate) struct Str<'buf> {
803 elem: json::Element<'buf>,
804}
805
806impl<'buf> Str<'buf> {
807 pub(super) fn new(elem: json::Element<'buf>) -> Self {
808 Self { elem }
809 }
810
811 #[allow(dead_code, reason = "Will be used in FromSchema integration PR")]
814 pub fn element(&self) -> &json::Element<'buf> {
815 &self.elem
816 }
817}
818
819#[derive(Clone, Debug)]
826pub(crate) enum Number<'buf> {
827 Number(json::Element<'buf>),
829 StringEncoded(json::Element<'buf>),
831}
832
833#[expect(dead_code, reason = "For use in future `FromSchema` trait")]
834impl<'buf> Number<'buf> {
835 pub fn element(&self) -> &json::Element<'buf> {
838 match self {
839 Self::Number(elem) | Self::StringEncoded(elem) => elem,
840 }
841 }
842}
843
844#[derive(Clone, Debug)]
850pub(crate) struct Enum<'buf> {
851 elem: json::Element<'buf>,
852 value: EnumValue,
853}
854
855#[expect(dead_code, reason = "For use in future `FromSchema` trait")]
856impl<'buf> Enum<'buf> {
857 pub fn new(elem: json::Element<'buf>, value: EnumValue) -> Self {
858 Self { elem, value }
859 }
860
861 pub fn element(&self) -> &json::Element<'buf> {
863 &self.elem
864 }
865
866 pub fn value(&self) -> EnumValue {
868 self.value
869 }
870
871 pub fn canonical(&self) -> &'static str {
873 self.value.canonical()
874 }
875}
876
877#[derive(Clone, Copy, Debug, PartialEq, Eq)]
883pub enum EnumValue {
884 V211Capability(v211::Capability),
885 V211ConnectorFormat(v211::ConnectorFormat),
886 V211PowerType(v211::PowerType),
887 V211ConnectorType(v211::ConnectorType),
888 V211ParkingRestriction(v211::ParkingRestriction),
889 V211Status(v211::Status),
890 V211Facility(v211::Facility),
891 V211LocationType(v211::LocationType),
892 V211AuthMethod(v211::AuthMethod),
893 V211CdrDimensionType(v211::CdrDimensionType),
894 V211TariffDimensionType(v211::TariffDimensionType),
895 V211DayOfWeek(v211::DayOfWeek),
896 V211EnergySourceCategory(v211::EnergySourceCategory),
897 V211EnvironmentalImpactCategory(v211::EnvironmentalImpactCategory),
898 V221TokenType(v221::TokenType),
899 V221ConnectorFormat(v221::ConnectorFormat),
900 V221PowerType(v221::PowerType),
901 V221ConnectorType(v221::ConnectorType),
902 V221AuthMethod(v221::AuthMethod),
903 V221CdrDimensionType(v221::CdrDimensionType),
904 V221TariffDimensionType(v221::TariffDimensionType),
905 V221DayOfWeek(v221::DayOfWeek),
906 V221ReservationRestrictionType(v221::ReservationRestrictionType),
907 V221TariffType(v221::TariffType),
908 V221EnergySourceCategory(v221::EnergySourceCategory),
909 V221EnvironmentalImpactCategory(v221::EnvironmentalImpactCategory),
910}
911
912impl EnumValue {
913 #[allow(dead_code, reason = "For use in future `FromSchema` trait")]
915 fn canonical(self) -> &'static str {
916 match self {
917 Self::V211Capability(value) => value.canonical(),
918 Self::V211ConnectorFormat(value) => value.canonical(),
919 Self::V211PowerType(value) => value.canonical(),
920 Self::V211ConnectorType(value) => value.canonical(),
921 Self::V211ParkingRestriction(value) => value.canonical(),
922 Self::V211Status(value) => value.canonical(),
923 Self::V211Facility(value) => value.canonical(),
924 Self::V211LocationType(value) => value.canonical(),
925 Self::V211AuthMethod(value) => value.canonical(),
926 Self::V211CdrDimensionType(value) => value.canonical(),
927 Self::V211TariffDimensionType(value) => value.canonical(),
928 Self::V211DayOfWeek(value) => value.canonical(),
929 Self::V211EnergySourceCategory(value) => value.canonical(),
930 Self::V211EnvironmentalImpactCategory(value) => value.canonical(),
931 Self::V221TokenType(value) => value.canonical(),
932 Self::V221ConnectorFormat(value) => value.canonical(),
933 Self::V221PowerType(value) => value.canonical(),
934 Self::V221ConnectorType(value) => value.canonical(),
935 Self::V221AuthMethod(value) => value.canonical(),
936 Self::V221CdrDimensionType(value) => value.canonical(),
937 Self::V221TariffDimensionType(value) => value.canonical(),
938 Self::V221DayOfWeek(value) => value.canonical(),
939 Self::V221ReservationRestrictionType(value) => value.canonical(),
940 Self::V221TariffType(value) => value.canonical(),
941 Self::V221EnergySourceCategory(value) => value.canonical(),
942 Self::V221EnvironmentalImpactCategory(value) => value.canonical(),
943 }
944 }
945}
946
947macro_rules! ocpi_enum {
956 ($wrapper:ident, $kind:ident { $($variant:ident = $value:literal),+ $(,)? }) => {
957 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
958 pub enum $kind {
959 $($variant),+
960 }
961
962 impl $kind {
963 const VARIANTS: &'static [(&'static str, super::EnumValue)] = &[
966 $(($value, super::EnumValue::$wrapper(Self::$variant))),+
967 ];
968
969 #[allow(dead_code, reason = "For use in future `FromSchema` trait")]
971 pub(crate) fn canonical(self) -> &'static str {
972 match self {
973 $(Self::$variant => $value),+
974 }
975 }
976 }
977 };
978}
979pub(crate) use ocpi_enum;