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, CodeSet, Warning};
use crate::{
    json,
    schema::{self, Integrity},
    warning::test::VerdictTestExt as _,
    FromSchema as _, Verdict,
};

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

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

        let couplet: [u8; 2] = [a.to_ascii_uppercase(), b.to_ascii_uppercase()];

        let Some(code) = Code::from_alpha_2(couplet) else {
            panic!("Unknown country code `{code}`");
        };

        code
    }
}

#[test]
fn alpha2_country_code_matches() {
    let code = Code::from_alpha_2(*b"NL").unwrap();

    assert_eq!(Code::Nl, code);
    assert_eq!("NL", code.into_alpha_2_str());
}

#[test]
fn alpha3_country_code_matches() {
    let code = Code::from_alpha_3(*b"NLD").unwrap();

    assert_eq!(Code::Nl, code);
}

#[test]
fn should_create_country3_without_issue() {
    const COUNTRY: &str = r#""NLD""#;

    let (code_set, warnings) = code_set(COUNTRY).unwrap().into_parts();

    let CodeSet::Alpha3(code) = code_set else {
        panic!("expected Alpha3, got {code_set:?}");
    };
    assert_eq!(code, Code::Nl);
    assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());
}

#[test]
fn should_create_country2_without_issue() {
    const COUNTRY: &str = r#""NL""#;

    let (code_set, warnings) = code_set(COUNTRY).unwrap().into_parts();

    let CodeSet::Alpha2(code) = code_set else {
        panic!("expected Alpha2, got {code_set:?}");
    };
    assert_eq!(code, Code::Nl);
    assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());
}

#[test]
fn should_raise_country_content_issue() {
    {
        const COUNTRY: &str = r#""VV""#;

        let error = code_set(COUNTRY).unwrap_only_error();

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

    {
        const COUNTRY: &str = r#""VVV""#;

        let error = code_set(COUNTRY).unwrap_only_error();

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

#[test]
fn should_parse_invalid_case() {
    {
        const COUNTRY: &str = r#""nl""#;

        let (code_set, warnings) = code_set(COUNTRY).unwrap().into_parts();
        let warnings = warnings.path_map();
        let warnings = &*warnings["$.country_code"];

        let CodeSet::Alpha2(code) = code_set else {
            panic!("expected Alpha2, got {code_set:?}");
        };
        assert_eq!(code, Code::Nl);
        assert_matches!(*warnings, [Warning::PreferUpperCase]);
    }

    {
        const COUNTRY: &str = r#""nld""#;

        let (code_set, warnings) = code_set(COUNTRY).unwrap().into_parts();
        let warnings = warnings.path_map();
        let warnings = &*warnings["$.country_code"];

        let CodeSet::Alpha3(code) = code_set else {
            panic!("expected Alpha3, got {code_set:?}");
        };
        assert_eq!(code, Code::Nl);
        assert_matches!(warnings, [Warning::PreferUpperCase]);
    }
}

#[test]
fn should_raise_country_length_issue() {
    const COUNTRY: &str = r#""IRELAND""#;

    let error = code_set(COUNTRY).unwrap_only_error();

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

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

/// Build a tariff carrying `country_code` and lower its `Str` leaf into a `CodeSet`.
///
/// The schema walk proves the value is a string; the alpha-2/alpha-3 resolution, the case
/// advice and the length check all come from the lowering.
#[track_caller]
fn code_set(country: &str) -> Verdict<CodeSet, Warning> {
    let src = TARIFF.replace("{COUNTRY}", country);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = schema::v221::build_tariff(&doc).ignore_warnings();

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

    CodeSet::from_schema(country_code)
}