ocpi-tariffs 0.51.0

OCPI tariff calculations
Documentation
//! Explain every tariff in `test_data/popular` and compare it against a checked-in expectation
//! file placed beside each `tariff.json`.
//!
//! The expectation file is named `explanation.<locale>.md`, where the locale segment (for example
//! `en-US` or `nl-NL`) records the language of the explanation. The explanation is Markdown.
//! `en-US` and `nl-NL` exist today; a German expectation will be added alongside them once the
//! German locale is implemented.
//!
//! To (re)generate the expectation files after an intentional wording change, run the tests with
//! the `BLESS_EXPLANATIONS` environment variable set, for example:
//!
//! ```text
//! BLESS_EXPLANATIONS=1 cargo test --package ocpi-tariffs --lib explain::test_real_world
//! ```

use std::{fs, path::Path};

use crate::{json, tariff, test, Language, Version};

/// The name of the expectation file containing the US English explanation (Markdown).
const EXPECTATION_FILE_NAME_EN: &str = "explanation.en-US.md";

/// The name of the expectation file containing the Dutch explanation (Markdown).
const EXPECTATION_FILE_NAME_NL: &str = "explanation.nl-NL.md";

/// The popular tariffs are all authored as OCPI `v2.2.1`.
const VERSION: Version = Version::V221;

#[test_each::file(
    glob = "ocpi-tariffs/test_data/popular/*/tariff.json",
    name(segments = 2)
)]
fn explain_popular_tariff_en(tariff_json: &str, path: &Path) {
    check_explanation(tariff_json, path, Language::EnUS, EXPECTATION_FILE_NAME_EN);
}

#[test_each::file(
    glob = "ocpi-tariffs/test_data/popular/*/tariff.json",
    name(segments = 2)
)]
fn explain_popular_tariff_nl(tariff_json: &str, path: &Path) {
    check_explanation(tariff_json, path, Language::NlNl, EXPECTATION_FILE_NAME_NL);
}

/// Explain the tariff in the given language and compare it against the named expectation file, or
/// (re)generate that file when `BLESS_EXPLANATIONS` is set.
fn check_explanation(tariff_json: &str, path: &Path, language: Language, expectation_name: &str) {
    test::setup();

    // Some popular `tariff.json` files contain `//` comments; the integrated parser does not accept
    // those, so strip them as `test::read_file_content` would when loading from disk.
    let mut tariff_json = tariff_json.to_owned();
    json_strip_comments::strip(&mut tariff_json)
        .unwrap_or_else(|err| panic!("`{}` comments should strip:\n{err}", path.display()));

    let tariff = json::parse_object(&tariff_json).unwrap();
    let tariff = tariff::build(tariff, VERSION).ignore_warnings();

    let explanation = tariff::explain(&tariff, language)
        .unwrap_or_else(|_err| panic!("`{}` should be explainable", path.display()))
        .ignore_warnings();

    let expectation_path = path
        .parent()
        .expect("the tariff file should live in a directory")
        .join(expectation_name);

    // When `BLESS_EXPLANATIONS` is set, write the expectation files instead of asserting against
    // them. This is how the checked-in expectations are (re)generated after a wording change.
    if std::env::var_os("BLESS_EXPLANATIONS").is_some() {
        fs::write(&expectation_path, format!("{explanation}\n")).unwrap_or_else(|err| {
            panic!(
                "`{}` should be writable:\n{err}",
                expectation_path.display()
            )
        });
        return;
    }

    let expected = fs::read_to_string(&expectation_path).unwrap_or_else(|err| {
        panic!(
            "Unable to read `{}`:\n{err}\nRun with `BLESS_EXPLANATIONS=1` to generate it.",
            expectation_path.display()
        )
    });

    // The checked-in file carries a trailing newline; the rendered explanation does not. Normalize
    // the trailing whitespace so that detail does not cause a spurious mismatch.
    assert_eq!(
        explanation.trim_end(),
        expected.trim_end(),
        "explanation for `{}` does not match `{expectation_name}`",
        path.display()
    );
}