ocpi-tariffs 0.51.0

OCPI tariff calculations
Documentation
//! Tests for lowering a `schema::Number` into `Seconds` 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 a price component's `step_size`
//! field (a `Number` leaf). The leaf is just a carrier of numeric content here; its
//! origin field is irrelevant to the lowering.

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

use std::assert_matches;

use chrono::TimeDelta;

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

/// A minimal, fully valid `v2.1.1` tariff. The price component's `step_size` is the
/// `Number` leaf the tests lower.
const VALID: &str = r#"{
    "currency": "EUR",
    "elements": [
        {"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]}
    ],
    "id": "ID",
    "last_updated": "2024-01-01T00:00:00Z"
}"#;

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

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

/// Lower the `step_size` of a tariff built from `VALID` with its step size replaced by
/// `value` (the raw JSON value text).
fn lower(value: &str) -> crate::Verdict<Seconds, Warning> {
    let src = VALID.replace(r#""step_size": 1"#, value);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = build_tariff(&doc);
    Seconds::from_schema(step_size(&tariff))
}

#[test]
fn positive_integer_lowers_to_seconds() {
    let doc = json::parse(VALID.into()).unwrap();
    let tariff = build_tariff(&doc);

    let (seconds, warnings) = Seconds::from_schema(step_size(&tariff))
        .unwrap()
        .into_parts();

    assert_eq!(TimeDelta::from(seconds), TimeDelta::seconds(1));
    assert!(warnings.path_map().is_empty());
}

#[test]
fn larger_value_lowers_to_seconds() {
    let (seconds, _warnings) = lower(r#""step_size": 3600"#).unwrap().into_parts();

    assert_eq!(TimeDelta::from(seconds), TimeDelta::seconds(3600));
}

#[test]
fn zero_lowers_to_zero_duration() {
    let (seconds, _warnings) = lower(r#""step_size": 0"#).unwrap().into_parts();

    assert_eq!(TimeDelta::from(seconds), TimeDelta::zero());
}

#[test]
fn negative_value_is_rejected() {
    let err_set = lower(r#""step_size": -5"#).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

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

#[test]
fn fractional_value_is_rejected() {
    let err_set = lower(r#""step_size": 1.5"#).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

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

#[test]
fn value_larger_than_i64_is_rejected() {
    // Fits in a `u64` but not an `i64`, so it parses then fails the `i64` conversion.
    let err_set = lower(r#""step_size": 10000000000000000000"#).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

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

#[test]
fn string_encoded_number_is_rejected() {
    // OCPI permits a number encoded as a string, and the schema builds it, but `Seconds`
    // (like its `FromJson` counterpart) only accepts a JSON number.
    let err_set = lower(r#""step_size": "3600""#).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

    assert_matches!(error, Warning::InvalidType { .. });
}