Skip to main content

ocpi_tariffs/schema/
v221.rs

1#[cfg(test)]
2mod tests;
3
4#[cfg(test)]
5mod tests_ir;
6
7#[cfg(test)]
8mod tests_cdr_ir;
9
10use super::{
11    build, ocpi_enum, BuilderKind, Enum, Field, Integrity, Number, Object, Scalar, Schema, Str,
12    Warning,
13};
14
15use crate::{json, Caveat};
16
17// Maximum character lengths for OCPI string fields, as declared in the spec
18// via the `string(n)` / `CiString(n)` notation.
19
20// -- Identifiers ---------------------------------------------------------
21const MAX_LEN_IDENTIFIER: usize = 36;
22const MAX_LEN_LONG_IDENTIFIER: usize = 39;
23const MAX_LEN_EVSE_ID: usize = 48;
24
25// -- Codes ---------------------------------------------------------------
26const MAX_LEN_CURRENCY: usize = 3;
27const MAX_LEN_PARTY_ID: usize = 3;
28const MAX_LEN_COUNTRY: usize = 3;
29const MAX_LEN_COUNTRY_CODE: usize = 2;
30const MAX_LEN_LANGUAGE: usize = 2;
31
32// -- Geographic and address ----------------------------------------------
33const MAX_LEN_LATITUDE: usize = 10;
34const MAX_LEN_LONGITUDE: usize = 11;
35const MAX_LEN_POSTAL_CODE: usize = 10;
36const MAX_LEN_ADDRESS: usize = 45;
37const MAX_LEN_CITY: usize = 45;
38const MAX_LEN_STATE: usize = 20;
39
40// -- Free text and names -------------------------------------------------
41const MAX_LEN_NAME: usize = 255;
42const MAX_LEN_URL: usize = 255;
43const MAX_LEN_METER_ID: usize = 255;
44const MAX_LEN_REMARK: usize = 255;
45const MAX_LEN_ENERGY_MIX_NAME: usize = 64;
46const MAX_LEN_DISPLAY_TEXT: usize = 512;
47
48// -- Signed data ---------------------------------------------------------
49const MAX_LEN_NATURE: usize = 32;
50const MAX_LEN_PLAIN_DATA: usize = 512;
51const MAX_LEN_SIGNED_DATA: usize = 5000;
52const MAX_LEN_PUBLIC_KEY: usize = 512;
53const MAX_LEN_SIGNED_DATA_URL: usize = 512;
54
55// -- Enum value sets ---------------------------------------------------------
56// A Rust enum for each OCPI 2.2.1 enum that appears in the CDR/Tariff graph.
57// The values are matched case-insensitively. Each `ocpi_enum!`
58// generates the `VARIANTS` table used by `Scalar::Enum`.
59
60// `TokenType` (mod_tokens).
61ocpi_enum!(TokenType {
62    AdHocUser = "AD_HOC_USER",
63    AppUser = "APP_USER",
64    Other = "OTHER",
65    Rfid = "RFID",
66});
67
68// `ConnectorFormat` (mod_locations).
69ocpi_enum!(ConnectorFormat {
70    Socket = "SOCKET",
71    Cable = "CABLE",
72});
73
74// `PowerType` (mod_locations).
75ocpi_enum!(PowerType {
76    Ac1Phase = "AC_1_PHASE",
77    Ac2Phase = "AC_2_PHASE",
78    Ac2PhaseSplit = "AC_2_PHASE_SPLIT",
79    Ac3Phase = "AC_3_PHASE",
80    Dc = "DC",
81});
82
83// `ConnectorType` (mod_locations).
84ocpi_enum!(ConnectorType {
85    Chademo = "CHADEMO",
86    Chaoji = "CHAOJI",
87    DomesticA = "DOMESTIC_A",
88    DomesticB = "DOMESTIC_B",
89    DomesticC = "DOMESTIC_C",
90    DomesticD = "DOMESTIC_D",
91    DomesticE = "DOMESTIC_E",
92    DomesticF = "DOMESTIC_F",
93    DomesticG = "DOMESTIC_G",
94    DomesticH = "DOMESTIC_H",
95    DomesticI = "DOMESTIC_I",
96    DomesticJ = "DOMESTIC_J",
97    DomesticK = "DOMESTIC_K",
98    DomesticL = "DOMESTIC_L",
99    DomesticM = "DOMESTIC_M",
100    DomesticN = "DOMESTIC_N",
101    DomesticO = "DOMESTIC_O",
102    GbtAc = "GBT_AC",
103    GbtDc = "GBT_DC",
104    Iec603092Single16 = "IEC_60309_2_single_16",
105    Iec603092Three16 = "IEC_60309_2_three_16",
106    Iec603092Three32 = "IEC_60309_2_three_32",
107    Iec603092Three64 = "IEC_60309_2_three_64",
108    Iec62196T1 = "IEC_62196_T1",
109    Iec62196T1Combo = "IEC_62196_T1_COMBO",
110    Iec62196T2 = "IEC_62196_T2",
111    Iec62196T2Combo = "IEC_62196_T2_COMBO",
112    Iec62196T3A = "IEC_62196_T3A",
113    Iec62196T3C = "IEC_62196_T3C",
114    Nema520 = "NEMA_5_20",
115    Nema630 = "NEMA_6_30",
116    Nema650 = "NEMA_6_50",
117    Nema1030 = "NEMA_10_30",
118    Nema1050 = "NEMA_10_50",
119    Nema1430 = "NEMA_14_30",
120    Nema1450 = "NEMA_14_50",
121    PantographBottomUp = "PANTOGRAPH_BOTTOM_UP",
122    PantographTopDown = "PANTOGRAPH_TOP_DOWN",
123    TeslaR = "TESLA_R",
124    TeslaS = "TESLA_S",
125});
126
127// `AuthMethod` (`mod_cdrs`).
128ocpi_enum!(AuthMethod {
129    AuthRequest = "AUTH_REQUEST",
130    Command = "COMMAND",
131    Whitelist = "WHITELIST",
132});
133
134// `CdrDimensionType` (`mod_cdrs`).
135ocpi_enum!(CdrDimensionType {
136    Current = "CURRENT",
137    Energy = "ENERGY",
138    EnergyExport = "ENERGY_EXPORT",
139    EnergyImport = "ENERGY_IMPORT",
140    MaxCurrent = "MAX_CURRENT",
141    MinCurrent = "MIN_CURRENT",
142    MaxPower = "MAX_POWER",
143    MinPower = "MIN_POWER",
144    ParkingTime = "PARKING_TIME",
145    Power = "POWER",
146    ReservationTime = "RESERVATION_TIME",
147    StateOfCharge = "STATE_OF_CHARGE",
148    Time = "TIME",
149});
150
151// `TariffDimensionType` (mod_tariffs).
152ocpi_enum!(TariffDimensionType {
153    Energy = "ENERGY",
154    Flat = "FLAT",
155    ParkingTime = "PARKING_TIME",
156    Time = "TIME",
157});
158
159// `DayOfWeek` (mod_tariffs).
160ocpi_enum!(DayOfWeek {
161    Monday = "MONDAY",
162    Tuesday = "TUESDAY",
163    Wednesday = "WEDNESDAY",
164    Thursday = "THURSDAY",
165    Friday = "FRIDAY",
166    Saturday = "SATURDAY",
167    Sunday = "SUNDAY",
168});
169
170// `ReservationRestrictionType` (mod_tariffs).
171ocpi_enum!(ReservationRestrictionType {
172    Reservation = "RESERVATION",
173    ReservationExpires = "RESERVATION_EXPIRES",
174});
175
176// `TariffType` (mod_tariffs).
177ocpi_enum!(TariffType {
178    AdHocPayment = "AD_HOC_PAYMENT",
179    ProfileCheap = "PROFILE_CHEAP",
180    ProfileFast = "PROFILE_FAST",
181    ProfileGreen = "PROFILE_GREEN",
182    Regular = "REGULAR",
183});
184
185// `EnergySourceCategory` (mod_locations).
186ocpi_enum!(EnergySourceCategory {
187    Nuclear = "NUCLEAR",
188    GeneralFossil = "GENERAL_FOSSIL",
189    Coal = "COAL",
190    Gas = "GAS",
191    GeneralGreen = "GENERAL_GREEN",
192    Solar = "SOLAR",
193    Wind = "WIND",
194    Water = "WATER",
195});
196
197// `EnvironmentalImpactCategory` (mod_locations).
198ocpi_enum!(EnvironmentalImpactCategory {
199    NuclearWaste = "NUCLEAR_WASTE",
200    CarbonDioxide = "CARBON_DIOXIDE",
201});
202
203// Item schema for arrays of enum values, which serialize as JSON strings.
204static DAY_OF_WEEK: Schema = Schema::Scalar(Scalar::Enum(DayOfWeek::VARIANTS));
205
206static PRICE_OBJ: Object = Object {
207    fields: &[
208        Field::required("excl_vat", Scalar::Number),
209        Field::optional("incl_vat", Scalar::Number),
210    ],
211    kind: BuilderKind::V221Price,
212};
213
214static GEO_LOCATION_OBJ: Object = Object {
215    // Latitude/longitude are decimal-degree strings in OCPI, not numbers.
216    fields: &[
217        Field::required("latitude", Scalar::StringMax(MAX_LEN_LATITUDE)),
218        Field::required("longitude", Scalar::StringMax(MAX_LEN_LONGITUDE)),
219    ],
220    kind: BuilderKind::Ignore,
221};
222
223static CDR_TOKEN_OBJ: Object = Object {
224    fields: &[
225        Field::required("contract_id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
226        Field::required("country_code", Scalar::StringMax(MAX_LEN_COUNTRY_CODE)),
227        Field::required("party_id", Scalar::StringMax(MAX_LEN_PARTY_ID)),
228        Field::required("type", Scalar::Enum(TokenType::VARIANTS)),
229        Field::required("uid", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
230    ],
231    kind: BuilderKind::Ignore,
232};
233
234static CDR_LOCATION_OBJ: Object = Object {
235    fields: &[
236        Field::required("address", Scalar::StringMax(MAX_LEN_ADDRESS)),
237        Field::required("city", Scalar::StringMax(MAX_LEN_CITY)),
238        Field::required("connector_format", Scalar::Enum(ConnectorFormat::VARIANTS)),
239        Field::required("connector_id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
240        Field::required("connector_power_type", Scalar::Enum(PowerType::VARIANTS)),
241        Field::required("connector_standard", Scalar::Enum(ConnectorType::VARIANTS)),
242        Field::required_object("coordinates", &GEO_LOCATION_OBJ),
243        Field::required("country", Scalar::StringMax(MAX_LEN_COUNTRY)),
244        Field::required("evse_id", Scalar::StringMax(MAX_LEN_EVSE_ID)),
245        Field::required("evse_uid", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
246        Field::required("id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
247        Field::optional("name", Scalar::StringMax(MAX_LEN_NAME)),
248        Field::optional("postal_code", Scalar::StringMax(MAX_LEN_POSTAL_CODE)),
249        Field::optional("state", Scalar::StringMax(MAX_LEN_STATE)),
250    ],
251    kind: BuilderKind::Ignore,
252};
253
254static CDR_DIMENSION_OBJ: Object = Object {
255    fields: &[
256        Field::required("type", Scalar::Enum(CdrDimensionType::VARIANTS)),
257        Field::required("volume", Scalar::Number),
258    ],
259    kind: BuilderKind::V221CdrDimension,
260};
261static CDR_DIMENSION: Schema = Schema::Object(&CDR_DIMENSION_OBJ);
262
263static CHARGING_PERIOD_OBJ: Object = Object {
264    fields: &[
265        Field::required_array("dimensions", &CDR_DIMENSION),
266        Field::required("start_date_time", Scalar::String),
267        Field::optional("tariff_id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
268    ],
269    kind: BuilderKind::V221ChargingPeriod,
270};
271static CHARGING_PERIOD: Schema = Schema::Object(&CHARGING_PERIOD_OBJ);
272
273static SIGNED_VALUE_OBJ: Object = Object {
274    fields: &[
275        Field::required("nature", Scalar::StringMax(MAX_LEN_NATURE)),
276        Field::required("plain_data", Scalar::StringMax(MAX_LEN_PLAIN_DATA)),
277        Field::required("signed_data", Scalar::StringMax(MAX_LEN_SIGNED_DATA)),
278    ],
279    kind: BuilderKind::Ignore,
280};
281static SIGNED_VALUE: Schema = Schema::Object(&SIGNED_VALUE_OBJ);
282
283// NOTE: encoding_method_version` is typed `int` in the 2.2.1 spec but modeled
284// here as `Scalar::String`. That is a preexisting type discrepancy, not a
285// length one, so it is left unchanged (and thus length-unconstrained).
286static SIGNED_DATA_OBJ: Object = Object {
287    fields: &[
288        Field::required("encoding_method", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
289        Field::optional("encoding_method_version", Scalar::String),
290        Field::optional("public_key", Scalar::StringMax(MAX_LEN_PUBLIC_KEY)),
291        Field::required_array("signed_values", &SIGNED_VALUE),
292        Field::optional("url", Scalar::StringMax(MAX_LEN_SIGNED_DATA_URL)),
293    ],
294    kind: BuilderKind::Ignore,
295};
296
297static DISPLAY_TEXT_OBJ: Object = Object {
298    fields: &[
299        Field::required("language", Scalar::StringMax(MAX_LEN_LANGUAGE)),
300        Field::required("text", Scalar::StringMax(MAX_LEN_DISPLAY_TEXT)),
301    ],
302    kind: BuilderKind::Ignore,
303};
304static DISPLAY_TEXT: Schema = Schema::Object(&DISPLAY_TEXT_OBJ);
305
306static ENERGY_SOURCE_OBJ: Object = Object {
307    fields: &[
308        Field::required("percentage", Scalar::Number),
309        Field::required("source", Scalar::Enum(EnergySourceCategory::VARIANTS)),
310    ],
311    kind: BuilderKind::Ignore,
312};
313static ENERGY_SOURCE: Schema = Schema::Object(&ENERGY_SOURCE_OBJ);
314
315static ENVIRONMENTAL_IMPACT_OBJ: Object = Object {
316    fields: &[
317        Field::required("amount", Scalar::Number),
318        Field::required(
319            "category",
320            Scalar::Enum(EnvironmentalImpactCategory::VARIANTS),
321        ),
322    ],
323    kind: BuilderKind::Ignore,
324};
325static ENVIRONMENTAL_IMPACT: Schema = Schema::Object(&ENVIRONMENTAL_IMPACT_OBJ);
326
327static ENERGY_MIX_OBJ: Object = Object {
328    fields: &[
329        Field::optional(
330            "energy_product_name",
331            Scalar::StringMax(MAX_LEN_ENERGY_MIX_NAME),
332        ),
333        Field::optional_array("energy_sources", &ENERGY_SOURCE),
334        Field::optional_array("environ_impact", &ENVIRONMENTAL_IMPACT),
335        Field::required("is_green_energy", Scalar::Boolean),
336        Field::optional("supplier_name", Scalar::StringMax(MAX_LEN_ENERGY_MIX_NAME)),
337    ],
338    kind: BuilderKind::Ignore,
339};
340
341static PRICE_COMPONENT_OBJ: Object = Object {
342    fields: &[
343        Field::required("price", Scalar::Number),
344        Field::required("step_size", Scalar::Number),
345        Field::required("type", Scalar::Enum(TariffDimensionType::VARIANTS)),
346        Field::optional("vat", Scalar::Number),
347    ],
348    kind: BuilderKind::V221PriceComponent,
349};
350static PRICE_COMPONENT: Schema = Schema::Object(&PRICE_COMPONENT_OBJ);
351
352static TARIFF_RESTRICTIONS_OBJ: Object = Object {
353    fields: &[
354        Field::optional_array("day_of_week", &DAY_OF_WEEK),
355        Field::optional("end_date", Scalar::String),
356        Field::optional("end_time", Scalar::String),
357        Field::optional("max_current", Scalar::Number),
358        Field::optional("max_duration", Scalar::Number),
359        Field::optional("max_kwh", Scalar::Number),
360        Field::optional("max_power", Scalar::Number),
361        Field::optional("min_current", Scalar::Number),
362        Field::optional("min_duration", Scalar::Number),
363        Field::optional("min_kwh", Scalar::Number),
364        Field::optional("min_power", Scalar::Number),
365        Field::optional(
366            "reservation",
367            Scalar::Enum(ReservationRestrictionType::VARIANTS),
368        ),
369        Field::optional("start_date", Scalar::String),
370        Field::optional("start_time", Scalar::String),
371    ],
372    kind: BuilderKind::V221Restrictions,
373};
374
375static TARIFF_ELEMENT_OBJ: Object = Object {
376    fields: &[
377        Field::required_array("price_components", &PRICE_COMPONENT),
378        Field::optional_object("restrictions", &TARIFF_RESTRICTIONS_OBJ),
379    ],
380    kind: BuilderKind::V221Element,
381};
382static TARIFF_ELEMENT: Schema = Schema::Object(&TARIFF_ELEMENT_OBJ);
383
384// -- Tariff ------------------------------------------------------------------
385
386static TARIFF_OBJ: Object = Object {
387    fields: &[
388        Field::required("country_code", Scalar::StringMax(MAX_LEN_COUNTRY_CODE)),
389        Field::required("currency", Scalar::StringMax(MAX_LEN_CURRENCY)),
390        Field::required_array("elements", &TARIFF_ELEMENT),
391        Field::optional("end_date_time", Scalar::String),
392        Field::optional_object("energy_mix", &ENERGY_MIX_OBJ),
393        Field::required("id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
394        Field::required("last_updated", Scalar::String),
395        Field::optional_price("max_price", &PRICE_OBJ),
396        Field::optional_price("min_price", &PRICE_OBJ),
397        Field::required("party_id", Scalar::StringMax(MAX_LEN_PARTY_ID)),
398        Field::optional("start_date_time", Scalar::String),
399        Field::optional_array("tariff_alt_text", &DISPLAY_TEXT),
400        Field::optional("tariff_alt_url", Scalar::StringMax(MAX_LEN_URL)),
401        Field::optional("type", Scalar::Enum(TariffType::VARIANTS)),
402    ],
403    kind: BuilderKind::V221Tariff,
404};
405
406/// OCPI 2.2.1 Tariff schema.
407///
408/// Validates top-level `Tariff` objects: required fields are `country_code`,
409/// `currency`, `elements`, `id`, `last_updated`, and `party_id`.
410static TARIFF: Schema = Schema::Object(&TARIFF_OBJ);
411
412// -- CDR ---------------------------------------------------------------------
413
414static CDR_OBJ: Object = Object {
415    fields: &[
416        Field::required("auth_method", Scalar::Enum(AuthMethod::VARIANTS)),
417        Field::optional(
418            "authorization_reference",
419            Scalar::StringMax(MAX_LEN_IDENTIFIER),
420        ),
421        Field::required_object("cdr_location", &CDR_LOCATION_OBJ),
422        Field::required_object("cdr_token", &CDR_TOKEN_OBJ),
423        Field::required_array("charging_periods", &CHARGING_PERIOD),
424        Field::required("country_code", Scalar::StringMax(MAX_LEN_COUNTRY_CODE)),
425        Field::optional("credit", Scalar::Boolean),
426        Field::optional(
427            "credit_reference_id",
428            Scalar::StringMax(MAX_LEN_LONG_IDENTIFIER),
429        ),
430        Field::required("currency", Scalar::StringMax(MAX_LEN_CURRENCY)),
431        Field::required("end_date_time", Scalar::String),
432        Field::optional("home_charging_compensation", Scalar::Boolean),
433        Field::required("id", Scalar::StringMax(MAX_LEN_LONG_IDENTIFIER)),
434        Field::optional(
435            "invoice_reference_id",
436            Scalar::StringMax(MAX_LEN_LONG_IDENTIFIER),
437        ),
438        Field::required("last_updated", Scalar::String),
439        Field::optional("meter_id", Scalar::StringMax(MAX_LEN_METER_ID)),
440        Field::required("party_id", Scalar::StringMax(MAX_LEN_PARTY_ID)),
441        Field::optional("remark", Scalar::StringMax(MAX_LEN_REMARK)),
442        Field::optional("session_id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
443        Field::optional_object("signed_data", &SIGNED_DATA_OBJ),
444        Field::required("start_date_time", Scalar::String),
445        Field::optional_array("tariffs", &TARIFF),
446        Field::required_price("total_cost", &PRICE_OBJ),
447        Field::required("total_energy", Scalar::Number),
448        Field::optional_price("total_energy_cost", &PRICE_OBJ),
449        Field::optional_price("total_fixed_cost", &PRICE_OBJ),
450        Field::optional_price("total_parking_cost", &PRICE_OBJ),
451        Field::optional("total_parking_time", Scalar::Number),
452        Field::optional_price("total_reservation_cost", &PRICE_OBJ),
453        Field::required("total_time", Scalar::Number),
454        Field::optional_price("total_time_cost", &PRICE_OBJ),
455    ],
456    kind: BuilderKind::V221Cdr,
457};
458
459/// OCPI 2.2.1 CDR (Charge Detail Record) schema.
460///
461/// Validates top-level `CDR` objects including all nested types: `CdrToken`,
462/// `CdrLocation`, `GeoLocation`, `ChargingPeriod`, `CdrDimension`,
463/// `SignedData`, `SignedValue`, `Price`, and embedded `Tariff` objects.
464static CDR: Schema = Schema::Object(&CDR_OBJ);
465
466// -- CDR intermediate representation (IR) ------------------------------------
467//
468// The CDR IR models the fields pricing and validation consume: the session window,
469// currency, charging periods (each with its CDR dimensions), the cost/volume totals,
470// and the embedded `tariffs`. As with the tariff IR, every field is an `Integrity`
471// value, so building is infallible. Fields the spec defines but this layer does not
472// use (`cdr_token`, `cdr_location`, `signed_data`, the various identifiers) are
473// validated by the walk but not represented here.
474
475/// The intermediate representation of a `v2.2.1` CDR.
476#[derive(Default, Clone, Debug)]
477pub(crate) struct Cdr<'buf> {
478    pub currency: Integrity<Str<'buf>>,
479    pub start_date_time: Integrity<Str<'buf>>,
480    pub end_date_time: Integrity<Str<'buf>>,
481    pub charging_periods: Integrity<Vec<Integrity<ChargingPeriod<'buf>>>>,
482    pub total_cost: Integrity<Price<'buf>>,
483    pub total_energy: Integrity<Number<'buf>>,
484    pub total_time: Integrity<Number<'buf>>,
485    pub total_parking_time: Integrity<Option<Number<'buf>>>,
486    pub total_energy_cost: Integrity<Option<Price<'buf>>>,
487    pub total_fixed_cost: Integrity<Option<Price<'buf>>>,
488    pub total_parking_cost: Integrity<Option<Price<'buf>>>,
489    pub total_reservation_cost: Integrity<Option<Price<'buf>>>,
490    pub total_time_cost: Integrity<Option<Price<'buf>>>,
491    pub tariffs: Integrity<Option<Vec<Integrity<Tariff<'buf>>>>>,
492}
493
494/// The intermediate representation of a `v2.2.1` CDR charging period.
495#[derive(Clone, Default, Debug)]
496pub(crate) struct ChargingPeriod<'buf> {
497    pub start_date_time: Integrity<Str<'buf>>,
498    pub dimensions: Integrity<Vec<Integrity<Dimension<'buf>>>>,
499}
500
501/// The intermediate representation of a `v2.2.1` CDR dimension.
502#[derive(Clone, Default, Debug)]
503pub(crate) struct Dimension<'buf> {
504    pub dimension_type: Integrity<Enum<'buf, CdrDimensionType>>,
505    pub volume: Integrity<Number<'buf>>,
506}
507
508/// The intermediate representation of a `v2.2.1` tariff.
509#[derive(Clone, Default, Debug)]
510pub(crate) struct Tariff<'buf> {
511    pub country_code: Integrity<Str<'buf>>,
512    pub currency: Integrity<Str<'buf>>,
513    pub id: Integrity<Str<'buf>>,
514    pub party_id: Integrity<Str<'buf>>,
515    pub min_price: Integrity<Option<Price<'buf>>>,
516    pub max_price: Integrity<Option<Price<'buf>>>,
517    pub start_date_time: Integrity<Option<Str<'buf>>>,
518    pub end_date_time: Integrity<Option<Str<'buf>>>,
519    pub elements: Integrity<Vec<Integrity<Element<'buf>>>>,
520}
521
522/// The intermediate representation of a tariff element.
523#[derive(Clone, Default, Debug)]
524pub(crate) struct Element<'buf> {
525    pub price_components: Integrity<Vec<Integrity<PriceComponent<'buf>>>>,
526    pub restrictions: Integrity<Option<Restrictions<'buf>>>,
527}
528
529/// The intermediate representation of a price component.
530#[derive(Clone, Default, Debug)]
531pub(crate) struct PriceComponent<'buf> {
532    pub dimension_type: Integrity<Enum<'buf, TariffDimensionType>>,
533    pub vat: Integrity<Option<Number<'buf>>>,
534    pub price: Integrity<Number<'buf>>,
535    pub step_size: Integrity<Number<'buf>>,
536}
537
538/// The intermediate representation of a `Price`.
539///
540/// OCPI 2.1.1 wrote a price as a bare `number`; 2.2.1 made it a `Price` object. Both
541/// forms are accepted (see [`super::Schema::Price`]); the variant records which was
542/// seen, just as [`super::Number`] records whether a number was written bare or
543/// string-encoded.
544#[derive(Clone, Debug)]
545pub(crate) enum Price<'buf> {
546    /// A JSON `Price` object.
547    Object {
548        /// The element this object was built from. The lowering step attaches its
549        /// object-level warnings (such as excl-vat-greater-than-incl-vat) to it.
550        elem: json::Element<'buf>,
551        excl_vat: Integrity<Number<'buf>>,
552        incl_vat: Integrity<Option<Number<'buf>>>,
553    },
554    /// A bare JSON number (the 2.1.1 shape), taken as `excl_vat` with no `incl_vat`.
555    Number(Number<'buf>),
556}
557
558impl<'buf> Price<'buf> {
559    /// Create an empty `Price` object builder for the object at `elem`. Its fields start
560    /// `Missing` and are filled by the walk as the object's children close.
561    pub(super) fn new(elem: json::Element<'buf>) -> Self {
562        Self::Object {
563            elem,
564            excl_vat: Integrity::Missing,
565            incl_vat: Integrity::Missing,
566        }
567    }
568
569    /// Build a `Price` from a bare JSON number. Per OCPI's evolution (a price was a bare
570    /// `number` in 2.1.1 and became a `Price` object in 2.2.1) the number is taken as
571    /// `excl_vat`, leaving `incl_vat` absent.
572    pub(super) fn from_number(elem: json::Element<'buf>, digits: &'buf str) -> Self {
573        Self::Number(Number::Number { elem, digits })
574    }
575}
576
577/// The intermediate representation of a tariff element's restrictions.
578#[derive(Clone, Default, Debug)]
579pub(crate) struct Restrictions<'buf> {
580    pub start_time: Integrity<Option<Str<'buf>>>,
581    pub end_time: Integrity<Option<Str<'buf>>>,
582    pub start_date: Integrity<Option<Str<'buf>>>,
583    pub end_date: Integrity<Option<Str<'buf>>>,
584    pub min_kwh: Integrity<Option<Number<'buf>>>,
585    pub max_kwh: Integrity<Option<Number<'buf>>>,
586    pub min_current: Integrity<Option<Number<'buf>>>,
587    pub max_current: Integrity<Option<Number<'buf>>>,
588    pub min_power: Integrity<Option<Number<'buf>>>,
589    pub max_power: Integrity<Option<Number<'buf>>>,
590    pub min_duration: Integrity<Option<Number<'buf>>>,
591    pub max_duration: Integrity<Option<Number<'buf>>>,
592    pub day_of_week: Integrity<Option<Vec<Integrity<Enum<'buf, DayOfWeek>>>>>,
593    pub reservation: Integrity<Option<Enum<'buf, ReservationRestrictionType>>>,
594}
595
596/// Build and validate a `v2.2.1` tariff from a `json::Document`.
597///
598/// Building is infallible: the returned [`Tariff`] always exists, with each field in
599/// an [`Integrity`] state, alongside the [`Warning`]s found during the walk.
600pub(crate) fn build_tariff<'buf>(doc: &json::Document<'buf>) -> Caveat<Tariff<'buf>, Warning> {
601    let (node, warnings) = super::walk(doc, &TARIFF).into_parts();
602
603    // An object root yields `Node::Tariff` (the `TARIFF` schema's `BuilderKind`); a
604    // non-object root yields no tariff builder, so it falls back to an empty tariff.
605    let tariff = if let build::Node::Tariff(tariff) = node {
606        tariff
607    } else {
608        Tariff::default()
609    };
610
611    Caveat::new(tariff, warnings)
612}
613
614/// Build and validate a `v2.2.1` CDR from a `json::Document`.
615///
616/// Building is infallible: the returned [`Cdr`] always exists, with each field in
617/// an [`Integrity`] state, alongside the [`Warning`]s found during the walk.
618pub(crate) fn build_cdr<'buf>(doc: &json::Document<'buf>) -> Caveat<Cdr<'buf>, Warning> {
619    let (node, warnings) = super::walk(doc, &CDR).into_parts();
620
621    // An object root yields `Node::Cdr` (the `CDR` schema's `BuilderKind`); a
622    // non-object root yields no CDR builder, so it falls back to an empty CDR.
623    let cdr = if let build::Node::Cdr(cdr) = node {
624        cdr
625    } else {
626        Cdr::default()
627    };
628
629    Caveat::new(cdr, warnings)
630}