ocpi-tariffs 0.51.0

OCPI tariff calculations
Documentation
//! Tests for lowering a `schema::Str` into a country `CodeSet` via `FromSchema`.
//!
//! A `schema::Str` is only produced by the builder, so each test drives a real `v2.2.1`
//! tariff through `build_tariff` and lowers its `country_code` field (a `Str` leaf).

#![allow(
    clippy::indexing_slicing,
    clippy::unwrap_in_result,
    reason = "unwraps and indexing are allowed anywhere in tests"
)]

use std::assert_matches;

use super::{Code, CodeSet, Warning};
use crate::{
    json,
    schema::{self, v221, Integrity},
    warning, FromSchema as _,
};

/// A minimal, fully valid `v2.2.1` tariff. `country_code` is the `Str` leaf the tests lower.
const VALID: &str = r#"{
    "country_code": "NL",
    "currency": "EUR",
    "elements": [
        {"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]}
    ],
    "id": "ID",
    "last_updated": "2024-01-01T00:00:00Z",
    "party_id": "ABC"
}"#;

/// Build a `v2.2.1` tariff, dropping the schema-level warnings (the tests assert on the
/// warnings produced by lowering the `country_code` leaf, not by the schema walk).
fn build_tariff<'buf>(doc: &json::Document<'buf>) -> v221::Tariff<'buf> {
    v221::build_tariff(doc).ignore_warnings()
}

/// Borrow the built `country_code` `Str` leaf out of a tariff.
fn country_code<'a, 'buf>(tariff: &'a v221::Tariff<'buf>) -> &'a schema::Str<'buf> {
    let Integrity::Ok(country_code) = &tariff.country_code else {
        panic!(
            "the country_code should be built: {:?}",
            tariff.country_code
        );
    };
    country_code
}

/// Collect every warning across all paths into a flat list.
fn all_warnings(warnings: &warning::Set<Warning>) -> Vec<&Warning> {
    warnings.path_map().into_values().flatten().collect()
}

/// Lower the `country_code` of a tariff built from `VALID` with its country code replaced
/// by `value` (the raw JSON value text, including quotes).
fn lower(value: &str) -> crate::Verdict<CodeSet, Warning> {
    let src = VALID.replace(r#""country_code": "NL""#, value);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = build_tariff(&doc);
    CodeSet::from_schema(country_code(&tariff))
}

#[test]
fn valid_alpha2_lowers_cleanly() {
    let doc = json::parse(VALID.into()).unwrap();
    let tariff = build_tariff(&doc);

    let (code_set, warnings) = CodeSet::from_schema(country_code(&tariff))
        .unwrap()
        .into_parts();

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

#[test]
fn valid_alpha3_lowers_cleanly() {
    let (code_set, warnings) = lower(r#""country_code": "NLD""#).unwrap().into_parts();

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

#[test]
fn lowercase_code_warns_but_lowers() {
    let (code_set, warnings) = lower(r#""country_code": "nl""#).unwrap().into_parts();

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

#[test]
fn wrong_length_is_rejected() {
    let err_set = lower(r#""country_code": "IRELAND""#).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

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

#[test]
fn unknown_code_is_rejected() {
    let err_set = lower(r#""country_code": "VV""#).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

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

#[test]
fn reserved_prefix_warns_and_is_rejected() {
    // Codes beginning with `X` are reserved; `XXX` is also not an assigned code, so the
    // reserved warning is accompanied by an `InvalidCode` rejection.
    let err_set = lower(r#""country_code": "XXX""#).unwrap_err();
    let (error, warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

    assert_matches!(error, Warning::InvalidCode);
    assert!(
        all_warnings(&warnings).contains(&&Warning::InvalidReserved),
        "{warnings:?}"
    );
}

#[test]
fn escape_sequence_is_rejected() {
    let err_set = lower(r#""country_code": "N\tL""#).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

    assert_matches!(error, Warning::ContainsEscapeCodes);
}