ocpi-tariffs 0.46.1

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, price, tariff, ObjectType, ParseError, ReasonableStr, Verdict, Version,
};

/// Parse a `&str` into a [`Versioned`] CDR using a schema for the given [`Version`][^spec-v211][^spec-v221] to check for
/// any unexpected fields.
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{cdr, Version, ParseError};
/// #
/// # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time/cdr.json");
///
/// let report = cdr::parse_with_version(CDR_JSON, Version::V211)?;
/// let cdr::ParseReport {
///     cdr,
///     unexpected_fields,
/// } = report;
///
/// if !unexpected_fields.is_empty() {
///     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
///
///     for path in &unexpected_fields {
///         eprintln!("{path}");
///     }
/// }
///
/// # Ok::<(), 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 parse_with_version(cdr_json: &str, version: Version) -> Result<ParseReport<'_>, ParseError> {
    let cdr_json = ReasonableStr::new(cdr_json).map_err(ParseError::from_kind(ObjectType::Cdr))?;
    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::ParseReport {
                element,
                unexpected_fields,
            } = report;
            Ok(ParseReport {
                cdr: Versioned {
                    source: cdr_json.into_inner(),
                    element,
                    version: Version::V221,
                },
                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::ParseReport {
                element,
                unexpected_fields,
            } = report;
            Ok(ParseReport {
                cdr: Versioned {
                    source: cdr_json.into_inner(),
                    element,
                    version,
                },
                unexpected_fields,
            })
        }
    }
}

/// A report of calling [`parse_with_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: The schema did not define them.
    pub unexpected_fields: json::UnexpectedFields<'buf>,
}

/// Parse the JSON and try to guess the [`Version`] based on fields defined in the OCPI v2.1.1[^spec-v211] and v2.2.1[^spec-v221] CDR spec.
///
/// The parser is forgiving and will not complain if the CDR JSON is missing required fields.
/// The parser will also not complain if unexpected fields are present in the JSON.
/// The [`Version`] guess is based on fields that exist.
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{cdr, guess, ParseError, Version, Versioned as _};
/// #
/// # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time/cdr.json");
/// let cdr = cdr::parse(CDR_JSON)?;
///
/// match cdr {
///     guess::Version::Certain(cdr) => {
///         println!("The CDR version is `{}`", cdr.version());
///     },
///     guess::Version::Uncertain(_cdr) => {
///         eprintln!("Unable to guess the version of given CDR JSON.");
///     }
/// }
///
/// # Ok::<(), ParseError>(())
/// ```
///
/// [^ocpi-spec-v211-cdr]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md>
/// [^ocpi-spec-v221-cdr]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>
pub fn parse(cdr_json: &str) -> Result<guess::CdrVersion<'_>, ParseError> {
    let cdr_json = ReasonableStr::new(cdr_json).map_err(ParseError::from_kind(ObjectType::Cdr))?;
    guess::cdr_version(cdr_json)
}

/// Guess the [`Version`][^spec-v211][^spec-v221] of the given CDR JSON and report on any unexpected fields.
///
/// The parser is forgiving and will not complain if the CDR JSON is missing required fields.
/// The parser will also not complain if unexpected fields are present in the JSON.
/// The [`Version`] guess is based on fields that exist.
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{cdr, guess, ParseError, Version, Versioned};
/// #
/// # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time/cdr.json");
///
/// let report = cdr::parse_and_report(CDR_JSON)?;
/// let guess::CdrReport {
///     version,
///     unexpected_fields,
/// } = report;
///
/// if !unexpected_fields.is_empty() {
///     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
///
///     for path in &unexpected_fields {
///         eprintln!("{path}");
///     }
/// }
///
/// match version {
///     guess::Version::Certain(cdr) => {
///         println!("The CDR version is `{}`", cdr.version());
///     },
///     guess::Version::Uncertain(_cdr) => {
///         eprintln!("Unable to guess the version of given CDR JSON.");
///     }
/// }
///
/// # Ok::<(), 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 parse_and_report(cdr_json: &str) -> Result<guess::CdrReport<'_>, ParseError> {
    let cdr_json = ReasonableStr::new(cdr_json).map_err(ParseError::from_kind(ObjectType::Cdr))?;
    guess::cdr_version_and_report(cdr_json)
}

/// 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, price, warning, Version, ParseError};
/// #
/// # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time/cdr.json");
///
/// let report = cdr::parse_with_version(CDR_JSON, Version::V211)?;
/// let cdr::ParseReport {
///     cdr,
///     unexpected_fields,
/// } = report;
///
/// # if !unexpected_fields.is_empty() {
/// #     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
/// #
/// #     for path in &unexpected_fields {
/// #         eprintln!("{path}");
/// #     }
/// # }
///
/// 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 parsed by the either the [`parse_with_version`] or [`parse`] functions
/// and was determined to be one of the supported [`Version`]s.
pub struct Versioned<'buf> {
    /// The source JSON as string.
    source: &'buf str,

    /// The parsed JSON as structured [`Element`](crate::json::Element)s.
    element: json::Element<'buf>,

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

impl fmt::Debug for Versioned<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            fmt::Debug::fmt(&self.element, 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) -> Version {
        self.version
    }
}

impl<'buf> Versioned<'buf> {
    pub(crate) fn new(source: &'buf str, element: json::Element<'buf>, version: Version) -> Self {
        Self {
            source,
            element,
            version,
        }
    }

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

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

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

/// 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> {
    source: &'buf str,
    element: json::Element<'buf>,
}

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

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

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

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

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

    fn force_into_versioned(self, version: Version) -> Versioned<'buf> {
        let Self { source, element } = self;
        Versioned {
            source,
            element,
            version,
        }
    }
}