ocpi-tariffs 0.52.0

OCPI tariff calculations
Documentation
#![allow(
    clippy::unwrap_in_result,
    reason = "unwraps are allowed anywhere in tests"
)]
#![allow(
    clippy::indexing_slicing,
    reason = "unwraps are allowed anywhere in tests"
)]
#![allow(clippy::panic_in_result_fn, reason = "tests are allowed to panic")]

use std::assert_matches;

use super::{Code, Warning};
use crate::{
    json,
    schema::{self, Integrity},
    warning::test::VerdictTestExt as _,
    FromSchema as _, Verdict,
};

impl Code {
    /// Return a `currency::Code` for the given `str`.
    ///
    /// # Panics
    ///
    /// Will panic when the given `str` is not three bytes long or an unknown currency code.
    pub fn from_alpha_3_str(code: &str) -> Self {
        let bytes = code.as_bytes();

        // ISO 4217 is expected to be 3 chars.
        let [a, b, c] = bytes else {
            panic!(
                "Unable to parse currency code. Expected a length of 3 chars. It has length: `{}`",
                code.len()
            );
        };

        let triplet: [u8; 3] = [
            a.to_ascii_uppercase(),
            b.to_ascii_uppercase(),
            c.to_ascii_uppercase(),
        ];

        let Some(code) = Code::from_alpha_3(triplet) else {
            panic!("Unknown currency code `{code}`");
        };

        code
    }
}

#[test]
fn should_create_currency_without_issue() {
    const CURRENCY: &str = r#""EUR""#;

    let (code, warnings) = parse_code(CURRENCY).unwrap().into_parts();

    assert_eq!(Code::Eur, code);
    assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());
}

#[test]
fn should_raise_currency_content_issue() {
    const CURRENCY: &str = r#""VVV""#;

    let error = parse_code(CURRENCY).unwrap_only_error();

    assert_matches!(error.into_warning(), Warning::InvalidCode);
}

#[test]
fn should_raise_currency_case_issue() {
    const CURRENCY: &str = r#""eur""#;

    let (code, warnings) = parse_code(CURRENCY).unwrap().into_parts();
    let warnings = warnings.path_map();
    let warnings = &*warnings["$.currency"];

    assert_eq!(code, Code::Eur);
    assert_matches!(warnings, [Warning::PreferUpperCase]);
}

#[test]
fn should_raise_currency_xts_issue() {
    const CURRENCY: &str = r#""xts""#;

    let (code, warnings) = parse_code(CURRENCY).unwrap().into_parts();
    let warnings = warnings.path_map();
    let warnings = &*warnings["$.currency"];

    assert_eq!(code, Code::Xts);
    assert_matches!(
        warnings,
        [Warning::PreferUpperCase, Warning::InvalidCodeXTS]
    );
}

#[test]
fn should_raise_currency_xxx_issue() {
    const CURRENCY: &str = r#""xxx""#;

    let (code, warnings) = parse_code(CURRENCY).unwrap().into_parts();
    let warnings = warnings.path_map();
    let warnings = &*warnings["$.currency"];

    assert_eq!(code, Code::Xxx);
    assert_matches!(
        warnings,
        [Warning::PreferUpperCase, Warning::InvalidCodeXXX]
    );
}

/// A minimal, valid `v2.1.1` tariff. `{CURRENCY}` is replaced with the value under test.
const TARIFF: &str = r#"{
    "currency": {CURRENCY},
    "elements": [
        {"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]}
    ],
    "id": "ID",
    "last_updated": "2024-01-01T00:00:00Z"
}"#;

/// Build a tariff carrying `currency` and lower its `currency` `Str` leaf.
///
/// The schema walk proves the value is a string; everything the tests assert on - the code
/// table lookup, the case advice and the reserved-code warnings - comes from the lowering.
#[track_caller]
fn parse_code(currency: &str) -> Verdict<Code, Warning> {
    let src = TARIFF.replace("{CURRENCY}", currency);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = schema::v211::build_tariff(&doc).ignore_warnings();

    let Integrity::Ok(currency) = &tariff.currency else {
        panic!("currency should be built: {:?}", tariff.currency);
    };

    Code::from_schema(currency)
}