ocpi-tariffs 0.20.0

OCPI tariff calculations
Documentation
use std::fmt;

use chrono_tz::Tz;

use crate::{
    guess, json,
    price::{self, price_cdr},
    ParseError, Version,
};

/// Parse a `&str` into a [`ParseReport`] that contains a [`Version`] tagged [`json::Element`].
pub fn parse_with_version(cdr_json: &str, version: Version) -> Result<ParseReport<'_>, ParseError> {
    match version {
        Version::V221 => {
            let schema = &*crate::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(ParseReport {
                cdr: Versioned::V221(element),
                unexpected_fields,
            })
        }
        Version::V211 => {
            let schema = &*crate::v211::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(ParseReport {
                cdr: Versioned::V211(element),
                unexpected_fields,
            })
        }
    }
}

/// Parse the JSON as a CDR and try guess the [`Version`].
///
/// The parser is very forgiving and will not complain if the CDR JSON is missing required fields.
/// The version guess is based on fields that exist.
pub fn parse(cdr_json: &str) -> Result<guess::CdrVersion<'_>, ParseError> {
    guess::cdr_version(cdr_json)
}

/// Guess the [`Version`] of the given CDR and report on any unexpected fields in the JSON.
///
/// The parser is very forgiving and will not complain if the CDR JSON is missing required fields.
/// The version guess is based on fields that exist.
pub fn parse_and_report(cdr_json: &str) -> Result<guess::CdrReport<'_>, ParseError> {
    guess::cdr_version_and_report(cdr_json)
}

/// 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::Error) 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) fn.
pub fn price(
    cdr_json: &str,
    tariff_source: price::TariffSource,
    timezone: Tz,
    version: Version,
) -> Result<price::Report, price::Error> {
    price_cdr(cdr_json, tariff_source, timezone, version)
}

#[derive(Debug)]
pub struct ParseReport<'buf> {
    /// The root JSON [`Element`](json::Element).
    pub cdr: Versioned<'buf>,

    /// A list of fields that were not expected.
    pub unexpected_fields: json::UnexpectedFields<'buf>,
}

/// A [`json::Element`] that has been parsed by the either the [`parse_with_version`] or [`parse`] functions
/// and was determined to be one of the supported [`Version`]s.
pub enum Versioned<'buf> {
    /// The parsing function determined that this is a v211 CDR.
    V211(json::Element<'buf>),

    /// The parsing function determined that this is a v221 CDR.
    V221(json::Element<'buf>),
}

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

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

impl<'buf> Versioned<'buf> {
    /// Return the inner [`json::Element`] and discard the version info.
    pub fn into_element(self) -> json::Element<'buf> {
        match self {
            Versioned::V211(element) | Versioned::V221(element) => element,
        }
    }

    /// Return the inner [`json::Element`] and discard the version info.
    pub fn as_element(&self) -> &json::Element<'buf> {
        match self {
            Versioned::V211(element) | Versioned::V221(element) => element,
        }
    }
}

/// A [`json::Element`] that has been parsed by the either the [`parse_with_version`] or [`parse`] functions
/// and was determined to not be one of the supported [`Version`]s.
#[derive(Debug)]
pub struct Unversioned<'buf>(json::Element<'buf>);

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

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

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

    fn force_into_versioned(self, version: Version) -> Versioned<'buf> {
        match version {
            Version::V221 => Versioned::V221(self.0),
            Version::V211 => Versioned::V211(self.0),
        }
    }
}