Skip to main content

ocpi_tariffs/
guess.rs

1//! Guess the `Version` of the given `CDR` or tariff.
2#[cfg(test)]
3mod test;
4
5#[cfg(test)]
6mod test_guess_cdr;
7
8#[cfg(test)]
9mod test_guess_tariff;
10
11#[cfg(test)]
12mod test_real_world;
13
14use std::fmt;
15
16use tracing::debug;
17
18use crate::{cdr, json, tariff, v211, v221, ParseError, ReasonableStr, Unversioned, Versioned};
19
20/// The result of calling `cdr::parse`.
21pub type CdrVersion<'buf> = Version<cdr::Versioned<'buf>, cdr::Unversioned<'buf>>;
22
23/// The result of calling `cdr::parse_and_report`.
24pub type CdrReport<'buf> = Report<'buf, cdr::Versioned<'buf>, cdr::Unversioned<'buf>>;
25
26/// Guess the `Version` of the given CDR.
27///
28/// See: `cdr::guess_version`.
29pub(crate) fn cdr_version(cdr_json: ReasonableStr<'_>) -> Result<CdrVersion<'_>, ParseError> {
30    guess_cdr_version(cdr_json)
31}
32
33/// Guess the `Version` of the given CDR and report on any unexpected fields in the JSON.
34///
35/// See: `cdr::guess_version_with_report`.
36pub(crate) fn cdr_version_and_report(
37    cdr_json: ReasonableStr<'_>,
38) -> Result<CdrReport<'_>, ParseError> {
39    let version = guess_cdr_version(cdr_json)?;
40
41    let Version::Certain(cdr) = version else {
42        return Ok(Report {
43            version,
44            unexpected_fields: json::UnexpectedFields::empty(),
45        });
46    };
47
48    let schema = match cdr.version() {
49        crate::Version::V211 => &v211::CDR_SCHEMA,
50        crate::Version::V221 => &v221::CDR_SCHEMA,
51    };
52
53    let report = json::parse_with_schema(cdr_json, schema).map_err(ParseError::from_cdr_err)?;
54    let json::ParseReport {
55        element: _,
56        unexpected_fields,
57    } = report;
58
59    Ok(CdrReport {
60        unexpected_fields,
61        version: Version::Certain(cdr),
62    })
63}
64
65/// Try to guess a CDR's [`Version`] and return a [`CdrVersion`] with the outcome.
66fn guess_cdr_version(source: ReasonableStr<'_>) -> Result<CdrVersion<'_>, ParseError> {
67    /// The list of field names exclusively defined in the `v221` spec.
68    const V211_EXCLUSIVE_FIELDS: &[&str] = &["stop_date_time"];
69
70    /// The list of field names shared between the `v211` and `v221` specs.
71    const V221_EXCLUSIVE_FIELDS: &[&str] = &["end_date_time", "cdr_location", "cdr_token"];
72
73    let element = json::parse(source).map_err(ParseError::from_cdr_err)?;
74    let value = element.value();
75    let json::Value::Object(fields) = value else {
76        return Err(ParseError::cdr_should_be_object());
77    };
78
79    let source = source.into_inner();
80
81    for field in fields {
82        let key = field.key().as_raw();
83
84        if V211_EXCLUSIVE_FIELDS.contains(&key) {
85            return Ok(Version::Certain(cdr::Versioned::new(
86                source,
87                element,
88                crate::Version::V211,
89            )));
90        } else if V221_EXCLUSIVE_FIELDS.contains(&key) {
91            return Ok(Version::Certain(cdr::Versioned::new(
92                source,
93                element,
94                crate::Version::V221,
95            )));
96        }
97    }
98
99    Ok(Version::Uncertain(cdr::Unversioned::new(source, element)))
100}
101
102/// The result of calling `tariff::parse`.
103pub type TariffVersion<'buf> = Version<tariff::Versioned<'buf>, tariff::Unversioned<'buf>>;
104
105/// The result of calling `tariff::parse_and_report`.
106pub type TariffReport<'buf> = Report<'buf, tariff::Versioned<'buf>, tariff::Unversioned<'buf>>;
107
108/// Guess the `Version` of the given tariff.
109///
110/// See: [`tariff::parse`]
111pub(super) fn tariff_version(
112    tariff_json: ReasonableStr<'_>,
113) -> Result<TariffVersion<'_>, ParseError> {
114    guess_tariff_version(tariff_json)
115}
116
117/// Guess the `Version` of the given tariff and report on any unexpected fields in the JSON.
118///
119/// See: [`tariff::parse_and_report`]
120pub(super) fn tariff_version_with_report(
121    tariff_json: ReasonableStr<'_>,
122) -> Result<TariffReport<'_>, ParseError> {
123    let version = guess_tariff_version(tariff_json)?;
124
125    let Version::Certain(object) = version else {
126        return Ok(Report {
127            version,
128            unexpected_fields: json::UnexpectedFields::empty(),
129        });
130    };
131
132    let schema = match object.version() {
133        crate::Version::V211 => &v211::TARIFF_SCHEMA,
134        crate::Version::V221 => &v221::TARIFF_SCHEMA,
135    };
136
137    let report =
138        json::parse_with_schema(tariff_json, schema).map_err(ParseError::from_tariff_err)?;
139    let json::ParseReport {
140        element: _,
141        unexpected_fields,
142    } = report;
143
144    Ok(TariffReport {
145        unexpected_fields,
146        version: Version::Certain(object),
147    })
148}
149
150/// The private impl for detecting a tariff's version
151fn guess_tariff_version(source: ReasonableStr<'_>) -> Result<TariffVersion<'_>, ParseError> {
152    /// The list of field names exclusively defined in the `v221` spec.
153    const V221_EXCLUSIVE_FIELDS: &[&str] = &[
154        "country_code",
155        "party_id",
156        "type",
157        "min_price",
158        "max_price",
159        "start_date_time",
160        "end_date_time",
161    ];
162
163    /// The list of field names shared between the `v211` and `v221` specs.
164    const V211_V221_SHARED_FIELDS: &[&str] = &[
165        "id",
166        "currency",
167        "tariff_alt_text",
168        "tariff_alt_url",
169        "elements",
170        "energy_mix",
171        "last_updated",
172    ];
173
174    // Parse the tariff without a schema first to determine the version.
175    let element = json::parse(source).map_err(ParseError::from_tariff_err)?;
176    let value = element.value();
177    let json::Value::Object(fields) = value else {
178        return Err(ParseError::tariff_should_be_object());
179    };
180    let source = source.into_inner();
181
182    for field in fields {
183        let key = field.key().as_raw();
184
185        // If the field is in the v221 exclusive list we know without a doubt that it's a v221 tariff.
186        if V221_EXCLUSIVE_FIELDS.contains(&key) {
187            debug!("Tariff is v221 because of field: `{key}`");
188            return Ok(TariffVersion::Certain(tariff::Versioned::new(
189                source,
190                element,
191                crate::Version::V221,
192            )));
193        }
194    }
195
196    for field in fields {
197        let key = field.key().as_raw();
198
199        if V211_V221_SHARED_FIELDS.contains(&key) {
200            return Ok(TariffVersion::Certain(tariff::Versioned::new(
201                source,
202                element,
203                crate::Version::V211,
204            )));
205        }
206    }
207
208    Ok(TariffVersion::Uncertain(tariff::Unversioned::new(
209        source, element,
210    )))
211}
212
213/// An OCPI object with a certain or uncertain version.
214#[derive(Debug)]
215pub enum Version<V, U>
216where
217    V: Versioned,
218    U: fmt::Debug,
219{
220    /// The version of the object `V` is certain.
221    Certain(V),
222
223    /// The version of the object `U` is uncertain.
224    Uncertain(U),
225}
226
227impl<V, U> Version<V, U>
228where
229    V: Versioned,
230    U: Unversioned<Versioned = V>,
231{
232    /// Convert the OCPI object into it's [`Version`](crate::Version) if it's [`Versioned`].
233    /// Otherwise, return `Uncertain(())`.
234    pub fn into_version(self) -> Version<crate::Version, ()> {
235        match self {
236            Version::Certain(v) => Version::Certain(v.version()),
237            Version::Uncertain(_) => Version::Uncertain(()),
238        }
239    }
240
241    /// Convert a reference to the an OCPI object into it's [`Version`](crate::Version) if it's [`Versioned`].
242    /// Otherwise, return `Uncertain(())`.
243    pub fn as_version(&self) -> Version<crate::Version, ()> {
244        match self {
245            Version::Certain(v) => Version::Certain(v.version()),
246            Version::Uncertain(_) => Version::Uncertain(()),
247        }
248    }
249
250    /// Return the contained OCPI object if it's [`Versioned`]. Otherwise, force convert the object
251    /// to the given [`Version`](crate::Version).
252    pub fn certain_or(self, fallback: crate::Version) -> V {
253        match self {
254            Version::Certain(v) => v,
255            Version::Uncertain(u) => u.force_into_versioned(fallback),
256        }
257    }
258
259    /// Return `Some` OCPI object if it's [`Versioned`]. Otherwise, return None if the object's version is uncertain.
260    pub fn certain_or_none(self) -> Option<V> {
261        match self {
262            Version::Certain(v) => Some(v),
263            Version::Uncertain(_) => None,
264        }
265    }
266}
267
268/// The guess version report.
269///
270/// This contains the guessed version of the given JSON object and a list of unexpected fields
271/// if the JSON contains fields not specified in the guessed version of the OCPI spec.
272///
273/// The `unexpected_fields` list will always be empty if the guessed `Version` is `Uncertain`.
274#[derive(Debug)]
275pub struct Report<'buf, V, U>
276where
277    V: Versioned,
278    U: fmt::Debug,
279{
280    /// A list of fields that were not expected: The schema did not define them.
281    ///
282    /// This list will always be empty if the guessed `Version` is `Uncertain`.
283    pub unexpected_fields: json::UnexpectedFields<'buf>,
284
285    /// The guessed version.
286    ///
287    /// The `unexpected_fields` list will always be empty if the guessed `Version` is `Uncertain`.
288    pub version: Version<V, U>,
289}