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