ocpi-tariffs 0.51.0

OCPI tariff calculations
Documentation
//! Tests for lowering a `schema::Str` into the `chrono` date/time types via `FromSchema`.
//!
//! A `schema::Str` is only produced by the builder, so each test drives a real `v2.1.1`
//! tariff through `build_tariff` and lowers its `id` field (a `Str` leaf) as the target
//! type. The leaf is just a carrier of string 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::{DateTime, NaiveDate, NaiveTime, TimeZone as _, Utc};

use super::Warning;
use crate::{
    json,
    schema::{self, v211, Integrity},
    warning, FromSchema,
};

/// A minimal, fully valid `v2.1.1` tariff. `id` is the `Str` 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 `id` 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 built `id` `Str` leaf out of a tariff.
fn id<'a, 'buf>(tariff: &'a v211::Tariff<'buf>) -> &'a schema::Str<'buf> {
    let Integrity::Ok(id) = &tariff.id else {
        panic!("the id should be built: {:?}", tariff.id);
    };
    id
}

/// 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()
}

/// Lower the `id` of a tariff built from `VALID` with its id replaced by `value` (the raw
/// JSON value text, including quotes) into the target `chrono` type `T`.
fn lower<T>(value: &str) -> crate::Verdict<T, Warning>
where
    T: for<'buf> FromSchema<'buf, schema::Str<'buf>, Warning = Warning>,
{
    let src = VALID.replace(r#""id": "ID""#, value);
    let doc = json::parse(src.as_str().into()).unwrap();
    let tariff = build_tariff(&doc);
    T::from_schema(id(&tariff))
}

#[test]
fn datetime_with_zulu_lowers_cleanly() {
    let (value, warnings) = lower::<DateTime<Utc>>(r#""id": "2024-01-01T00:00:00Z""#)
        .unwrap()
        .into_parts();

    assert_eq!(value, Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap());
    assert!(all_warnings(&warnings).is_empty());
}

#[test]
fn datetime_without_timezone_is_assumed_utc() {
    let (value, warnings) = lower::<DateTime<Utc>>(r#""id": "2024-01-01T00:00:00""#)
        .unwrap()
        .into_parts();

    assert_eq!(value, Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap());
    assert!(all_warnings(&warnings).is_empty());
}

#[test]
fn datetime_invalid_is_rejected() {
    let err_set = lower::<DateTime<Utc>>(r#""id": "not-a-date""#).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

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

#[test]
fn date_lowers_cleanly() {
    let (value, warnings) = lower::<NaiveDate>(r#""id": "2024-01-01""#)
        .unwrap()
        .into_parts();

    assert_eq!(value, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
    assert!(all_warnings(&warnings).is_empty());
}

#[test]
fn date_invalid_is_rejected() {
    let err_set = lower::<NaiveDate>(r#""id": "2024-13-99""#).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

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

#[test]
fn time_lowers_cleanly() {
    let (value, warnings) = lower::<NaiveTime>(r#""id": "08:30""#).unwrap().into_parts();

    assert_eq!(value, NaiveTime::from_hms_opt(8, 30, 0).unwrap());
    assert!(all_warnings(&warnings).is_empty());
}

#[test]
fn time_invalid_is_rejected() {
    let err_set = lower::<NaiveTime>(r#""id": "99:99""#).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

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

#[test]
fn escape_sequence_is_rejected() {
    // The escape is flagged before any parse attempt, so the content need not be a date.
    let err_set = lower::<DateTime<Utc>>(r#""id": "a\tb""#).unwrap_err();
    let (error, _warnings) = err_set.into_parts();
    let (error, _element) = error.into_parts();

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