ocpi-tariffs 0.49.1

OCPI tariff calculations
Documentation
//! # OCPI Tariffs library
//!
//! Calculate the (sub)totals of a [charge session](https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc)
//! using the [`cdr::price`] function and use the generated [`price::Report`] to review and compare the calculated
//! totals versus the sources from the `CDR`.
//!
//! - Use [`json::parse_object`] to parse a CDR or tariff `&str` into a [`json::Document`].
//! - Use [`cdr::infer_version`] or [`tariff::infer_version`] to guess which OCPI [`Version`] a CDR or tariff is.
//! - Use the [`cdr::build`] and [`tariff::build`] functions to check a [`json::Document`] against the schema for a given version.
//! - Use the [`tariff::lint`] to lint a tariff: flag common errors, bugs, dangerous constructs and stylistic flaws in the tariff.
//!
//! # Examples
//!
//! ## Price a CDR with embedded tariff
//!
//! If you have a CDR JSON with an embedded tariff you can price the CDR with the following code:
//!
//! ```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>>(())
//! ```
//!
//! ## Price a CDR using tariff in separate JSON file
//!
//! If you have a CDR JSON with a tariff in a separate JSON file you can price the CDR with the
//! following code:
//!
//! ```rust
//! # use ocpi_tariffs::{cdr, json, price, tariff, warning, Version};
//! #
//! # const CDR_JSON: &str = include_str!("cdr.json");
//! # const TARIFF_JSON: &str = include_str!("tariff.json");
//!
//! let cdr_doc = json::parse_object(CDR_JSON)?;
//! let (cdr, _cdr_warnings) = cdr::build(cdr_doc, Version::V211).into_parts();
//!
//! let tariff_doc = json::parse_object(TARIFF_JSON)?;
//! let (tariff, _tariff_warnings) = tariff::build(tariff_doc, Version::V211).into_parts();
//!
//! let report = cdr::price(&cdr, price::TariffSource::Override(vec![tariff]), 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>>(())
//! ```
//!
//! ## Lint a tariff
//!
//! ```rust
//! # use ocpi_tariffs::{guess, json, tariff, warning};
//! #
//! # const TARIFF_JSON: &str = include_str!("tariff.json");
//!
//! let doc = json::parse_object(TARIFF_JSON)?;
//! let guess::Version::Certain(tariff) = tariff::infer_version(doc) else {
//!     return Err("Unable to guess the version of given tariff JSON.".into());
//! };
//! let tariff = tariff::build_versioned(tariff).ignore_warnings();
//!
//! let report = tariff::lint(&tariff);
//!
//! eprintln!("`{}` lint warnings found", report.warnings.len_warnings());
//!
//! for group in report.warnings {
//!     let (element, warnings) = group.to_parts();
//!     eprintln!(
//!         "Warnings reported for `json::Element` at path: `{}`",
//!         element.path
//!     );
//!
//!     for warning in warnings {
//!         eprintln!("  * {warning}");
//!     }
//!
//!     eprintln!();
//! }
//!
//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
//! ```

#[cfg(test)]
mod test;

#[cfg(test)]
mod test_rust_decimal_arbitrary_precision;

pub mod cdr;
pub mod country;
pub mod currency;
pub mod datetime;
pub mod duration;
mod energy;
pub mod enumeration;
pub mod explain;
pub mod generate;
pub mod guess;
pub mod json;
pub mod lint;
pub mod money;
pub mod number;
pub mod price;
pub mod schema;
pub mod string;
pub mod tariff;
pub mod timezone;
pub mod warning;
pub mod weekday;

use std::fmt;

#[doc(inline)]
pub use duration::{ToDuration, ToHoursDecimal};
#[doc(inline)]
pub use energy::{Ampere, Kw, Kwh};
#[doc(inline)]
use enumeration::{Enum, IntoEnum};
#[doc(inline)]
pub use money::{Cost, Money, Price, Vat};
use warning::IntoCaveat;
#[doc(inline)]
pub use warning::{Caveat, Verdict, VerdictExt, Warning};
use weekday::Weekday;

/// The Id for a tariff used in the pricing of a CDR.
pub type TariffId = String;

/// The OCPI versions supported by this crate.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Version {
    /// OCPI version 2.2.1.
    ///
    /// See: <https://github.com/ocpi/ocpi/tree/release-2.2.1-bugfixes>.
    V221,

    /// OCPI version 2.1.1.
    ///
    /// See: <https://github.com/ocpi/ocpi/tree/release-2.1.1-bugfixes>.
    V211,
}

impl Versioned for Version {
    fn version(&self) -> Version {
        *self
    }
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Version::V221 => f.write_str("v221"),
            Version::V211 => f.write_str("v211"),
        }
    }
}

/// An object for a specific OCPI [`Version`].
pub trait Versioned: fmt::Debug {
    /// Return the OCPI `Version` of this object.
    fn version(&self) -> Version;
}

/// An object with an uncertain [`Version`].
pub trait Unversioned: fmt::Debug {
    /// The concrete [`Versioned`] type.
    type Versioned: Versioned;

    /// Forced an [`Unversioned`] object to be the given [`Version`].
    ///
    /// This does not change the structure of the OCPI object.
    /// It simply relabels the object as a different OCPI Version.
    ///
    /// Use this with care.
    fn force_into_versioned(self, version: Version) -> Self::Versioned;
}

/// Add two types together and saturate to max if the addition operation overflows.
///
/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
trait SaturatingAdd {
    /// Add two types together and saturate to max if the addition operation overflows.
    #[must_use]
    fn saturating_add(self, other: Self) -> Self;
}

/// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
///
/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
trait SaturatingSub {
    /// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
    #[must_use]
    fn saturating_sub(self, other: Self) -> Self;
}

/// A debug utility to `Display` an `Option<T>` as either `Display::fmt(T)` or the null set `∅`.
struct DisplayOption<T>(Option<T>)
where
    T: fmt::Display;

impl<T> fmt::Display for DisplayOption<T>
where
    T: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.0 {
            Some(v) => fmt::Display::fmt(v, f),
            None => f.write_str(""),
        }
    }
}