Skip to main content

ocpi_tariffs/
guess.rs

1//! Guess the `Version` of the given `CDR` or tariff.
2
3#[cfg(test)]
4mod test;
5
6#[cfg(test)]
7mod test_guess_cdr;
8
9#[cfg(test)]
10mod test_guess_tariff;
11
12#[cfg(test)]
13mod test_real_world;
14
15use std::fmt;
16
17use tracing::debug;
18
19use crate::{cdr, json, tariff, Unversioned, Versioned};
20
21/// The result of calling [`cdr::infer_version`].
22pub type CdrVersion<'buf> = Version<cdr::VersionedJson<'buf>, cdr::Unversioned<'buf>>;
23
24/// Guess the `Version` of the given CDR [`json::Document`].
25pub(crate) fn cdr_version(doc: json::Document<'_>) -> CdrVersion<'_> {
26    guess_cdr_version(doc)
27}
28
29/// Try to guess a CDR's [`Version`] and return a [`CdrVersion`] with the outcome.
30fn guess_cdr_version(doc: json::Document<'_>) -> CdrVersion<'_> {
31    /// Fields present in `v2.1.1` CDR that do not exist in `v2.2.1`.
32    /// `auth_id` and `location` were replaced by `cdr_token` and `cdr_location`.
33    const V211_EXCLUSIVE_FIELDS: &[&str] = &["auth_id", "location", "stop_date_time"];
34
35    /// Fields introduced in `v2.2.1` CDR that do not exist in `v2.1.1`.
36    /// Sorted alphabetically; required fields marked for reference.
37    const V221_EXCLUSIVE_FIELDS: &[&str] = &[
38        "authorization_reference",
39        "cdr_location", // Required in `v2.2.1`
40        "cdr_token",    // Required in `v2.2.1`
41        "country_code", // Required in `v2.2.1`
42        "credit",
43        "credit_reference_id",
44        "end_date_time", // Required in `v2.2.1`
45        "home_charging_compensation",
46        "invoice_reference_id",
47        "party_id", // Required in `v2.2.1`
48        "session_id",
49        "signed_data",
50        "total_energy_cost",
51        "total_fixed_cost",
52        "total_parking_cost",
53        "total_reservation_cost",
54        "total_time_cost",
55    ];
56
57    // The `Document` is expected to be an object (see `json::parse_object`); if it is not,
58    // there are no fields to inspect so the version stays uncertain.
59    let Some(fields) = doc.root().as_object_fields() else {
60        return Version::Uncertain(cdr::Unversioned::new(doc));
61    };
62
63    for field in fields {
64        let key = field.key();
65
66        if key.eq_any_escape_aware(V211_EXCLUSIVE_FIELDS) {
67            return Version::Certain(cdr::VersionedJson::new(doc, crate::Version::V211));
68        } else if key.eq_any_escape_aware(V221_EXCLUSIVE_FIELDS) {
69            return Version::Certain(cdr::VersionedJson::new(doc, crate::Version::V221));
70        }
71    }
72
73    Version::Uncertain(cdr::Unversioned::new(doc))
74}
75
76/// The result of calling [`tariff::infer_version`].
77pub type TariffVersion<'buf> = Version<tariff::VersionedJson<'buf>, tariff::Unversioned<'buf>>;
78
79/// Guess the `Version` of the given tariff [`json::Document`].
80pub(crate) fn tariff_version(doc: json::Document<'_>) -> TariffVersion<'_> {
81    guess_tariff_version(doc)
82}
83
84/// The private impl for detecting a tariff's version.
85fn guess_tariff_version(doc: json::Document<'_>) -> TariffVersion<'_> {
86    /// Fields introduced in `v2.2.1` Tariff that do not exist in `v2.1.1.`.
87    const V221_EXCLUSIVE_FIELDS: &[&str] = &[
88        "country_code", // Required in `v2.2.1`
89        "end_date_time",
90        "max_price",
91        "min_price",
92        "party_id", // Required in `v2.2.1`
93        "start_date_time",
94        "type",
95    ];
96
97    /// Fields present in both `v2.1.1` and `v2.2.1` Tariff.
98    /// Seeing any of these — without a `v2.2.1` exclusive field — confirms the object
99    /// is a `v2.1.1` tariff rather than an unrecognized document.
100    const V211_V221_SHARED_FIELDS: &[&str] = &[
101        "currency",
102        "elements",
103        "energy_mix",
104        "id",
105        "last_updated",
106        "tariff_alt_text",
107        "tariff_alt_url",
108    ];
109
110    // The `Document` is expected to be an object (see `json::parse_object`); if it is not,
111    // there are no fields to inspect so the version stays uncertain.
112    let Some(fields) = doc.root().as_object_fields() else {
113        return Version::Uncertain(tariff::Unversioned::new(doc));
114    };
115
116    let mut seen_known_field = false;
117    for field in fields {
118        let key = field.key();
119        if key.eq_any_escape_aware(V221_EXCLUSIVE_FIELDS) {
120            debug!(
121                "Tariff is v221 because of field: `{}`",
122                key.as_unescaped_str()
123            );
124            return TariffVersion::Certain(tariff::VersionedJson::new(doc, crate::Version::V221));
125        }
126        if key.eq_any_escape_aware(V211_V221_SHARED_FIELDS) {
127            seen_known_field = true;
128        }
129    }
130
131    if seen_known_field {
132        return TariffVersion::Certain(tariff::VersionedJson::new(doc, crate::Version::V211));
133    }
134
135    Version::Uncertain(tariff::Unversioned::new(doc))
136}
137
138/// An OCPI object with a certain or uncertain version.
139#[derive(Debug)]
140pub enum Version<V, U>
141where
142    V: Versioned,
143    U: fmt::Debug,
144{
145    /// The version of the object `V` is certain.
146    Certain(V),
147
148    /// The version of the object `U` is uncertain.
149    Uncertain(U),
150}
151
152impl<V, U> Version<V, U>
153where
154    V: Versioned,
155    U: Unversioned<Versioned = V>,
156{
157    /// Convert the OCPI object into it's [`Version`](crate::Version) if it's [`Versioned`].
158    /// Otherwise, return `Uncertain(())`.
159    pub fn into_version(self) -> Version<crate::Version, ()> {
160        match self {
161            Version::Certain(v) => Version::Certain(v.version()),
162            Version::Uncertain(_) => Version::Uncertain(()),
163        }
164    }
165
166    /// Convert a reference to the an OCPI object into it's [`Version`](crate::Version) if it's [`Versioned`].
167    /// Otherwise, return `Uncertain(())`.
168    pub fn as_version(&self) -> Version<crate::Version, ()> {
169        match self {
170            Version::Certain(v) => Version::Certain(v.version()),
171            Version::Uncertain(_) => Version::Uncertain(()),
172        }
173    }
174
175    /// Return the contained OCPI object if it's [`Versioned`]. Otherwise, force convert the object
176    /// to the given [`Version`](crate::Version).
177    pub fn certain_or(self, fallback: crate::Version) -> V {
178        match self {
179            Version::Certain(v) => v,
180            Version::Uncertain(u) => u.force_into_versioned(fallback),
181        }
182    }
183
184    /// Return `Some` OCPI object if it's [`Versioned`]. Otherwise, return None if the object's version is uncertain.
185    pub fn certain_or_none(self) -> Option<V> {
186        match self {
187            Version::Certain(v) => Some(v),
188            Version::Uncertain(_) => None,
189        }
190    }
191}