ocpi-tariffs 0.52.0

OCPI tariff calculations
Documentation
use crate::{
    json::{self, test::PathGlob},
    schema, test,
    test::{ExpectValue, Expectation, WarningMap},
    warning::{self, test::assert_warnings},
};

/// Where the expectations would have been written; it only appears in panic messages.
const EXPECTATION: &str = "`warnings` in the `output_test__tariff.json` file";

const TARIFF: &str = r#"{
    "party_id": "ENE",
    "currency": "EUR",
    "elements": [
        { "price_components": [ { "step_round": 1 } ] },
        { "price_components": [ { "step_round": 1 } ] }
    ]
}"#;

/// Report a `NullField` on `$.currency` and an `UnexpectedField` on the `step_round` of every
/// price component.
///
/// The warnings are inserted directly rather than obtained from the schema walk, so these tests
/// cover the assertion alone and do not move when the walk changes what it reports.
fn build_warnings(doc: &json::Document<'_>) -> warning::Set<schema::Warning> {
    let root = doc.root();

    let mut warnings = warning::Set::new();

    let currency = root.find_field("currency").unwrap().element();
    warnings.insert(currency, schema::Warning::NullField);

    let elements = root.find_field("elements").unwrap().element();
    let elements = elements.as_array().unwrap();

    for element in elements {
        let components = element.find_field("price_components").unwrap().element();

        for component in components.as_array().unwrap() {
            let step_round = component.find_field("step_round").unwrap().element();
            warnings.insert(step_round, schema::Warning::UnexpectedField);
        }
    }

    warnings
}

/// Build the expectation that an expect file holding these entries would deserialize into.
fn expect(entries: Vec<(&str, Vec<&str>)>) -> Expectation<WarningMap> {
    let entries = entries
        .into_iter()
        .map(|(glob, ids)| {
            let ids = ids.into_iter().map(str::to_owned).collect();
            (PathGlob::from(glob), ids)
        })
        .collect();

    Expectation::Present(ExpectValue::Some(entries))
}

/// An entry with no `*` component names one element, so precise expectations keep working.
#[test]
fn exact_paths_match_their_own_element() {
    test::setup();

    let doc = json::parse_object(TARIFF).unwrap();
    let warnings = build_warnings(&doc);

    let expect = expect(vec![
        ("$.currency", vec!["null_field"]),
        (
            "$.elements[0].price_components[0].step_round",
            vec!["unexpected_field"],
        ),
        (
            "$.elements[1].price_components[0].step_round",
            vec!["unexpected_field"],
        ),
    ]);

    assert_warnings(EXPECTATION, &warnings, expect);
}

/// One wildcard entry stands in for the same warning on every entry of an array. This is what
/// keeps a repetitive expectation from being written out once per array index.
#[test]
fn a_wildcard_entry_covers_every_element_it_matches() {
    test::setup();

    let doc = json::parse_object(TARIFF).unwrap();
    let warnings = build_warnings(&doc);

    let expect = expect(vec![
        ("$.currency", vec!["null_field"]),
        (
            "$.elements.*.price_components[0].step_round",
            vec!["unexpected_field"],
        ),
    ]);

    assert_warnings(EXPECTATION, &warnings, expect);
}

/// An element that has warnings but that no entry matches is a warning nobody has signed off on.
#[test]
#[should_panic(expected = "Elements with warnings that no entry of `warnings` in the")]
fn an_element_matched_by_no_entry_fails() {
    test::setup();

    let doc = json::parse_object(TARIFF).unwrap();
    let warnings = build_warnings(&doc);

    let expect = expect(vec![(
        "$.elements.*.price_components[0].step_round",
        vec!["unexpected_field"],
    )]);

    assert_warnings(EXPECTATION, &warnings, expect);
}

/// An entry that matches nothing is a stale expectation left behind after the warning it pinned
/// down stopped being raised.
#[test]
#[should_panic(expected = "that match no element with warnings")]
fn an_entry_matching_no_element_fails() {
    test::setup();

    let doc = json::parse_object(TARIFF).unwrap();
    let warnings = build_warnings(&doc);

    let expect = expect(vec![
        ("$.currency", vec!["null_field"]),
        ("$.party_id", vec!["null_field"]),
        (
            "$.elements.*.price_components[0].step_round",
            vec!["unexpected_field"],
        ),
    ]);

    assert_warnings(EXPECTATION, &warnings, expect);
}

/// The ids listed by the matching entry have to be the ids actually reported, not a subset.
#[test]
#[should_panic(expected = "Elements whose warnings are not the warnings listed by `warnings`")]
fn an_entry_listing_the_wrong_ids_fails() {
    test::setup();

    let doc = json::parse_object(TARIFF).unwrap();
    let warnings = build_warnings(&doc);

    let expect = expect(vec![
        ("$.currency", vec!["unexpected_field"]),
        (
            "$.elements.*.price_components[0].step_round",
            vec!["unexpected_field"],
        ),
    ]);

    assert_warnings(EXPECTATION, &warnings, expect);
}

/// A wildcard entry and an exact entry that both match an element are reported rather than
/// resolved: which of the two id lists applied would depend on the order the entries sort in.
#[test]
#[should_panic(expected = "Elements matched by more than one entry of `warnings` in the")]
fn an_element_matched_by_two_entries_fails() {
    test::setup();

    let doc = json::parse_object(TARIFF).unwrap();
    let warnings = build_warnings(&doc);

    let expect = expect(vec![
        ("$.currency", vec!["null_field"]),
        (
            "$.elements.*.price_components[0].step_round",
            vec!["unexpected_field"],
        ),
        (
            "$.elements[0].price_components[0].step_round",
            vec!["unexpected_field"],
        ),
    ]);

    assert_warnings(EXPECTATION, &warnings, expect);
}

/// An expect file with no `warnings` field asserts that there are no warnings at all.
#[test]
#[should_panic(expected = "There is no `warnings` in the")]
fn an_absent_expectation_fails_when_there_are_warnings() {
    test::setup();

    let doc = json::parse_object(TARIFF).unwrap();
    let warnings = build_warnings(&doc);

    assert_warnings(EXPECTATION, &warnings, Expectation::Absent);
}

/// An absent expectation and an empty set agree, so nothing has to be written down for the
/// tariffs that produce no warnings at all.
#[test]
fn an_absent_expectation_passes_when_there_are_no_warnings() {
    test::setup();

    let warnings = warning::Set::<schema::Warning>::new();

    assert_warnings(EXPECTATION, &warnings, Expectation::Absent);
}