ocpi-tariffs 0.52.0

OCPI tariff calculations
Documentation
//! Tests for lowering a string-encoded `schema::Number` into a `Decimal` via `FromSchema`.
//!
//! OCPI permits a `number` to be written as a JSON string. The builder accepts both forms
//! and records which was used; the literal form arrives with its digits already proven, so
//! only the string-encoded form still has to be decoded and parsed during lowering. Each
//! test drives a real `v2.1.1` tariff through `build_tariff` and lowers a price component's
//! `price` field.

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

use std::assert_matches;

use rust_decimal::Decimal;
use rust_decimal_macros::dec;

use super::Warning;
use crate::{
    json,
    schema::{self, Integrity},
    FromSchema as _,
};

/// A minimal, valid `v2.1.1` tariff whose price component carries a string-encoded `price`.
/// `{PRICE}` is replaced by each test with the literal JSON to place in that position.
const TARIFF: &str = r#"{
    "currency": "EUR",
    "elements": [
        {"price_components": [{"price": {PRICE}, "step_size": 1, "type": "ENERGY"}]}
    ],
    "id": "ID",
    "last_updated": "2024-01-01T00:00:00Z"
}"#;

/// Borrow the single price component's `price` `Number` leaf out of a tariff.
fn price<'a, 'buf>(tariff: &'a schema::v211::Tariff<'buf>) -> &'a schema::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(price) = &component.price else {
        panic!("price should be built: {:?}", component.price);
    };
    price
}

#[test]
fn string_encoded_number_lowers_to_decimal() {
    let src = TARIFF.replace("{PRICE}", r#""3.14159""#);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = schema::v211::build_tariff(&doc).ignore_warnings();

    let (number, warnings) = Decimal::from_schema(price(&tariff)).unwrap().into_parts();

    assert_eq!(number, dec!(3.14159));
    // Five decimal places exceeds the OCPI scale, so the value is kept and flagged.
    let warnings = warnings.path_map();
    assert_matches!(
        *warnings["$.elements[0].price_components[0].price"],
        [Warning::ExcessivePrecision]
    );
}

#[test]
fn string_encoded_non_numeric_is_rejected() {
    let src = TARIFF.replace("{PRICE}", r#""3.14A1""#);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = schema::v211::build_tariff(&doc).ignore_warnings();

    let err_set = Decimal::from_schema(price(&tariff)).unwrap_err();
    let (error, warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

    assert_matches!(error, Warning::Decimal(_));
    assert!(warnings.path_map().is_empty());
}

#[test]
fn string_encoded_with_escapes_is_rejected() {
    let src = TARIFF.replace("{PRICE}", r#""3.14\n159""#);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = schema::v211::build_tariff(&doc).ignore_warnings();

    let err_set = Decimal::from_schema(price(&tariff)).unwrap_err();
    let (error, warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

    // A number never needs escapes, so an escaped string is rejected before parsing.
    assert_matches!(error, Warning::ContainsEscapeCodes);
    assert!(warnings.path_map().is_empty());
}