ocpi-tariffs 0.51.0

OCPI tariff calculations
Documentation
//! Tests for lowering a `schema::Number` into a `Decimal` via `FromSchema`.
//!
//! A `schema::Number` is only produced by the builder, so each test drives a real
//! `v2.1.1` tariff through `build_tariff` and lowers its `price` field.

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

use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::assert_matches;

use super::Warning;
use crate::{
    json,
    schema::{v211, Integrity, Number},
    warning, FromSchema as _,
};

/// A minimal, fully valid `v2.1.1` tariff with one energy price component.
const VALID: &str = r#"{
    "currency": "EUR",
    "elements": [
        {"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]}
    ],
    "id": "T1",
    "last_updated": "2024-01-01T00:00:00Z"
}"#;

/// Borrow the single price component's `price` out of a built tariff.
fn price<'a, 'buf>(tariff: &'a v211::Tariff<'buf>) -> &'a Number<'buf> {
    let Integrity::Ok(elements) = &tariff.elements else {
        panic!("elements should be built: {:?}", tariff.elements);
    };
    let Integrity::Ok(element) = &elements[0] else {
        panic!("the element should be built");
    };
    let Integrity::Ok(components) = &element.price_components else {
        panic!("price_components should be built");
    };
    let Integrity::Ok(component) = &components[0] else {
        panic!("the price component should be built");
    };
    let Integrity::Ok(number) = &component.price else {
        panic!("the price should be built: {:?}", component.price);
    };
    number
}

/// Collect every warning across all paths into a flat list.
fn all_warnings(warnings: &warning::Set<Warning>) -> Vec<&Warning> {
    warnings.path_map().into_values().flatten().collect()
}

#[test]
fn literal_number_lowers_to_decimal() {
    let doc = json::parse(VALID.into()).unwrap();
    let tariff = build_tariff(&doc);
    let number = price(&tariff);
    assert_matches!(number, Number::Number { .. });

    let (value, warnings) = Decimal::from_schema(number).unwrap().into_parts();

    assert_eq!(value, dec!(0.25));
    assert!(all_warnings(&warnings).is_empty());
}

#[test]
fn literal_number_with_excessive_precision_warns_but_lowers() {
    let src = VALID.replace(r#""price": 0.25"#, r#""price": 0.12345"#);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = build_tariff(&doc);
    let number = price(&tariff);

    let (value, warnings) = Decimal::from_schema(number).unwrap().into_parts();

    assert_eq!(value, dec!(0.12345));
    assert_matches!(all_warnings(&warnings)[..], [Warning::ExcessivePrecision]);
}

#[test]
fn string_encoded_number_lowers_to_decimal() {
    let src = VALID.replace(r#""price": 0.25"#, r#""price": "0.25""#);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = build_tariff(&doc);
    let number = price(&tariff);
    assert_matches!(number, Number::StringEncoded { .. });

    let (value, warnings) = Decimal::from_schema(number).unwrap().into_parts();

    assert_eq!(value, dec!(0.25));
    assert!(all_warnings(&warnings).is_empty());
}

#[test]
fn string_encoded_with_escapes_is_rejected() {
    let src = VALID.replace(r#""price": 0.25"#, r#""price": "0.25\n""#);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = build_tariff(&doc);
    let number = price(&tariff);

    let err_set = Decimal::from_schema(number).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

    assert_matches!(error, Warning::ContainsEscapeCodes);
}

#[test]
fn string_encoded_non_number_is_rejected() {
    let src = VALID.replace(r#""price": 0.25"#, r#""price": "abc""#);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = build_tariff(&doc);
    let number = price(&tariff);

    let err_set = Decimal::from_schema(number).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

    assert_matches!(error, Warning::Decimal(_));
}

/// Build a `v2.1.1` tariff IR, dropping the schema-level warnings (the tests assert on
/// the warnings produced by lowering, not by the schema walk).
fn build_tariff<'buf>(doc: &json::Document<'buf>) -> v211::Tariff<'buf> {
    v211::build_tariff(doc).ignore_warnings()
}