ocpi-tariffs 0.49.0

OCPI tariff calculations
Documentation
//! Guess the `Version` of the given `CDR` or tariff.

#[cfg(test)]
mod test;

#[cfg(test)]
mod test_guess_cdr;

#[cfg(test)]
mod test_guess_tariff;

#[cfg(test)]
mod test_real_world;

use std::fmt;

use tracing::debug;

use crate::{cdr, json, tariff, Unversioned, Versioned};

/// The result of calling [`cdr::infer_version`].
pub type CdrVersion<'buf> = Version<cdr::VersionedJson<'buf>, cdr::Unversioned<'buf>>;

/// Guess the `Version` of the given CDR [`json::Document`].
pub(crate) fn cdr_version(doc: json::Document<'_>) -> CdrVersion<'_> {
    guess_cdr_version(doc)
}

/// Try to guess a CDR's [`Version`] and return a [`CdrVersion`] with the outcome.
fn guess_cdr_version(doc: json::Document<'_>) -> CdrVersion<'_> {
    /// Fields present in `v2.1.1` CDR that do not exist in `v2.2.1`.
    /// `auth_id` and `location` were replaced by `cdr_token` and `cdr_location`.
    const V211_EXCLUSIVE_FIELDS: &[&str] = &["auth_id", "location", "stop_date_time"];

    /// Fields introduced in `v2.2.1` CDR that do not exist in `v2.1.1`.
    /// Sorted alphabetically; required fields marked for reference.
    const V221_EXCLUSIVE_FIELDS: &[&str] = &[
        "authorization_reference",
        "cdr_location", // Required in `v2.2.1`
        "cdr_token",    // Required in `v2.2.1`
        "country_code", // Required in `v2.2.1`
        "credit",
        "credit_reference_id",
        "end_date_time", // Required in `v2.2.1`
        "home_charging_compensation",
        "invoice_reference_id",
        "party_id", // Required in `v2.2.1`
        "session_id",
        "signed_data",
        "total_energy_cost",
        "total_fixed_cost",
        "total_parking_cost",
        "total_reservation_cost",
        "total_time_cost",
    ];

    // The `Document` is expected to be an object (see `json::parse_object`); if it is not,
    // there are no fields to inspect so the version stays uncertain.
    let Some(fields) = doc.root().as_object_fields() else {
        return Version::Uncertain(cdr::Unversioned::new(doc));
    };

    for field in fields {
        let key = field.key();

        if key.eq_any_escape_aware(V211_EXCLUSIVE_FIELDS) {
            return Version::Certain(cdr::VersionedJson::new(doc, crate::Version::V211));
        } else if key.eq_any_escape_aware(V221_EXCLUSIVE_FIELDS) {
            return Version::Certain(cdr::VersionedJson::new(doc, crate::Version::V221));
        }
    }

    Version::Uncertain(cdr::Unversioned::new(doc))
}

/// The result of calling [`tariff::infer_version`].
pub type TariffVersion<'buf> = Version<tariff::VersionedJson<'buf>, tariff::Unversioned<'buf>>;

/// Guess the `Version` of the given tariff [`json::Document`].
pub(crate) fn tariff_version(doc: json::Document<'_>) -> TariffVersion<'_> {
    guess_tariff_version(doc)
}

/// The private impl for detecting a tariff's version.
fn guess_tariff_version(doc: json::Document<'_>) -> TariffVersion<'_> {
    /// Fields introduced in `v2.2.1` Tariff that do not exist in `v2.1.1.`.
    const V221_EXCLUSIVE_FIELDS: &[&str] = &[
        "country_code", // Required in `v2.2.1`
        "end_date_time",
        "max_price",
        "min_price",
        "party_id", // Required in `v2.2.1`
        "start_date_time",
        "type",
    ];

    /// Fields present in both `v2.1.1` and `v2.2.1` Tariff.
    /// Seeing any of these — without a `v2.2.1` exclusive field — confirms the object
    /// is a `v2.1.1` tariff rather than an unrecognized document.
    const V211_V221_SHARED_FIELDS: &[&str] = &[
        "currency",
        "elements",
        "energy_mix",
        "id",
        "last_updated",
        "tariff_alt_text",
        "tariff_alt_url",
    ];

    // The `Document` is expected to be an object (see `json::parse_object`); if it is not,
    // there are no fields to inspect so the version stays uncertain.
    let Some(fields) = doc.root().as_object_fields() else {
        return Version::Uncertain(tariff::Unversioned::new(doc));
    };

    let mut seen_known_field = false;
    for field in fields {
        let key = field.key();
        if key.eq_any_escape_aware(V221_EXCLUSIVE_FIELDS) {
            debug!(
                "Tariff is v221 because of field: `{}`",
                key.as_unescaped_str()
            );
            return TariffVersion::Certain(tariff::VersionedJson::new(doc, crate::Version::V221));
        }
        if key.eq_any_escape_aware(V211_V221_SHARED_FIELDS) {
            seen_known_field = true;
        }
    }

    if seen_known_field {
        return TariffVersion::Certain(tariff::VersionedJson::new(doc, crate::Version::V211));
    }

    Version::Uncertain(tariff::Unversioned::new(doc))
}

/// An OCPI object with a certain or uncertain version.
#[derive(Debug)]
pub enum Version<V, U>
where
    V: Versioned,
    U: fmt::Debug,
{
    /// The version of the object `V` is certain.
    Certain(V),

    /// The version of the object `U` is uncertain.
    Uncertain(U),
}

impl<V, U> Version<V, U>
where
    V: Versioned,
    U: Unversioned<Versioned = V>,
{
    /// Convert the OCPI object into it's [`Version`](crate::Version) if it's [`Versioned`].
    /// Otherwise, return `Uncertain(())`.
    pub fn into_version(self) -> Version<crate::Version, ()> {
        match self {
            Version::Certain(v) => Version::Certain(v.version()),
            Version::Uncertain(_) => Version::Uncertain(()),
        }
    }

    /// Convert a reference to the an OCPI object into it's [`Version`](crate::Version) if it's [`Versioned`].
    /// Otherwise, return `Uncertain(())`.
    pub fn as_version(&self) -> Version<crate::Version, ()> {
        match self {
            Version::Certain(v) => Version::Certain(v.version()),
            Version::Uncertain(_) => Version::Uncertain(()),
        }
    }

    /// Return the contained OCPI object if it's [`Versioned`]. Otherwise, force convert the object
    /// to the given [`Version`](crate::Version).
    pub fn certain_or(self, fallback: crate::Version) -> V {
        match self {
            Version::Certain(v) => v,
            Version::Uncertain(u) => u.force_into_versioned(fallback),
        }
    }

    /// Return `Some` OCPI object if it's [`Versioned`]. Otherwise, return None if the object's version is uncertain.
    pub fn certain_or_none(self) -> Option<V> {
        match self {
            Version::Certain(v) => Some(v),
            Version::Uncertain(_) => None,
        }
    }
}