use std::fmt;
use tracing::debug;
use crate::{cdr, json, tariff, v211, v221, ParseError, Unversioned, Versioned};
pub type CdrVersion<'buf> = Version<cdr::Versioned<'buf>, cdr::Unversioned<'buf>>;
pub type CdrReport<'buf> = Report<'buf, cdr::Versioned<'buf>, cdr::Unversioned<'buf>>;
pub(crate) fn cdr_version(cdr_json: &str) -> Result<CdrVersion<'_>, ParseError> {
cdr_version_intern(cdr_json)
}
pub(crate) fn cdr_version_and_report(cdr_json: &str) -> Result<CdrReport<'_>, ParseError> {
let version = cdr_version_intern(cdr_json)?;
let Version::Certain(object) = version else {
return Ok(Report {
version,
unexpected_fields: json::UnexpectedFields::empty(),
});
};
let schema = match object {
cdr::Versioned::V211(_) => &v211::CDR_SCHEMA,
cdr::Versioned::V221(_) => &v221::CDR_SCHEMA,
};
let report = json::parse_with_schema(cdr_json, schema).map_err(ParseError::from_cdr_err)?;
let json::Report {
element: _,
unexpected_fields,
} = report;
Ok(CdrReport {
unexpected_fields,
version: Version::Certain(object),
})
}
fn cdr_version_intern(cdr_json: &str) -> Result<CdrVersion<'_>, ParseError> {
let element = json::parse(cdr_json).map_err(ParseError::from_cdr_err)?;
let value = element.value();
let json::Value::Object(fields) = value else {
return Err(ParseError::cdr_should_be_object());
};
for field in fields {
let key = field.key().as_raw();
if key.eq_ignore_ascii_case("stop_date_time") {
return Ok(Version::Certain(cdr::Versioned::V211(element)));
} else if key.eq_ignore_ascii_case("end_date_time")
|| key.eq_ignore_ascii_case("cdr_location")
|| key.eq_ignore_ascii_case("cdr_token")
{
return Ok(Version::Certain(cdr::Versioned::V221(element)));
}
}
Ok(Version::Uncertain(cdr::Unversioned::new(element)))
}
pub type TariffVersion<'buf> = Version<tariff::Versioned<'buf>, tariff::Unversioned<'buf>>;
pub type TariffReport<'buf> = Report<'buf, tariff::Versioned<'buf>, tariff::Unversioned<'buf>>;
pub(crate) fn tariff_version(tariff_json: &str) -> Result<TariffVersion<'_>, ParseError> {
tariff_version_intern(tariff_json)
}
pub(crate) fn tariff_version_with_report(
tariff_json: &str,
) -> Result<TariffReport<'_>, ParseError> {
let version = tariff_version_intern(tariff_json)?;
let Version::Certain(object) = version else {
return Ok(Report {
version,
unexpected_fields: json::UnexpectedFields::empty(),
});
};
let schema = match object {
tariff::Versioned::V211(_) => &v211::TARIFF_SCHEMA,
tariff::Versioned::V221(_) => &v221::TARIFF_SCHEMA,
};
let report =
json::parse_with_schema(tariff_json, schema).map_err(ParseError::from_tariff_err)?;
let json::Report {
element: _,
unexpected_fields,
} = report;
Ok(TariffReport {
unexpected_fields,
version: Version::Certain(object),
})
}
fn tariff_version_intern(tariff_json: &str) -> Result<TariffVersion<'_>, ParseError> {
const V221_EXCLUSIVE_FIELDS: &[&str] = &[
"country_code",
"party_id",
"type",
"min_price",
"max_price",
"start_date_time",
"end_date_time",
];
const V211_V221_SHARED_FIELDS: &[&str] = &[
"id",
"currency",
"tariff_alt_text",
"tariff_alt_url",
"elements",
"energy_mix",
"last_updated",
];
let element = json::parse(tariff_json).map_err(ParseError::from_tariff_err)?;
let value = element.value();
let json::Value::Object(fields) = value else {
return Err(ParseError::tariff_should_be_object());
};
for field in fields {
let key = field.key().as_raw();
if V221_EXCLUSIVE_FIELDS.contains(&key) {
debug!("Tariff is v221 because of field: `{key}`");
return Ok(TariffVersion::Certain(tariff::Versioned::V221(element)));
}
}
for field in fields {
let key = field.key().as_raw();
if V211_V221_SHARED_FIELDS.contains(&key) {
return Ok(TariffVersion::Certain(tariff::Versioned::V211(element)));
}
}
Ok(TariffVersion::Uncertain(tariff::Unversioned::new(element)))
}
#[derive(Debug)]
pub enum Version<V, U>
where
V: Versioned,
U: fmt::Debug,
{
Certain(V),
Uncertain(U),
}
impl<V, U> Version<V, U>
where
V: Versioned,
U: Unversioned<Versioned = V>,
{
pub fn into_version(self) -> Version<crate::Version, ()> {
match self {
Version::Certain(v) => Version::Certain(v.version()),
Version::Uncertain(_) => Version::Uncertain(()),
}
}
pub fn as_version(&self) -> Version<crate::Version, ()> {
match self {
Version::Certain(v) => Version::Certain(v.version()),
Version::Uncertain(_) => Version::Uncertain(()),
}
}
pub fn certain_or(self, fallback: crate::Version) -> V {
match self {
Version::Certain(v) => v,
Version::Uncertain(u) => u.force_into_versioned(fallback),
}
}
pub fn certain_or_none(self) -> Option<V> {
match self {
Version::Certain(v) => Some(v),
Version::Uncertain(_) => None,
}
}
}
#[derive(Debug)]
pub struct Report<'buf, V, U>
where
V: Versioned,
U: fmt::Debug,
{
unexpected_fields: json::UnexpectedFields<'buf>,
version: Version<V, U>,
}
impl<'buf, V, U> Report<'buf, V, U>
where
V: Versioned,
U: fmt::Debug,
{
pub fn version(&self) -> &Version<V, U> {
&self.version
}
pub fn into_parts(self) -> (Version<V, U>, json::UnexpectedFields<'buf>) {
let Self {
unexpected_fields,
version,
} = self;
(version, unexpected_fields)
}
}
#[cfg(test)]
mod test {
use super::{Unversioned, Version, Versioned};
impl<V, U> Version<V, U>
where
V: Versioned,
U: Unversioned<Versioned = V>,
{
pub fn unwrap_certain(self) -> V {
match self {
Version::Certain(v) => v,
Version::Uncertain(_) => panic!("The version of the object is unknown"),
}
}
}
}
#[cfg(test)]
mod test_guess_cdr {
use assert_matches::assert_matches;
use crate::{test, Versioned as _};
use super::{cdr_version_and_report, Report, Version};
#[test]
fn should_guess_cdr_version_v211() {
const JSON: &str = include_str!("../test_data/v211/lint/every_field_set/cdr.json");
test::setup();
let Report {
version,
unexpected_fields,
} = cdr_version_and_report(JSON).unwrap();
let cdr = assert_matches!(version, Version::Certain ( cdr ) => cdr );
assert_matches!(cdr.version(), crate::Version::V211);
test::assert_no_unexpected_fields(&unexpected_fields);
}
#[test]
fn should_guess_cdr_version_v221() {
const JSON: &str = include_str!("../test_data/v221/lint/every_field_set/cdr.json");
test::setup();
let Report {
version,
unexpected_fields,
} = cdr_version_and_report(JSON).unwrap();
let cdr = assert_matches!(version, Version::Certain ( cdr ) => cdr );
assert_matches!(cdr.version(), crate::Version::V221);
test::assert_no_unexpected_fields(&unexpected_fields);
}
}
#[cfg(test)]
mod test_guess_tariff {
use assert_matches::assert_matches;
use crate::{test, Versioned as _};
use super::{tariff_version_with_report, Report, Version};
#[test]
fn should_guess_tariff_version_v211() {
const JSON: &str = include_str!("../test_data/v211/lint/every_field_set/tariff.json");
test::setup();
let Report {
version,
unexpected_fields,
} = tariff_version_with_report(JSON).unwrap();
let tariff = assert_matches!(version, Version::Certain ( tariff ) => tariff );
assert_matches!(tariff.version(), crate::Version::V211);
test::assert_no_unexpected_fields(&unexpected_fields);
}
#[test]
fn should_guess_tariff_version_v221() {
const JSON: &str = include_str!("../test_data/v221/lint/every_field_set/tariff.json");
test::setup();
let Report {
version,
unexpected_fields,
} = tariff_version_with_report(JSON).unwrap();
let tariff = assert_matches!(version, Version::Certain ( tariff ) => tariff );
assert_matches!(tariff.version(), crate::Version::V221);
test::assert_no_unexpected_fields(&unexpected_fields);
}
}
#[cfg(test)]
mod test_real_world {
use std::path::Path;
use assert_matches::assert_matches;
use crate::{test, Versioned as _};
use super::{cdr_version_and_report, tariff_version_with_report, Report, Version};
#[test_each::file(
glob = "ocpi-tariffs/test_data/v221/real_world/*/cdr*.json",
name(segments = 2)
)]
fn should_guess_version_v221(cdr_json: &str, path: &Path) {
test::setup();
{
let Report {
version,
unexpected_fields,
} = cdr_version_and_report(cdr_json).unwrap();
let tariff = assert_matches!(version, Version::Certain ( tariff ) => tariff );
assert_matches!(tariff.version(), crate::Version::V221);
test::assert_no_unexpected_fields(&unexpected_fields);
}
{
let tariff = std::fs::read_to_string(path.parent().unwrap().join("tariff.json")).ok();
if let Some(tariff) = tariff {
let Report {
version: guess,
unexpected_fields,
} = tariff_version_with_report(&tariff).unwrap();
let tariff = assert_matches!(guess, Version::Certain ( tariff ) => tariff);
assert_matches!(tariff.version(), crate::Version::V221);
test::assert_no_unexpected_fields(&unexpected_fields);
}
}
}
#[test_each::file(
glob = "ocpi-tariffs/test_data/v211/lint/*/cdr*.json",
name(segments = 2)
)]
fn should_guess_version_v211(cdr_json: &str, path: &Path) {
test::setup();
{
let Report {
version: guess,
unexpected_fields,
} = cdr_version_and_report(cdr_json).unwrap();
let cdr = assert_matches!(guess, Version::Certain ( cdr ) => cdr);
assert_matches!(cdr.version(), crate::Version::V211);
test::assert_no_unexpected_fields(&unexpected_fields);
}
{
let tariff_json =
std::fs::read_to_string(path.parent().unwrap().join("tariff.json")).ok();
if let Some(tariff) = tariff_json {
let Report {
version: guess,
unexpected_fields,
} = tariff_version_with_report(&tariff).unwrap();
let tariff = assert_matches!(guess, Version::Certain ( tariff ) => tariff);
assert_matches!(tariff.version(), crate::Version::V211);
test::assert_no_unexpected_fields(&unexpected_fields);
}
}
}
}