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