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