Skip to main content

ocpi_kit/v2_2_1/
locations.rs

1//! The *Locations* module of OCPI 2.2.1, as a delta from
2//! [`v2_3_0::locations`](crate::v2_3_0::locations).
3//!
4//! What changed in 2.3.0 and is therefore **absent here**:
5//!
6//! * the [`Parking`](crate::v2_3_0::locations::Parking) object and everything that references it
7//!   (`Location.parking_places`, `EVSE.parking`, `EVSEParking`, `EVSEPosition`,
8//!   `ParkingDirection`, `VehicleType`) — added for EU AFIR reporting;
9//! * `Location.help_phone`;
10//! * `EVSE.accepted_service_providers`;
11//! * `Connector.capabilities` and the `ConnectorCapability` enum;
12//! * the `MCS` and `SAE_J3400` connector types;
13//! * the `EMPLOYEES`, `TAXIS` and `TENANTS` parking restrictions.
14//!
15//! Everything else is wire-identical and re-exported from the 2.3.0 module, so a
16//! `GeoLocation` is the same type in both versions and needs no conversion.
17//!
18//! Spec: 2.2.1 §mod_locations_locations_module
19
20use bon::Builder;
21use serde::{Deserialize, Serialize};
22
23use crate::ocpi_lenient_enum;
24use crate::types::validate_fields;
25use crate::types::{
26    CiString, CountryCode, DateTime, DisplayText, EvseId, Extensions, OcpiString, PartyId, PartyRef, Url,
27    Validate, Validator, ViolationCode,
28};
29
30use super::tokens::TokenType;
31
32// Wire-identical to OCPI 2.3.0.
33pub use crate::v2_3_0::locations::{
34    AdditionalGeoLocation, BusinessDetails, Capability, ConnectorFormat, EnergyMix, EnergySource,
35    EnergySourceCategory, EnvironmentalImpact, EnvironmentalImpactCategory, ExceptionalPeriod, Facility,
36    GeoLocation, Hours, Image, ImageCategory, ParkingType, PowerType, RegularHours, Status, StatusSchedule,
37};
38
39/// Where a group of EVSEs that belong together is installed, in OCPI 2.2.1.
40///
41/// Spec: 2.2.1 §mod_locations_location_object
42#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44#[builder(on(_, into))]
45pub struct Location {
46    /// ISO-3166 alpha-2 country code of the CPO that 'owns' this Location.
47    pub country_code: CountryCode,
48    /// ID of the CPO that 'owns' this Location.
49    pub party_id: PartyId,
50    /// Uniquely identifies the location within the CPO's platform.
51    pub id: CiString<36>,
52    /// Whether the Location may be published on a website or app.
53    pub publish: bool,
54    /// Tokens allowed to be shown this Location when [`publish`](Self::publish) is `false`.
55    #[serde(default, skip_serializing_if = "Vec::is_empty")]
56    #[builder(default)]
57    pub publish_allowed_to: Vec<PublishTokenType>,
58    /// Display name of the location.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub name: Option<OcpiString<255>>,
61    /// Street/block name and house number if available.
62    pub address: OcpiString<255>,
63    /// City or town.
64    pub city: OcpiString<45>,
65    /// Postal code, omitted only where the location genuinely has none.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub postal_code: Option<OcpiString<10>>,
68    /// State or province, only where relevant.
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub state: Option<OcpiString<45>>,
71    /// ISO 3166-1 alpha-3 code for the country of this location.
72    pub country: OcpiString<3>,
73    /// Coordinates of the location.
74    pub coordinates: GeoLocation,
75    /// Geographical locations of related points relevant to the user.
76    #[serde(default, skip_serializing_if = "Vec::is_empty")]
77    #[builder(default)]
78    pub related_locations: Vec<AdditionalGeoLocation>,
79    /// The general type of parking at the charge point location.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub parking_type: Option<ParkingType>,
82    /// The EVSEs that belong to this Location.
83    #[serde(default, skip_serializing_if = "Vec::is_empty")]
84    #[builder(default)]
85    pub evses: Vec<Evse>,
86    /// Human-readable directions on how to reach the location.
87    #[serde(default, skip_serializing_if = "Vec::is_empty")]
88    #[builder(default)]
89    pub directions: Vec<DisplayText>,
90    /// Information of the operator.
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub operator: Option<BusinessDetails>,
93    /// Information of the suboperator if available.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub suboperator: Option<BusinessDetails>,
96    /// Information of the owner if available.
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub owner: Option<BusinessDetails>,
99    /// Facilities this charging location directly belongs to.
100    #[serde(default, skip_serializing_if = "Vec::is_empty")]
101    #[builder(default)]
102    pub facilities: Vec<Facility>,
103    /// One of IANA tzdata's TZ values, e.g. `Europe/Oslo`.
104    pub time_zone: OcpiString<255>,
105    /// When the EVSEs at the location can be accessed for charging.
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub opening_times: Option<Hours>,
108    /// Whether the EVSEs still charge outside the opening hours. Default: `true`.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub charging_when_closed: Option<bool>,
111    /// Links to images related to the location.
112    #[serde(default, skip_serializing_if = "Vec::is_empty")]
113    #[builder(default)]
114    pub images: Vec<Image>,
115    /// Details on the energy supplied at this location.
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub energy_mix: Option<EnergyMix>,
118    /// Timestamp when this Location or one of its EVSEs or Connectors was last updated.
119    pub last_updated: DateTime,
120    /// Undocumented JSON fields, preserved verbatim.
121    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
122    #[builder(default)]
123    pub extensions: Extensions,
124}
125
126impl Location {
127    /// The CPO that owns this Location.
128    #[must_use]
129    pub fn owner_party(&self) -> PartyRef {
130        PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
131    }
132
133    /// Whether the EVSEs keep charging outside opening hours, applying the spec's default.
134    #[must_use]
135    pub fn charging_when_closed_or_default(&self) -> bool {
136        self.charging_when_closed.unwrap_or(true)
137    }
138
139    /// Finds an EVSE by its `uid`, comparing case-insensitively.
140    #[must_use]
141    pub fn evse(&self, uid: &str) -> Option<&Evse> {
142        self.evses.iter().find(|e| e.uid.eq_ignore_case(uid))
143    }
144
145    /// Whether this Location may be shown to the holder of the given token.
146    ///
147    /// See [`v2_3_0::locations::Location::may_publish_to`](crate::v2_3_0::locations::Location::may_publish_to).
148    #[must_use]
149    pub fn may_publish_to(&self, token: Option<&PublishTokenType>) -> bool {
150        if self.publish {
151            return true;
152        }
153        token.is_some_and(|t| self.publish_allowed_to.iter().any(|allowed| allowed.matches(t)))
154    }
155}
156
157impl Validate for Location {
158    fn validate_in(&self, v: &mut Validator) {
159        validate_fields!(
160            self,
161            v,
162            country_code,
163            party_id,
164            id,
165            publish_allowed_to,
166            name,
167            address,
168            city,
169            postal_code,
170            state,
171            country,
172            coordinates,
173            related_locations,
174            parking_type,
175            evses,
176            directions,
177            operator,
178            suboperator,
179            owner,
180            facilities,
181            time_zone,
182            opening_times,
183            images,
184            energy_mix,
185            last_updated,
186        );
187        if self.publish && !self.publish_allowed_to.is_empty() {
188            v.report_at(
189                "publish_allowed_to",
190                ViolationCode::Inconsistent,
191                "this field may only be used when `publish` is false",
192            );
193        }
194    }
195}
196
197/// The part that controls the power supply to a single EV, in OCPI 2.2.1.
198///
199/// Spec: 2.2.1 §mod_locations_evse_object
200#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
201#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
202#[builder(on(_, into))]
203pub struct Evse {
204    /// Uniquely identifies the EVSE within the CPO's platform.
205    pub uid: CiString<36>,
206    /// The human-readable EVSE ID in the eMI3 format.
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub evse_id: Option<EvseId>,
209    /// The current status of the EVSE.
210    pub status: Status,
211    /// Planned status updates of the EVSE.
212    #[serde(default, skip_serializing_if = "Vec::is_empty")]
213    #[builder(default)]
214    pub status_schedule: Vec<StatusSchedule>,
215    /// Functionalities that the EVSE is capable of.
216    #[serde(default, skip_serializing_if = "Vec::is_empty")]
217    #[builder(default)]
218    pub capabilities: Vec<Capability>,
219    /// Available connectors on the EVSE. Cardinality `+`.
220    pub connectors: Vec<Connector>,
221    /// Level on which the Charge Point is located.
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    pub floor_level: Option<OcpiString<4>>,
224    /// Coordinates of the EVSE.
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub coordinates: Option<GeoLocation>,
227    /// A number/string printed on the outside of the EVSE for visual identification.
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub physical_reference: Option<OcpiString<16>>,
230    /// Directions on how to reach the EVSE from the Location.
231    #[serde(default, skip_serializing_if = "Vec::is_empty")]
232    #[builder(default)]
233    pub directions: Vec<DisplayText>,
234    /// The restrictions that apply to the parking spot.
235    #[serde(default, skip_serializing_if = "Vec::is_empty")]
236    #[builder(default)]
237    pub parking_restrictions: Vec<ParkingRestriction>,
238    /// Links to images related to the EVSE.
239    #[serde(default, skip_serializing_if = "Vec::is_empty")]
240    #[builder(default)]
241    pub images: Vec<Image>,
242    /// Timestamp when this EVSE or one of its Connectors was last updated.
243    pub last_updated: DateTime,
244    /// Undocumented JSON fields, preserved verbatim.
245    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
246    #[builder(default)]
247    pub extensions: Extensions,
248}
249
250impl Evse {
251    /// Whether a `StartSession` for this EVSE must carry a `connector_id`.
252    #[must_use]
253    pub fn requires_connector_id_on_start(&self) -> bool {
254        self.capabilities.contains(&Capability::StartSessionConnectorRequired)
255    }
256
257    /// Finds a Connector by its `id`, comparing case-insensitively.
258    #[must_use]
259    pub fn connector(&self, id: &str) -> Option<&Connector> {
260        self.connectors.iter().find(|c| c.id.eq_ignore_case(id))
261    }
262}
263
264impl Validate for Evse {
265    fn validate_in(&self, v: &mut Validator) {
266        validate_fields!(
267            self,
268            v,
269            uid,
270            evse_id,
271            status_schedule,
272            capabilities,
273            connectors,
274            floor_level,
275            coordinates,
276            physical_reference,
277            directions,
278            parking_restrictions,
279            images,
280            last_updated,
281        );
282        if self.connectors.is_empty() {
283            v.report_at(
284                "connectors",
285                ViolationCode::EmptyRequiredList,
286                "an EVSE has cardinality `+` connectors: at least one is required",
287            );
288        }
289    }
290}
291
292/// The socket, or cable and plug, available for the EV to use, in OCPI 2.2.1.
293///
294/// Spec: 2.2.1 §mod_locations_connector_object
295#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
296#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
297#[builder(on(_, into))]
298pub struct Connector {
299    /// Identifier of the Connector within the EVSE.
300    pub id: CiString<36>,
301    /// The standard of the installed connector.
302    pub standard: ConnectorType,
303    /// The format (socket/cable) of the installed connector.
304    pub format: ConnectorFormat,
305    /// Whether the connector supplies AC or DC, and on how many phases.
306    pub power_type: PowerType,
307    /// Maximum voltage of the connector, in volt.
308    pub max_voltage: i32,
309    /// Maximum amperage of the connector, in ampere.
310    pub max_amperage: i32,
311    /// Maximum electric power this connector can deliver, in watt.
312    #[serde(default, skip_serializing_if = "Option::is_none")]
313    pub max_electric_power: Option<i32>,
314    /// Identifiers of the currently valid charging tariffs.
315    #[serde(default, skip_serializing_if = "Vec::is_empty")]
316    #[builder(default)]
317    pub tariff_ids: Vec<CiString<36>>,
318    /// URL to the operator's terms and conditions.
319    #[serde(default, skip_serializing_if = "Option::is_none")]
320    pub terms_and_conditions: Option<Url>,
321    /// Timestamp when this Connector was last updated.
322    pub last_updated: DateTime,
323    /// Undocumented JSON fields, preserved verbatim.
324    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
325    #[builder(default)]
326    pub extensions: Extensions,
327}
328
329impl Validate for Connector {
330    fn validate_in(&self, v: &mut Validator) {
331        validate_fields!(
332            self,
333            v,
334            id,
335            standard,
336            format,
337            power_type,
338            tariff_ids,
339            terms_and_conditions,
340            last_updated,
341        );
342        if self.max_voltage <= 0 {
343            v.report_at("max_voltage", ViolationCode::OutOfRange, "must be a positive voltage");
344        }
345        if self.max_amperage <= 0 {
346            v.report_at("max_amperage", ViolationCode::OutOfRange, "must be a positive amperage");
347        }
348    }
349}
350
351/// The set of values that identify a token to which a Location might be published.
352///
353/// Identical in shape to the 2.3.0 object, but its `type` is the 2.2.1
354/// [`TokenType`], which has no `EMAID`.
355///
356/// Spec: 2.2.1 §mod_locations_publish_token_class
357#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Builder)]
358#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
359#[builder(on(_, into))]
360pub struct PublishTokenType {
361    /// Unique ID by which this Token can be identified.
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub uid: Option<CiString<36>>,
364    /// Type of the token.
365    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
366    pub token_type: Option<TokenType>,
367    /// Visual readable number/identification as printed on the Token.
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub visual_number: Option<OcpiString<64>>,
370    /// Issuing company, most of the time the name printed on the token.
371    #[serde(default, skip_serializing_if = "Option::is_none")]
372    pub issuer: Option<OcpiString<64>>,
373    /// Groups a couple of tokens so that two or more tokens work as one.
374    #[serde(default, skip_serializing_if = "Option::is_none")]
375    pub group_id: Option<CiString<36>>,
376    /// Undocumented JSON fields, preserved verbatim.
377    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
378    #[builder(default)]
379    pub extensions: Extensions,
380}
381
382impl PublishTokenType {
383    /// Whether `candidate` matches all the set fields of this publish token.
384    #[must_use]
385    pub fn matches(&self, candidate: &Self) -> bool {
386        fn agree<T: PartialEq>(required: Option<&T>, given: Option<&T>) -> bool {
387            required.is_none_or(|r| given == Some(r))
388        }
389        agree(self.uid.as_ref(), candidate.uid.as_ref())
390            && agree(self.token_type.as_ref(), candidate.token_type.as_ref())
391            && agree(self.visual_number.as_ref(), candidate.visual_number.as_ref())
392            && agree(self.issuer.as_ref(), candidate.issuer.as_ref())
393            && agree(self.group_id.as_ref(), candidate.group_id.as_ref())
394    }
395}
396
397impl Validate for PublishTokenType {
398    fn validate_in(&self, v: &mut Validator) {
399        validate_fields!(self, v, uid, token_type as "type", visual_number, issuer, group_id);
400        if self.uid.is_none() && self.visual_number.is_none() && self.group_id.is_none() {
401            v.report(
402                ViolationCode::MissingConditional,
403                "at least one of `uid`, `visual_number` or `group_id` SHALL be set",
404            );
405        }
406        if self.uid.is_some() && self.token_type.is_none() {
407            v.report_at("type", ViolationCode::MissingConditional, "SHALL be set when `uid` is set");
408        }
409        if self.visual_number.is_some() && self.issuer.is_none() {
410            v.report_at(
411                "issuer",
412                ViolationCode::MissingConditional,
413                "SHALL be set when `visual_number` is set",
414            );
415        }
416    }
417}
418
419ocpi_lenient_enum! {
420    /// The socket or plug standard of the charging point, in OCPI 2.2.1.
421    ///
422    /// The 2.3.0 list plus `MCS` and `SAE_J3400`, minus those two. OCPI 2.2.1 declares this a
423    /// closed enum; OCPI 2.3.0 reclassified it as an `OpenEnum`, which is why this crate keeps an
424    /// unrecognised value instead of failing the object. See [`ocpi_lenient_enum!`].
425    ///
426    /// Spec: 2.2.1 §mod_locations_connectortype_enum
427    pub enum ConnectorType {
428        /// CHAdeMO, DC.
429        Chademo = "CHADEMO",
430        /// The ChaoJi connector, harmonized between CHAdeMO and GB/T. DC.
431        ChaoJi = "CHAOJI",
432        /// Standard/Domestic household, type "A", NEMA 1-15, 2 pins.
433        DomesticA = "DOMESTIC_A",
434        /// Standard/Domestic household, type "B", NEMA 5-15, 3 pins.
435        DomesticB = "DOMESTIC_B",
436        /// Standard/Domestic household, type "C", CEE 7/17, 2 pins.
437        DomesticC = "DOMESTIC_C",
438        /// Standard/Domestic household, type "D", 3 pin.
439        DomesticD = "DOMESTIC_D",
440        /// Standard/Domestic household, type "E", CEE 7/5, 3 pins.
441        DomesticE = "DOMESTIC_E",
442        /// Standard/Domestic household, type "F", CEE 7/4, Schuko, 3 pins.
443        DomesticF = "DOMESTIC_F",
444        /// Standard/Domestic household, type "G", BS 1363, Commonwealth, 3 pins.
445        DomesticG = "DOMESTIC_G",
446        /// Standard/Domestic household, type "H", SI-32, 3 pins.
447        DomesticH = "DOMESTIC_H",
448        /// Standard/Domestic household, type "I", AS 3112, 3 pins.
449        DomesticI = "DOMESTIC_I",
450        /// Standard/Domestic household, type "J", SEV 1011, 3 pins.
451        DomesticJ = "DOMESTIC_J",
452        /// Standard/Domestic household, type "K", DS 60884-2-D1, 3 pins.
453        DomesticK = "DOMESTIC_K",
454        /// Standard/Domestic household, type "L", CEI 23-16-VII, 3 pins.
455        DomesticL = "DOMESTIC_L",
456        /// Standard/Domestic household, type "M", BS 546, 3 pins.
457        DomesticM = "DOMESTIC_M",
458        /// Standard/Domestic household, type "N", NBR 14136, 3 pins.
459        DomesticN = "DOMESTIC_N",
460        /// Standard/Domestic household, type "O", TIS 166-2549, 3 pins.
461        DomesticO = "DOMESTIC_O",
462        /// Guobiao GB/T 20234.2 AC socket/connector.
463        GbtAc = "GBT_AC",
464        /// Guobiao GB/T 20234.3 DC connector.
465        GbtDc = "GBT_DC",
466        /// IEC 60309-2 Industrial Connector single phase 16 A.
467        Iec603092Single16 = "IEC_60309_2_single_16",
468        /// IEC 60309-2 Industrial Connector three phases 16 A.
469        Iec603092Three16 = "IEC_60309_2_three_16",
470        /// IEC 60309-2 Industrial Connector three phases 32 A.
471        Iec603092Three32 = "IEC_60309_2_three_32",
472        /// IEC 60309-2 Industrial Connector three phases 64 A.
473        Iec603092Three64 = "IEC_60309_2_three_64",
474        /// IEC 62196 Type 1 "SAE J1772".
475        Iec62196T1 = "IEC_62196_T1",
476        /// Combo Type 1 based, DC.
477        Iec62196T1Combo = "IEC_62196_T1_COMBO",
478        /// IEC 62196 Type 2 "Mennekes".
479        Iec62196T2 = "IEC_62196_T2",
480        /// Combo Type 2 based, DC.
481        Iec62196T2Combo = "IEC_62196_T2_COMBO",
482        /// IEC 62196 Type 3A.
483        Iec62196T3A = "IEC_62196_T3A",
484        /// IEC 62196 Type 3C "Scame".
485        Iec62196T3C = "IEC_62196_T3C",
486        /// NEMA 5-20, 3 pins.
487        Nema520 = "NEMA_5_20",
488        /// NEMA 6-30, 3 pins.
489        Nema630 = "NEMA_6_30",
490        /// NEMA 6-50, 3 pins.
491        Nema650 = "NEMA_6_50",
492        /// NEMA 10-30, 3 pins.
493        Nema1030 = "NEMA_10_30",
494        /// NEMA 10-50, 3 pins.
495        Nema1050 = "NEMA_10_50",
496        /// NEMA 14-30, 3 pins, rating of 30 A.
497        Nema1430 = "NEMA_14_30",
498        /// NEMA 14-50, 3 pins, rating of 50 A.
499        Nema1450 = "NEMA_14_50",
500        /// On-board bottom-up pantograph, typically for bus charging.
501        PantographBottomUp = "PANTOGRAPH_BOTTOM_UP",
502        /// Off-board top-down pantograph, typically for bus charging.
503        PantographTopDown = "PANTOGRAPH_TOP_DOWN",
504        /// Tesla Connector "Roadster"-type (round, 4 pin).
505        TeslaR = "TESLA_R",
506        /// Tesla Connector "Model-S"-type (oval, 5 pin).
507        TeslaS = "TESLA_S",
508    }
509}
510
511ocpi_lenient_enum! {
512    /// Restrictions on the parking spot, in OCPI 2.2.1.
513    ///
514    /// OCPI 2.3.0 added `EMPLOYEES`, `TAXIS` and `TENANTS` and made the enum open.
515    ///
516    /// Spec: 2.2.1 §mod_locations_parkingrestriction_enum
517    pub enum ParkingRestriction {
518        /// Reserved parking spot for electric vehicles.
519        EvOnly = "EV_ONLY",
520        /// Parking is only allowed while plugged in (charging).
521        Plugged = "PLUGGED",
522        /// Reserved parking spot for disabled people with a valid ID.
523        Disabled = "DISABLED",
524        /// Parking spot for customers or guests only.
525        Customers = "CUSTOMERS",
526        /// Parking spot only suitable for (electric) motorcycles or scooters.
527        Motorcycles = "MOTORCYCLES",
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    #[test]
536    fn the_2_3_0_connector_types_are_absent_but_still_decode() {
537        assert!(ConnectorType::ALL_KNOWN_WIRE.iter().all(|v| *v != "MCS"));
538        let mcs: ConnectorType = "MCS".into();
539        assert!(!mcs.is_known(), "MCS arrived in OCPI 2.3.0");
540        // Decoding must still succeed: one unknown plug cannot lose a page of Locations …
541        assert_eq!(serde_json::to_string(&mcs).unwrap(), "\"MCS\"");
542        // … but a conformance report says the peer sent something 2.2.1 does not define.
543        assert!(mcs.validate().is_err());
544        assert!(ConnectorType::Iec62196T2.validate().is_ok());
545    }
546
547    #[test]
548    fn the_2_3_0_parking_restrictions_are_absent() {
549        assert_eq!(ConnectorType::ALL_KNOWN.len(), 40);
550        assert_eq!(ParkingRestriction::ALL_KNOWN.len(), 5);
551        assert!(!ParkingRestriction::from("TENANTS").is_known());
552    }
553
554    #[test]
555    fn wire_identical_types_are_the_same_rust_type_in_both_versions() {
556        // A GeoLocation needs no conversion between 2.2.1 and 2.3.0 because it is one type.
557        let geo: GeoLocation = crate::v2_3_0::locations::GeoLocation::new("52.010", "4.35000").unwrap();
558        let _: crate::v2_3_0::locations::GeoLocation = geo;
559    }
560}