ocpi-tariffs 0.49.1

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`) records the language of the explanation. The explanation is Markdown. Only `en-US`
//! exists today; Dutch and German expectations will be added alongside it once `explain` is
//! localized.
//!
//! 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, Version};

/// The name of the expectation file containing the US English explanation (Markdown).
const EXPECTATION_FILE_NAME: &str = "explanation.en-US.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(tariff_json: &str, path: &Path) {
    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)
        .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_FILE_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_FILE_NAME}`",
        path.display()
    );
}