Skip to main content

ocpi_tariffs/schema/
v211.rs

1#[cfg(test)]
2mod tests;
3
4#[cfg(test)]
5mod tests_ir;
6
7#[cfg(test)]
8mod tests_cdr_ir;
9
10use crate::{json, schema::Warning, Caveat};
11
12use super::{
13    build, ir_object, ocpi_enum, BuilderKind, Enum, Field, Integrity, List, Number, Object, Scalar,
14    Schema, Str,
15};
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_COUNTRY: usize = 3;
28const MAX_LEN_LANGUAGE: usize = 2;
29
30// -- Geographic and address ----------------------------------------------
31const MAX_LEN_LATITUDE: usize = 10;
32const MAX_LEN_LONGITUDE: usize = 11;
33const MAX_LEN_POSTAL_CODE: usize = 10;
34const MAX_LEN_ADDRESS: usize = 45;
35const MAX_LEN_CITY: usize = 45;
36
37// -- Free text and names -------------------------------------------------
38/// Human-readable name of a location.
39const MAX_LEN_NAME: usize = 255;
40const MAX_LEN_URL: usize = 255;
41const MAX_LEN_METER_ID: usize = 255;
42const MAX_LEN_REMARK: usize = 255;
43const MAX_LEN_TIME_ZONE: usize = 255;
44const MAX_LEN_ENERGY_MIX_NAME: usize = 64;
45const MAX_LEN_DISPLAY_TEXT: usize = 512;
46const MAX_LEN_FLOOR_LEVEL: usize = 4;
47const MAX_LEN_PHYSICAL_REFERENCE: usize = 16;
48
49// Item schema for arrays of objects this layer does not model (e.g. `Image`,
50// `StatusSchedule`, `AdditionalGeoLocation`).
51static ANY: Schema = Schema::Scalar(Scalar::Any);
52
53// -- Enum value sets ---------------------------------------------------------
54// A Rust enum for each OCPI 2.1.1 enum that appears in the CDR/Tariff graph.
55// The values are matched case-insensitively. Each `ocpi_enum!` also
56// generates the `VARIANTS` table used by `Scalar::Enum`.
57
58// `ConnectorFormat` (mod_locations).
59ocpi_enum!(ConnectorFormat {
60    Socket = "SOCKET",
61    Cable = "CABLE",
62});
63
64// `PowerType` (mod_locations).
65ocpi_enum!(PowerType {
66    Ac1Phase = "AC_1_PHASE",
67    Ac3Phase = "AC_3_PHASE",
68    Dc = "DC",
69});
70
71// `ConnectorType` (mod_locations).
72ocpi_enum!(ConnectorType {
73    Chademo = "CHADEMO",
74    DomesticA = "DOMESTIC_A",
75    DomesticB = "DOMESTIC_B",
76    DomesticC = "DOMESTIC_C",
77    DomesticD = "DOMESTIC_D",
78    DomesticE = "DOMESTIC_E",
79    DomesticF = "DOMESTIC_F",
80    DomesticG = "DOMESTIC_G",
81    DomesticH = "DOMESTIC_H",
82    DomesticI = "DOMESTIC_I",
83    DomesticJ = "DOMESTIC_J",
84    DomesticK = "DOMESTIC_K",
85    DomesticL = "DOMESTIC_L",
86    Iec603092Single16 = "IEC_60309_2_single_16",
87    Iec603092Three16 = "IEC_60309_2_three_16",
88    Iec603092Three32 = "IEC_60309_2_three_32",
89    Iec603092Three64 = "IEC_60309_2_three_64",
90    Iec62196T1 = "IEC_62196_T1",
91    Iec62196T1Combo = "IEC_62196_T1_COMBO",
92    Iec62196T2 = "IEC_62196_T2",
93    Iec62196T2Combo = "IEC_62196_T2_COMBO",
94    Iec62196T3A = "IEC_62196_T3A",
95    Iec62196T3C = "IEC_62196_T3C",
96    TeslaR = "TESLA_R",
97    TeslaS = "TESLA_S",
98});
99
100// `Capability` (mod_locations).
101ocpi_enum!(Capability {
102    ChargingProfileCapable = "CHARGING_PROFILE_CAPABLE",
103    CreditCardPayable = "CREDIT_CARD_PAYABLE",
104    RemoteStartStopCapable = "REMOTE_START_STOP_CAPABLE",
105    Reservable = "RESERVABLE",
106    RfidReader = "RFID_READER",
107    UnlockCapable = "UNLOCK_CAPABLE",
108});
109
110// `ParkingRestriction` (mod_locations).
111ocpi_enum!(ParkingRestriction {
112    EvOnly = "EV_ONLY",
113    Plugged = "PLUGGED",
114    Disabled = "DISABLED",
115    Customers = "CUSTOMERS",
116    Motorcycles = "MOTORCYCLES",
117});
118
119// `Status` (mod_locations).
120ocpi_enum!(Status {
121    Available = "AVAILABLE",
122    Blocked = "BLOCKED",
123    Charging = "CHARGING",
124    Inoperative = "INOPERATIVE",
125    OutOfOrder = "OUTOFORDER",
126    Planned = "PLANNED",
127    Removed = "REMOVED",
128    Reserved = "RESERVED",
129    Unknown = "UNKNOWN",
130});
131
132// `Facility` (mod_locations).
133ocpi_enum!(Facility {
134    Hotel = "HOTEL",
135    Restaurant = "RESTAURANT",
136    Cafe = "CAFE",
137    Mall = "MALL",
138    Supermarket = "SUPERMARKET",
139    Sport = "SPORT",
140    RecreationArea = "RECREATION_AREA",
141    Nature = "NATURE",
142    Museum = "MUSEUM",
143    BusStop = "BUS_STOP",
144    TaxiStand = "TAXI_STAND",
145    TrainStation = "TRAIN_STATION",
146    Airport = "AIRPORT",
147    CarpoolParking = "CARPOOL_PARKING",
148    FuelStation = "FUEL_STATION",
149    Wifi = "WIFI",
150});
151
152// `LocationType` (mod_locations).
153ocpi_enum!(LocationType {
154    OnStreet = "ON_STREET",
155    ParkingGarage = "PARKING_GARAGE",
156    UndergroundGarage = "UNDERGROUND_GARAGE",
157    ParkingLot = "PARKING_LOT",
158    Other = "OTHER",
159    Unknown = "UNKNOWN",
160});
161
162// `AuthMethod` (`mod_cdrs`).
163ocpi_enum!(AuthMethod {
164    AuthRequest = "AUTH_REQUEST",
165    Whitelist = "WHITELIST",
166});
167
168// `CdrDimensionType` (`mod_cdrs`).
169ocpi_enum!(CdrDimensionType {
170    Energy = "ENERGY",
171    Flat = "FLAT",
172    MaxCurrent = "MAX_CURRENT",
173    MinCurrent = "MIN_CURRENT",
174    ParkingTime = "PARKING_TIME",
175    Time = "TIME",
176});
177
178// `TariffDimensionType` (mod_tariffs).
179ocpi_enum!(TariffDimensionType {
180    Energy = "ENERGY",
181    Flat = "FLAT",
182    ParkingTime = "PARKING_TIME",
183    Time = "TIME",
184});
185
186// `DayOfWeek` (mod_tariffs).
187ocpi_enum!(DayOfWeek {
188    Monday = "MONDAY",
189    Tuesday = "TUESDAY",
190    Wednesday = "WEDNESDAY",
191    Thursday = "THURSDAY",
192    Friday = "FRIDAY",
193    Saturday = "SATURDAY",
194    Sunday = "SUNDAY",
195});
196
197// `EnergySourceCategory` (mod_locations).
198ocpi_enum!(EnergySourceCategory {
199    Nuclear = "NUCLEAR",
200    GeneralFossil = "GENERAL_FOSSIL",
201    Coal = "COAL",
202    Gas = "GAS",
203    GeneralGreen = "GENERAL_GREEN",
204    Solar = "SOLAR",
205    Wind = "WIND",
206    Water = "WATER",
207});
208
209// `EnvironmentalImpactCategory` (mod_locations).
210ocpi_enum!(EnvironmentalImpactCategory {
211    NuclearWaste = "NUCLEAR_WASTE",
212    CarbonDioxide = "CARBON_DIOXIDE",
213});
214
215// Item schemas for arrays of enum values, which serialize as JSON strings.
216static CAPABILITY: Schema = Schema::Scalar(Scalar::Enum(Capability::VARIANTS));
217static PARKING_RESTRICTION: Schema = Schema::Scalar(Scalar::Enum(ParkingRestriction::VARIANTS));
218static FACILITY: Schema = Schema::Scalar(Scalar::Enum(Facility::VARIANTS));
219static DAY_OF_WEEK: Schema = Schema::Scalar(Scalar::Enum(DayOfWeek::VARIANTS));
220
221static GEO_LOCATION_OBJ: Object = Object {
222    // Latitude/longitude are decimal-degree strings in OCPI, not numbers.
223    fields: &[
224        Field::required("latitude", Scalar::StringMax(MAX_LEN_LATITUDE)),
225        Field::required("longitude", Scalar::StringMax(MAX_LEN_LONGITUDE)),
226    ],
227    kind: BuilderKind::Ignore,
228};
229
230static DISPLAY_TEXT_OBJ: Object = Object {
231    fields: &[
232        Field::required("language", Scalar::StringMax(MAX_LEN_LANGUAGE)),
233        Field::required("text", Scalar::StringMax(MAX_LEN_DISPLAY_TEXT)),
234    ],
235    kind: BuilderKind::Ignore,
236};
237static DISPLAY_TEXT: Schema = Schema::Object(&DISPLAY_TEXT_OBJ);
238
239static ENERGY_SOURCE_OBJ: Object = Object {
240    fields: &[
241        Field::required("percentage", Scalar::Number),
242        Field::required("source", Scalar::Enum(EnergySourceCategory::VARIANTS)),
243    ],
244    kind: BuilderKind::Ignore,
245};
246static ENERGY_SOURCE: Schema = Schema::Object(&ENERGY_SOURCE_OBJ);
247
248// In 2.1.1 the impact category field is named "source", not "category" (2.2.1).
249
250static ENVIRONMENTAL_IMPACT_OBJ: Object = Object {
251    fields: &[
252        Field::required("amount", Scalar::Number),
253        Field::required(
254            "source",
255            Scalar::Enum(EnvironmentalImpactCategory::VARIANTS),
256        ),
257    ],
258    kind: BuilderKind::Ignore,
259};
260static ENVIRONMENTAL_IMPACT: Schema = Schema::Object(&ENVIRONMENTAL_IMPACT_OBJ);
261
262static ENERGY_MIX_OBJ: Object = Object {
263    fields: &[
264        Field::optional(
265            "energy_product_name",
266            Scalar::StringMax(MAX_LEN_ENERGY_MIX_NAME),
267        ),
268        Field::optional_array("energy_sources", &ENERGY_SOURCE),
269        Field::optional_array("environ_impact", &ENVIRONMENTAL_IMPACT),
270        Field::required("is_green_energy", Scalar::Boolean),
271        Field::optional("supplier_name", Scalar::StringMax(MAX_LEN_ENERGY_MIX_NAME)),
272    ],
273    kind: BuilderKind::Ignore,
274};
275
276// -- Connector ---------------------------------------------------------------
277
278static CONNECTOR_OBJ: Object = Object {
279    fields: &[
280        Field::required("amperage", Scalar::Number),
281        Field::required("format", Scalar::Enum(ConnectorFormat::VARIANTS)),
282        Field::required("id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
283        Field::required("last_updated", Scalar::String),
284        Field::required("power_type", Scalar::Enum(PowerType::VARIANTS)),
285        Field::required("standard", Scalar::Enum(ConnectorType::VARIANTS)),
286        Field::optional("tariff_id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
287        Field::optional("terms_and_conditions", Scalar::StringMax(MAX_LEN_URL)),
288        Field::required("voltage", Scalar::Number),
289    ],
290    kind: BuilderKind::Ignore,
291};
292static CONNECTOR: Schema = Schema::Object(&CONNECTOR_OBJ);
293
294// -- EVSE --------------------------------------------------------------------
295
296static EVSE_OBJ: Object = Object {
297    fields: &[
298        Field::optional_array("capabilities", &CAPABILITY),
299        Field::required_array("connectors", &CONNECTOR),
300        Field::optional_object("coordinates", &GEO_LOCATION_OBJ),
301        Field::optional_array("directions", &DISPLAY_TEXT),
302        Field::optional("evse_id", Scalar::StringMax(MAX_LEN_EVSE_ID)),
303        Field::optional("floor_level", Scalar::StringMax(MAX_LEN_FLOOR_LEVEL)),
304        Field::optional_array("images", &ANY),
305        Field::required("last_updated", Scalar::String),
306        Field::optional_array("parking_restrictions", &PARKING_RESTRICTION),
307        Field::optional(
308            "physical_reference",
309            Scalar::StringMax(MAX_LEN_PHYSICAL_REFERENCE),
310        ),
311        Field::required("status", Scalar::Enum(Status::VARIANTS)),
312        Field::optional_array("status_schedule", &ANY),
313        Field::required("uid", Scalar::StringMax(MAX_LEN_LONG_IDENTIFIER)),
314    ],
315    kind: BuilderKind::Ignore,
316};
317static EVSE: Schema = Schema::Object(&EVSE_OBJ);
318
319// In 2.1.1 the CDR embeds a full `Location` snapshot (not a `CdrLocation` as in 2.2.1).
320// `BusinessDetails` (`operator`/`suboperator`/`owner`) and Hours
321// (`opening_times`) are opaque to this schema layer and validated as Any.
322
323static LOCATION_OBJ: Object = Object {
324    fields: &[
325        Field::required("address", Scalar::StringMax(MAX_LEN_ADDRESS)),
326        Field::optional("charging_when_closed", Scalar::Boolean),
327        Field::required("city", Scalar::StringMax(MAX_LEN_CITY)),
328        Field::required_object("coordinates", &GEO_LOCATION_OBJ),
329        Field::required("country", Scalar::StringMax(MAX_LEN_COUNTRY)),
330        Field::optional_array("directions", &DISPLAY_TEXT),
331        Field::optional_object("energy_mix", &ENERGY_MIX_OBJ),
332        Field::optional_array("evses", &EVSE),
333        Field::optional_array("facilities", &FACILITY),
334        Field::required("id", Scalar::StringMax(MAX_LEN_LONG_IDENTIFIER)),
335        Field::optional_array("images", &ANY),
336        Field::required("last_updated", Scalar::String),
337        Field::optional("name", Scalar::StringMax(MAX_LEN_NAME)),
338        Field::optional("opening_times", Scalar::Any),
339        Field::optional("operator", Scalar::Any),
340        Field::optional("owner", Scalar::Any),
341        Field::required("postal_code", Scalar::StringMax(MAX_LEN_POSTAL_CODE)),
342        Field::optional_array("related_locations", &ANY),
343        Field::optional("suboperator", Scalar::Any),
344        Field::optional("time_zone", Scalar::StringMax(MAX_LEN_TIME_ZONE)),
345        Field::required("type", Scalar::Enum(LocationType::VARIANTS)),
346    ],
347    kind: BuilderKind::V211Location,
348};
349
350static CDR_DIMENSION_OBJ: Object = Object {
351    fields: &[
352        Field::required("type", Scalar::Enum(CdrDimensionType::VARIANTS)),
353        Field::required("volume", Scalar::Number),
354    ],
355    kind: BuilderKind::V211CdrDimension,
356};
357static CDR_DIMENSION: Schema = Schema::Object(&CDR_DIMENSION_OBJ);
358
359// In 2.1.1 `ChargingPeriod` has no "tariff_id" field (added in 2.2.1).
360
361static CHARGING_PERIOD_OBJ: Object = Object {
362    fields: &[
363        Field::required_array("dimensions", &CDR_DIMENSION),
364        Field::required("start_date_time", Scalar::String),
365    ],
366    kind: BuilderKind::V211ChargingPeriod,
367};
368static CHARGING_PERIOD: Schema = Schema::Object(&CHARGING_PERIOD_OBJ);
369
370// In 2.1.1 `PriceComponent` has no "vat" field (added in 2.2.1).
371
372static PRICE_COMPONENT_OBJ: Object = Object {
373    fields: &[
374        Field::required("price", Scalar::Number),
375        Field::required("step_size", Scalar::Number),
376        Field::required("type", Scalar::Enum(TariffDimensionType::VARIANTS)),
377    ],
378    kind: BuilderKind::V211PriceComponent,
379};
380static PRICE_COMPONENT: Schema = Schema::Object(&PRICE_COMPONENT_OBJ);
381
382// In 2.1.1 `TariffRestrictions` has no `max_current`, `min_current`, or
383// `reservation` fields (added in 2.2.1).
384
385static TARIFF_RESTRICTIONS_OBJ: Object = Object {
386    fields: &[
387        Field::optional_array("day_of_week", &DAY_OF_WEEK),
388        Field::optional("end_date", Scalar::String),
389        Field::optional("end_time", Scalar::String),
390        Field::optional("max_duration", Scalar::Number),
391        Field::optional("max_kwh", Scalar::Number),
392        Field::optional("max_power", Scalar::Number),
393        Field::optional("min_duration", Scalar::Number),
394        Field::optional("min_kwh", Scalar::Number),
395        Field::optional("min_power", Scalar::Number),
396        Field::optional("start_date", Scalar::String),
397        Field::optional("start_time", Scalar::String),
398    ],
399    kind: BuilderKind::V211Restrictions,
400};
401
402static TARIFF_ELEMENT_OBJ: Object = Object {
403    fields: &[
404        Field::required_array("price_components", &PRICE_COMPONENT),
405        Field::optional_object("restrictions", &TARIFF_RESTRICTIONS_OBJ),
406    ],
407    kind: BuilderKind::V211Element,
408};
409static TARIFF_ELEMENT: Schema = Schema::Object(&TARIFF_ELEMENT_OBJ);
410
411// In 2.1.1 Tariff has no `country_code`, `end_date_time`, `max_price`,
412// `min_price`, `party_id`, `start_date_time`, or `type` fields (added in 2.2.1).
413
414static TARIFF_OBJ: Object = Object {
415    fields: &[
416        Field::required("currency", Scalar::StringMax(MAX_LEN_CURRENCY)),
417        Field::required_array("elements", &TARIFF_ELEMENT),
418        Field::optional_object("energy_mix", &ENERGY_MIX_OBJ),
419        Field::required("id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
420        Field::required("last_updated", Scalar::String),
421        Field::optional_array("tariff_alt_text", &DISPLAY_TEXT),
422        Field::optional("tariff_alt_url", Scalar::StringMax(MAX_LEN_URL)),
423    ],
424    kind: BuilderKind::V211Tariff,
425};
426
427/// OCPI 2.1.1 Tariff schema.
428///
429/// Validates top-level `Tariff` objects: required fields are `currency`,
430/// `elements`, `id`, and `last_updated`.
431static TARIFF: Schema = Schema::Object(&TARIFF_OBJ);
432
433/// -- CDR ---------------------------------------------------------------------
434/// Key differences from 2.2.1:
435///   - "`auth_id`" (string) instead of a "`cdr_token`" object
436///   - "location" (full embedded Location) instead of "`cdr_location`"
437///   - "`stop_date_time`" instead of "`end_date_time`"
438///   - "`total_cost`" is a plain number, not a Price object
439///   - no "`country_code`", "`party_id`", "credit", "`credit_reference_id`",
440///     "`home_charging_compensation`", "`invoice_reference_id`", "`session_id`",
441///     "`signed_data`", "`authorization_reference`", or itemized cost fields
442static CDR_OBJ: Object = Object {
443    fields: &[
444        Field::required("auth_id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
445        Field::required("auth_method", Scalar::Enum(AuthMethod::VARIANTS)),
446        Field::required_array("charging_periods", &CHARGING_PERIOD),
447        Field::required("currency", Scalar::StringMax(MAX_LEN_CURRENCY)),
448        Field::required("id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
449        Field::required("last_updated", Scalar::String),
450        Field::required_object("location", &LOCATION_OBJ),
451        Field::optional("meter_id", Scalar::StringMax(MAX_LEN_METER_ID)),
452        Field::optional("remark", Scalar::StringMax(MAX_LEN_REMARK)),
453        Field::required("start_date_time", Scalar::String),
454        Field::required("stop_date_time", Scalar::String),
455        Field::optional_array("tariffs", &TARIFF),
456        Field::required("total_cost", Scalar::Number),
457        Field::required("total_energy", Scalar::Number),
458        Field::optional("total_parking_time", Scalar::Number),
459        Field::required("total_time", Scalar::Number),
460    ],
461    kind: BuilderKind::V211Cdr,
462};
463
464/// OCPI 2.1.1 CDR (Charge Detail Record) schema.
465///
466/// Validates top-level `CDR` objects including all nested types: `Location`,
467/// `EVSE`, `Connector`, `GeoLocation`, `ChargingPeriod`, `CdrDimension`,
468/// `EnergyMix`, and embedded `Tariff` objects.
469static CDR: Schema = Schema::Object(&CDR_OBJ);
470
471// -- CDR intermediate representation (IR) ------------------------------------
472//
473// Mirrors the modeled fields of `CDR_OBJ` and its children. Compared with 2.2.1:
474// `stop_date_time` (not `end_date_time`), `total_cost` is a plain number (not a
475// `Price` object), and there are no itemized cost totals. As elsewhere, every field
476// is an `Integrity` value, so building is infallible.
477
478/// The intermediate representation of a `v2.1.1` CDR.
479#[derive(Clone, Debug)]
480pub(crate) struct Cdr<'buf> {
481    pub currency: Integrity<Str<'buf>>,
482    pub start_date_time: Integrity<Str<'buf>>,
483    pub stop_date_time: Integrity<Str<'buf>>,
484    pub charging_periods: Integrity<List<'buf, ChargingPeriod<'buf>>>,
485    pub total_cost: Integrity<Number<'buf>>,
486    pub total_energy: Integrity<Number<'buf>>,
487    pub total_time: Integrity<Number<'buf>>,
488    pub total_parking_time: Integrity<Option<Number<'buf>>>,
489    pub tariffs: Integrity<Option<List<'buf, Tariff<'buf>>>>,
490    pub location: Integrity<Location<'buf>>,
491}
492
493/// The intermediate representation of the `Location` a `v2.1.1` CDR embeds.
494///
495/// Only the two fields the timezone is found or inferred from are retained; the rest of
496/// the `Location` is validated by the walk and dropped.
497#[derive(Clone, Debug)]
498pub(crate) struct Location<'buf> {
499    pub country: Integrity<Str<'buf>>,
500    pub time_zone: Integrity<Option<Str<'buf>>>,
501}
502
503/// The intermediate representation of a `v2.1.1` CDR charging period.
504#[derive(Clone, Debug)]
505pub(crate) struct ChargingPeriod<'buf> {
506    pub start_date_time: Integrity<Str<'buf>>,
507    pub dimensions: Integrity<List<'buf, Dimension<'buf>>>,
508}
509
510/// The intermediate representation of a `v2.1.1` CDR dimension.
511#[derive(Clone, Debug)]
512pub(crate) struct Dimension<'buf> {
513    pub dimension_type: Integrity<Enum<'buf, CdrDimensionType>>,
514    pub volume: Integrity<Number<'buf>>,
515}
516
517// -- Tariff intermediate representation (IR) ---------------------------------
518//
519// The `v2.1.1` tariff IR mirrors the modeled fields of `TARIFF_OBJ` and its children.
520// Compared with 2.2.1 it has no `country_code`, `party_id`, `min_price`, `max_price`,
521// or `start`/`end_date_time`; a price component has no `vat`; and restrictions have no
522// `min_current`/`max_current` or `reservation`. Every field is an `Integrity` value,
523// so building is infallible.
524
525/// The intermediate representation of a `v2.1.1` tariff.
526#[derive(Clone, Debug)]
527pub(crate) struct Tariff<'buf> {
528    pub currency: Integrity<Str<'buf>>,
529    pub id: Integrity<Str<'buf>>,
530    pub elements: Integrity<List<'buf, Element<'buf>>>,
531}
532
533/// The intermediate representation of a `v2.1.1` tariff element.
534#[derive(Clone, Debug)]
535pub(crate) struct Element<'buf> {
536    pub price_components: Integrity<List<'buf, PriceComponent<'buf>>>,
537    pub restrictions: Integrity<Option<Restrictions<'buf>>>,
538}
539
540/// The intermediate representation of a `v2.1.1` price component (no `vat` field).
541#[derive(Clone, Debug)]
542pub(crate) struct PriceComponent<'buf> {
543    pub dimension_type: Integrity<Enum<'buf, TariffDimensionType>>,
544    pub price: Integrity<Number<'buf>>,
545    pub step_size: Integrity<Number<'buf>>,
546}
547
548/// The intermediate representation of a `v2.1.1` tariff element's restrictions (no
549/// `min_current`/`max_current` or `reservation`).
550#[derive(Clone, Debug)]
551pub(crate) struct Restrictions<'buf> {
552    pub start_time: Integrity<Option<Str<'buf>>>,
553    pub end_time: Integrity<Option<Str<'buf>>>,
554    pub start_date: Integrity<Option<Str<'buf>>>,
555    pub end_date: Integrity<Option<Str<'buf>>>,
556    pub min_kwh: Integrity<Option<Number<'buf>>>,
557    pub max_kwh: Integrity<Option<Number<'buf>>>,
558    pub min_power: Integrity<Option<Number<'buf>>>,
559    pub max_power: Integrity<Option<Number<'buf>>>,
560    pub min_duration: Integrity<Option<Number<'buf>>>,
561    pub max_duration: Integrity<Option<Number<'buf>>>,
562    pub day_of_week: Integrity<Option<List<'buf, Enum<'buf, DayOfWeek>>>>,
563}
564
565// All-`Missing` `new` constructors, replacing the `#[derive(Default)]` that
566// [`Integrity`]'s element-carrying `Missing`/`Err` no longer permit.
567ir_object!(Cdr {
568    currency,
569    start_date_time,
570    stop_date_time,
571    charging_periods,
572    total_cost,
573    total_energy,
574    total_time,
575    total_parking_time,
576    tariffs,
577    location,
578});
579ir_object!(Location { country, time_zone });
580ir_object!(ChargingPeriod {
581    start_date_time,
582    dimensions,
583});
584ir_object!(Dimension {
585    dimension_type,
586    volume,
587});
588ir_object!(Tariff {
589    currency,
590    id,
591    elements,
592});
593ir_object!(Element {
594    price_components,
595    restrictions,
596});
597ir_object!(PriceComponent {
598    dimension_type,
599    price,
600    step_size,
601});
602ir_object!(Restrictions {
603    start_time,
604    end_time,
605    start_date,
606    end_date,
607    min_kwh,
608    max_kwh,
609    min_power,
610    max_power,
611    min_duration,
612    max_duration,
613    day_of_week,
614});
615
616/// Validate a `v2.1.1` tariff `json::Document` and build its intermediate representation.
617///
618/// Building is infallible: the returned [`Tariff`] always exists, with each field in
619/// an [`Integrity`] state, alongside the [`Warning`]s found during the walk.
620pub(crate) fn build_tariff<'buf>(doc: &json::Document<'buf>) -> Caveat<Tariff<'buf>, Warning> {
621    let (node, warnings) = super::walk(doc, &TARIFF).into_parts();
622
623    // An object root yields `Node::V211Tariff` (the `TARIFF` schema's `BuilderKind`); a
624    // non-object root yields no tariff builder, so it falls back to an empty tariff.
625    let tariff = if let build::Node::V211Tariff(tariff) = node {
626        tariff
627    } else {
628        Tariff::new(doc.root())
629    };
630
631    Caveat::new(tariff, warnings)
632}
633
634/// Build and validate a `v2.1.1` CDR from a `json::Document`.
635///
636/// Building is infallible: the returned [`Cdr`] always exists, with each field in
637/// an [`Integrity`] state, alongside the [`Warning`]s found during the walk.
638pub(crate) fn build_cdr<'buf>(doc: &json::Document<'buf>) -> Caveat<Cdr<'buf>, Warning> {
639    let (node, warnings) = super::walk(doc, &CDR).into_parts();
640
641    // An object root yields `Node::V211Cdr` (the `CDR` schema's `BuilderKind`); a
642    // non-object root yields no CDR builder, so it falls back to an empty CDR.
643    let cdr = if let build::Node::V211Cdr(cdr) = node {
644        cdr
645    } else {
646        Cdr::new(doc.root())
647    };
648
649    Caveat::new(cdr, warnings)
650}