ocpi-tariffs 0.52.0

OCPI tariff calculations
Documentation
//! A leaf lowering that cannot build a value seeds a `Rejected` marker into the lint set.
//!
//! The marker carries no detail on purpose - the schema walk already located the missing or
//! invalid field. A consumer that has the schema warnings in hand can therefore drop the
//! follow-on markers, which is what `is_rejected` and `warning::Set::remove_rejected` are
//! for. These tests pin that contract down; the fixture
//! `test_data/v221/lint/min_max_price/test_min_price_missing_excl_vat.json` pins the ids it
//! reports.

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

use std::assert_matches;

use super::Warning;
use crate::{json, money, tariff, test, warning::Warning as _, Version};

/// A `v2.2.1` tariff whose `min_price` object is missing the required `excl_vat`.
const TARIFF: &str = r#"{
    "country_code": "NL",
    "party_id": "ENE",
    "currency": "EUR",
    "id": "ID",
    "last_updated": "2024-01-01T00:00:00Z",
    "min_price": {"incl_vat": 10.2},
    "elements": [
        {"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY", "vat": 21}]}
    ]
}"#;

#[test]
fn unbuildable_price_is_reported_as_rejected() {
    test::setup();

    let doc = json::parse_object(TARIFF).unwrap();
    let (tariff, schema_warnings) = tariff::from_json(doc, Version::V221).into_parts();

    // The schema walk names the cause.
    let schema_warnings = schema_warnings.path_map();
    assert_matches!(
        *schema_warnings["$.min_price"],
        [crate::schema::Warning::MissingField { name: "excl_vat" }]
    );

    // The linter only signals that it could not build a value, at the same path.
    let report = tariff::lint(&tariff);
    let warnings = report.warnings.path_map();
    assert_matches!(
        *warnings["$.min_price"],
        [Warning::Money(money::Warning::Rejected)]
    );
}

#[test]
fn rejected_is_reported_through_the_wrapper() {
    let warning = Warning::Money(money::Warning::Rejected);

    // The wrapper delegates, so a `Rejected` seeded by any leaf lowering stays recognisable
    // once it has been converted into the lint `Warning`.
    assert!(warning.is_rejected());
    assert_eq!(warning.id().as_str(), "rejected");
}

#[test]
fn a_lint_of_its_own_is_not_rejected() {
    assert!(!Warning::CpoCountryCodeShouldBeAlpha2.is_rejected());
    assert!(!Warning::MinPriceIsGreaterThanMax.is_rejected());
    assert!(!Warning::StartDateTimeIsAfterEndDateTime.is_rejected());
}

#[test]
fn remove_rejected_drops_the_marker_and_keeps_the_rest() {
    test::setup();

    let doc = json::parse_object(TARIFF).unwrap();
    let tariff = tariff::from_json(doc, Version::V221).ignore_warnings();

    let mut warnings = tariff::lint(&tariff).warnings;
    assert!(!warnings.is_empty());

    warnings.remove_rejected();

    assert!(
        warnings.is_empty(),
        "the `rejected` marker should be the only warning: {:#?}",
        warnings.path_id_map()
    );
}