ocpi-tariffs 0.49.0

OCPI tariff calculations
Documentation
//! Parse a CDR and price the result with a tariff.

#[cfg(test)]
mod test_every_field_set;

use std::fmt;

use chrono_tz::Tz;

use crate::{
    generate, guess,
    json::{self},
    price, schema, tariff,
    warning::{Caveat, IntoCaveat as _},
    Verdict,
};

/// Infer which OCPI [`Version`] a CDR [`json::Document`] is, without validating it.
///
/// Use this when the version of the CDR is not known up front. The [`json::Document`] is obtained
/// by calling [`json::parse_object`]. The returned [`guess::CdrVersion`] is either
/// [`Certain`](guess::Version::Certain) or [`Uncertain`](guess::Version::Uncertain) about the version.
///
/// To check the CDR against the OCPI schema for a known [`Version`], use [`build`].
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{cdr, json, Version};
/// #
/// # const CDR_JSON: &str = include_str!("cdr.json");
///
/// let doc = json::parse_object(CDR_JSON)?;
/// let cdr = cdr::infer_version(doc).certain_or(Version::V211);
///
/// # Ok::<(), json::ParseError>(())
/// ```
pub fn infer_version(json: json::Document<'_>) -> guess::CdrVersion<'_> {
    guess::cdr_version(json)
}

/// Validate a [`json::Document`] against the OCPI CDR schema for the given [`Version`][^spec-v211][^spec-v221].
///
/// The [`json::Document`] is obtained by calling [`json::parse_object`]. Any unexpected, missing,
/// or wrongly typed fields are reported as a [`warning::Set`](crate::warning::Set) of
/// [`schema::Warning`]s carried by the returned [`Caveat`].
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{cdr, json, Version};
/// #
/// # const CDR_JSON: &str = include_str!("cdr.json");
///
/// let doc = json::parse_object(CDR_JSON)?;
/// let (cdr, warnings) = cdr::build(doc, Version::V211).into_parts();
///
/// if !warnings.is_empty() {
///     eprintln!("The CDR has `{}` schema warnings.", warnings.len_warnings());
/// }
///
/// # Ok::<(), json::ParseError>(())
/// ```
///
/// [^spec-v211]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md>.
/// [^spec-v221]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>.
pub fn build(
    json: json::Document<'_>,
    version: crate::Version,
) -> Caveat<Versioned<'_>, schema::Warning> {
    let (version, warnings) = match version {
        crate::Version::V221 => {
            let (cdr, warnings) = schema::v221::build_cdr(&json).into_parts();
            (Version::V221(cdr), warnings)
        }
        crate::Version::V211 => {
            let (cdr, warnings) = schema::v211::build_cdr(&json).into_parts();
            (Version::V211(cdr), warnings)
        }
    };
    let versioned = Versioned { doc: json, version };
    versioned.into_caveat(warnings)
}

/// Validate a [`VersionedJson`] against the OCPI CDR schema for its known [`Version`].
///
/// Use this when the [`Version`] has already been resolved - for example a
/// [`VersionedJson`] obtained from [`infer_version`] via [`certain_or`](guess::Version::certain_or).
pub fn build_versioned(json: VersionedJson<'_>) -> Caveat<Versioned<'_>, schema::Warning> {
    let VersionedJson { doc, version } = json;
    build(doc, version)
}

/// Generate a [`PartialCdr`](generate::PartialCdr) that can be priced by the given tariff.
///
/// The CDR is partial as not all required fields are set as the `cdr_from_tariff` function
/// does not know anything about the EVSE location or the token used to authenticate the chargesession.
///
/// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>)
pub fn generate_from_tariff(
    tariff: &tariff::Versioned<'_>,
    config: &generate::Config,
) -> Verdict<generate::Report, generate::Warning> {
    generate::cdr_from_tariff(tariff, config)
}

/// Price a single `CDR` and return a [`Report`](price::Report).
///
/// The `CDR` is checked for internal consistency before being priced. As pricing a `CDR` with
/// contradictory data will lead to a difficult to debug [`Report`](price::Report).
/// An [`Error`](price::Warning) is returned if the `CDR` is deemed to be internally inconsistent.
///
/// > **_Note_** Pricing the CDR does not require a spec compliant CDR or tariff.
/// > A best effort is made to parse the given CDR and tariff JSON.
///
/// The [`Report`](price::Report) contains the charge session priced according to the specified
/// tariff and a selection of fields from the source `CDR` that can be used for comparing the
/// source `CDR` totals with the calculated totals. The [`Report`](price::Report) also contains
/// a list of unknown fields to help spot misspelled fields.
///
/// The source of the tariffs can be controlled using the [`TariffSource`](price::TariffSource).
/// The timezone can be found or inferred using the [`timezone::find_or_infer`](crate::timezone::find_or_infer) function.
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{cdr, json, price, warning, Version};
/// #
/// # const CDR_JSON: &str = include_str!("cdr.json");
///
/// let doc = json::parse_object(CDR_JSON)?;
/// let (cdr, _warnings) = cdr::build(doc, Version::V211).into_parts();
///
/// let report = cdr::price(&cdr, price::TariffSource::UseCdr, chrono_tz::Tz::Europe__Amsterdam).unwrap();
/// let (report, warnings) = report.into_parts();
///
/// if !warnings.is_empty() {
///     eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
///
///     for group in warnings {
///         let (element, warnings) = group.to_parts();
///         eprintln!("  {}", element.path);
///
///         for warning in warnings {
///             eprintln!("    - {warning}");
///         }
///     }
/// }
///
/// # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
/// ```
pub fn price(
    cdr: &Versioned<'_>,
    tariff_source: price::TariffSource<'_>,
    timezone: Tz,
) -> Verdict<price::Report, price::Warning> {
    price::cdr(cdr, tariff_source, timezone)
}

/// A `json::Element` that has been processed by either the [`infer_version`] or [`build`]
/// functions and has been identified as being a certain [`Version`].
#[derive(Clone)]
pub struct Versioned<'buf> {
    /// The parsed JSON.
    doc: json::Document<'buf>,

    /// The `Version` of the tariff, determined during parsing.
    version: Version<'buf>,
}

impl fmt::Debug for Versioned<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            match &self.version {
                Version::V211(cdr) => fmt::Debug::fmt(&cdr, f),
                Version::V221(cdr) => fmt::Debug::fmt(&cdr, f),
            }
        } else {
            match &self.version {
                Version::V211(_) => f.write_str("V211"),
                Version::V221(_) => f.write_str("V221"),
            }
        }
    }
}

impl crate::Versioned for Versioned<'_> {
    fn version(&self) -> crate::Version {
        match self.version {
            Version::V211(_) => crate::Version::V211,
            Version::V221(_) => crate::Version::V221,
        }
    }
}

impl<'buf> Versioned<'buf> {
    /// Return the inner [`json::Document`] and discard the version info.
    pub fn into_doc(self) -> json::Document<'buf> {
        self.doc
    }

    /// Return the inner [`json::Element`] and discard the version info.
    pub fn as_element(&self) -> &json::Element<'buf> {
        self.doc.root()
    }

    /// Return the inner [`json::Document`] and discard the version info.
    pub fn as_doc(&self) -> &json::Document<'buf> {
        &self.doc
    }

    /// Return the inner JSON `str` and discard the version info.
    pub fn as_json_str(&self) -> &'buf str {
        self.doc.source()
    }
}

#[expect(
    clippy::large_enum_variant,
    reason = "the v2.1.1 and v2.2.1 CDR IRs differ in size; this short-lived versioned \
              value is not worth boxing"
)]
#[derive(Clone)]
enum Version<'buf> {
    V211(schema::v211::Cdr<'buf>),
    V221(schema::v221::Cdr<'buf>),
}

/// A `json::Document` that has been processed by [`infer_version`] and has been identified
/// as being a concrete [`Version`].
pub struct VersionedJson<'buf> {
    /// The parsed JSON.
    doc: json::Document<'buf>,

    /// The `Version` of the CDR, determined during parsing.
    version: crate::Version,
}

impl fmt::Debug for VersionedJson<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            fmt::Debug::fmt(&self.doc, f)
        } else {
            match self.version {
                crate::Version::V211 => f.write_str("V211"),
                crate::Version::V221 => f.write_str("V221"),
            }
        }
    }
}

impl crate::Versioned for VersionedJson<'_> {
    fn version(&self) -> crate::Version {
        self.version
    }
}

impl<'buf> VersionedJson<'buf> {
    /// Create a new `Versioned` object.
    pub(crate) fn new(element: json::Document<'buf>, version: crate::Version) -> Self {
        Self {
            doc: element,
            version,
        }
    }

    /// Return the inner [`json::Document`] and discard the version info.
    pub fn into_doc(self) -> json::Document<'buf> {
        self.doc
    }

    /// Return the inner [`json::Element`] and discard the version info.
    pub fn as_element(&self) -> &json::Element<'buf> {
        self.doc.root()
    }

    /// Return a reference to the inner [`json::Document`].
    pub fn as_doc(&self) -> &json::Document<'buf> {
        &self.doc
    }

    /// Return the inner JSON `str` and discard the version info.
    pub fn as_json_str(&self) -> &'buf str {
        self.doc.source()
    }
}

/// A `json::Document` that has been processed by [`infer_version`] and has been identified
/// as being a concrete [`Version`].
#[derive(Debug)]
pub struct Unversioned<'buf> {
    /// The root `Element` of the parsed source.
    doc: json::Document<'buf>,
}

impl<'buf> Unversioned<'buf> {
    /// Create an unversioned [`json::Element`].
    pub(crate) fn new(doc: json::Document<'buf>) -> Self {
        Self { doc }
    }

    /// Return the inner [`json::Element`] and discard the version info.
    pub fn into_doc(self) -> json::Document<'buf> {
        self.doc
    }

    /// Return the inner [`json::Element`] and discard the version info.
    pub fn as_element(&self) -> &json::Element<'buf> {
        self.doc.root()
    }
}

impl<'buf> crate::Unversioned for Unversioned<'buf> {
    type Versioned = VersionedJson<'buf>;

    fn force_into_versioned(self, version: crate::Version) -> VersionedJson<'buf> {
        let Self { doc } = self;
        VersionedJson { doc, version }
    }
}