ocpi-tariffs 0.53.0

OCPI tariff calculations
Documentation
//! Tests for [`Fixable`] as `schema::Warning` implements it: which schema warnings a
//! mechanical edit resolves, and which are left for the document's author.

#![allow(
    clippy::indexing_slicing,
    clippy::panic,
    clippy::unwrap_used,
    reason = "test code with known-structure parsed data"
)]

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

use super::{apply, edits, Edit, Fixable as _};

/// Validate `json` as a v2.2.1 tariff and return it with its schema warnings.
///
/// The tariff keeps the [`json::Document`] alive, reachable through `as_doc`, which is what
/// turns the `ElemId` on a warning back into the element the edit applies to.
fn validated(json: &str) -> (tariff::Versioned<'_>, warning::Set<schema::Warning>) {
    let doc = json::parse_object(json).unwrap();

    tariff::from_json(doc, Version::V221).into_parts()
}

/// A parsed document and the element of its only field, which the warnings are raised on.
fn only_field(json: &str) -> (json::Document<'_>, json::ElemId) {
    let doc = json::parse_object(json).unwrap();
    let id = doc.root().find_field("a").unwrap().element().id();

    (doc, id)
}

#[test]
fn a_null_field_is_removed() {
    let (doc, id) = only_field(r#"{"a": null}"#);
    let element = doc.element(id).unwrap();

    assert_eq!(
        schema::Warning::NullField.fix(element),
        Some(Edit::remove(id))
    );
}

#[test]
fn an_unexpected_field_is_removed() {
    let (doc, id) = only_field(r#"{"a": 1}"#);
    let element = doc.element(id).unwrap();

    assert_eq!(
        schema::Warning::UnexpectedField.fix(element),
        Some(Edit::remove(id))
    );
}

/// The library reads a non-spec field, so removing it would change how a document prices.
#[test]
fn a_non_spec_field_is_left_alone() {
    let (doc, id) = only_field(r#"{"a": 1}"#);
    let element = doc.element(id).unwrap();

    assert_eq!(schema::Warning::NonSpecField.fix(element), None);
}

#[test]
fn a_miscased_enum_value_is_rewritten_in_the_spec_case() {
    let (doc, id) = only_field(r#"{"a": "energy"}"#);
    let element = doc.element(id).unwrap();

    let warning = schema::Warning::IncorrectCase {
        expected: "ENERGY",
        actual: "energy".to_owned(),
    };

    assert_eq!(
        warning.fix(element),
        Some(Edit::replace(id, super::Json::string("ENERGY")))
    );
}

/// The whole round trip over a tariff whose `type` is miscased: validate, propose, apply.
#[test]
fn a_tariff_has_its_miscased_enum_value_rewritten() {
    const TARIFF: &str = r#"{
  "country_code": "NL",
  "party_id": "TDR",
  "id": "TEST",
  "currency": "EUR",
  "elements": [
    {
      "price_components": [
        {
          "type": "energy",
          "price": 0.38,
          "step_size": 1
        }
      ]
    }
  ],
  "last_updated": "2020-01-01T00:00:00Z"
}"#;

    test::setup();

    let (tariff, warnings) = validated(TARIFF);
    let doc = tariff.as_doc();
    let proposed = edits(doc, &warnings).unwrap();
    let fixed = apply(doc, &proposed).unwrap();

    assert_eq!(fixed, TARIFF.replace(r#""energy""#, r#""ENERGY""#));
}

#[test]
fn a_warning_needing_a_human_decision_proposes_nothing() {
    let (doc, id) = only_field(r#"{"a": 1}"#);
    let element = doc.element(id).unwrap();

    let unfixable = [
        schema::Warning::MissingField { name: "currency" },
        schema::Warning::Cardinality,
    ];

    for warning in unfixable {
        assert_eq!(warning.fix(element), None, "for `{warning:?}`");
    }
}

/// The whole round trip: validate a tariff, propose the edits, apply them.
#[test]
fn a_tariff_loses_its_null_and_unexpected_fields() {
    const TARIFF: &str = r#"{
  "country_code": "NL",
  "party_id": "TDR",
  "id": "TEST",
  "currency": "EUR",
  "tariff_alt_url": null,
  "made_up_field": "nonsense",
  "elements": [
    {
      "price_components": [
        {
          "type": "ENERGY",
          "price": 0.38,
          "step_size": 1
        }
      ]
    }
  ],
  "last_updated": "2020-01-01T00:00:00Z"
}"#;

    test::setup();

    let (tariff, warnings) = validated(TARIFF);
    let doc = tariff.as_doc();
    let proposed = edits(doc, &warnings).unwrap();
    let fixed = apply(doc, &proposed).unwrap();

    assert!(
        !fixed.contains("tariff_alt_url") && !fixed.contains("made_up_field"),
        "both fields should be gone, got:\n{fixed}"
    );

    // Everything the schema does define is still there, formatting and all.
    assert!(fixed.contains("\"currency\": \"EUR\""), "got:\n{fixed}");
    assert!(fixed.contains("\"price\": 0.38"), "got:\n{fixed}");

    // The fixed tariff has neither warning left.
    let (_tariff, after) = validated(&fixed);
    let remaining: Vec<String> = after
        .iter()
        .flat_map(|group| group.warnings())
        .map(|warning| warning.id().as_str().to_owned())
        .collect();

    assert_eq!(remaining, Vec::<String>::new());
}

/// Filtering the set is how a caller declines a fix, so the trait never has to know.
#[test]
fn filtering_the_set_leaves_the_unexpected_field_in_place() {
    const TARIFF: &str = r#"{"made_up_field": "nonsense", "currency": null}"#;

    test::setup();

    let (tariff, mut warnings) = validated(TARIFF);
    warnings.remove_unexpected_fields();

    let doc = tariff.as_doc();
    let proposed = edits(doc, &warnings).unwrap();
    let fixed = apply(doc, &proposed).unwrap();

    assert_eq!(fixed, r#"{"made_up_field": "nonsense"}"#.to_owned());
}