#![allow(
clippy::indexing_slicing,
clippy::unwrap_in_result,
reason = "unwraps and indexing are allowed anywhere in tests"
)]
use std::assert_matches;
use chrono::{NaiveDate, NaiveTime, TimeDelta};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use super::{Element, PriceComponent, Restrictions, Tariff};
use crate::{
currency, json,
schema::{v211, Integrity, Warning as SchemaWarning},
tariff::{v2x::DimensionType, Warning},
warning, FromSchema as _, Weekday,
};
const VALID: &str = r#"{
"currency": "EUR",
"id": "ID",
"last_updated": "2024-01-01T00:00:00Z",
"elements": [
{"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]}
]
}"#;
const VALID_RESTRICTIONS: &str = r#"{
"currency": "EUR",
"id": "ID",
"last_updated": "2024-01-01T00:00:00Z",
"elements": [
{
"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}],
"restrictions": {
"start_time": "09:00",
"end_time": "18:00",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"min_kwh": 1.0,
"max_kwh": 50.0,
"min_power": 3.0,
"max_power": 22.0,
"min_duration": 60,
"max_duration": 3600,
"day_of_week": ["MONDAY", "TUESDAY"]
}
}
]
}"#;
fn build_tariff<'buf>(doc: &json::Document<'buf>) -> v211::Tariff<'buf> {
v211::build_tariff(doc).ignore_warnings()
}
fn element<'a, 'buf>(tariff: &'a v211::Tariff<'buf>) -> &'a v211::Element<'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");
};
element
}
fn component<'a, 'buf>(tariff: &'a v211::Tariff<'buf>) -> &'a v211::PriceComponent<'buf> {
let Integrity::Ok(components) = &element(tariff).price_components else {
panic!("price_components should be built");
};
let Integrity::Ok(comp) = &components[0] else {
panic!("the price component should be built");
};
comp
}
fn restrictions<'a, 'buf>(tariff: &'a v211::Tariff<'buf>) -> &'a v211::Restrictions<'buf> {
let element = element(tariff);
let Integrity::Ok(Some(restrictions)) = &element.restrictions else {
panic!("restrictions should be built: {:?}", element.restrictions);
};
restrictions
}
fn all_warnings(warnings: &warning::Set<Warning>) -> Vec<&Warning> {
warnings.path_map().into_values().flatten().collect()
}
#[test]
fn price_component_lowers_from_schema() {
let doc = json::parse(VALID.into()).unwrap();
let tariff = build_tariff(&doc);
let (comp, warnings) = Option::<PriceComponent>::from_schema(component(&tariff))
.unwrap()
.into_parts();
let comp = comp.expect("a valid price component should build");
assert_eq!(comp.dimension_type, DimensionType::Energy);
assert_eq!(Decimal::from(comp.price), dec!(0.25));
assert_eq!(comp.step_size, 1);
assert!(all_warnings(&warnings).is_empty());
}
#[test]
fn unknown_dimension_type_drops_component() {
let src = VALID.replace(r#""type": "ENERGY""#, r#""type": "FOO""#);
let doc = json::parse(src.as_str().into()).unwrap();
let tariff = build_tariff(&doc);
let comp = Option::<PriceComponent>::from_schema(component(&tariff))
.unwrap()
.unwrap();
assert!(comp.is_none());
}
#[test]
fn missing_price_is_rejected() {
let src = VALID.replace(r#""price": 0.25, "#, "");
let doc = json::parse(src.as_str().into()).unwrap();
let tariff = build_tariff(&doc);
let err = Option::<PriceComponent>::from_schema(component(&tariff)).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn restrictions_lower_from_schema() {
let doc = json::parse(VALID_RESTRICTIONS.into()).unwrap();
let tariff = build_tariff(&doc);
let (res, warnings) = Restrictions::from_schema(restrictions(&tariff))
.unwrap()
.into_parts();
assert_eq!(res.start_time, NaiveTime::from_hms_opt(9, 0, 0));
assert_eq!(res.end_time, NaiveTime::from_hms_opt(18, 0, 0));
assert_eq!(res.start_date, NaiveDate::from_ymd_opt(2024, 1, 1));
assert_eq!(res.end_date, NaiveDate::from_ymd_opt(2024, 12, 31));
assert_eq!(res.min_kwh.map(Decimal::from), Some(dec!(1.0)));
assert_eq!(res.max_kwh.map(Decimal::from), Some(dec!(50.0)));
assert_eq!(res.min_power.map(Decimal::from), Some(dec!(3.0)));
assert_eq!(res.max_power.map(Decimal::from), Some(dec!(22.0)));
assert_eq!(res.min_duration, Some(TimeDelta::seconds(60)));
assert_eq!(res.max_duration, Some(TimeDelta::seconds(3600)));
assert_eq!(
res.day_of_week,
Some(vec![Weekday::Monday, Weekday::Tuesday])
);
assert!(all_warnings(&warnings).is_empty());
}
#[test]
fn absent_restriction_fields_are_none() {
let src = VALID.replace(
r#""price_components": ["#,
r#""restrictions": {}, "price_components": ["#,
);
let doc = json::parse(src.as_str().into()).unwrap();
let tariff = build_tariff(&doc);
let res = Restrictions::from_schema(restrictions(&tariff))
.unwrap()
.unwrap();
assert_eq!(res.start_time, None);
assert_eq!(res.end_time, None);
assert_eq!(res.start_date, None);
assert_eq!(res.end_date, None);
assert_eq!(res.min_kwh, None);
assert_eq!(res.max_kwh, None);
assert_eq!(res.min_power, None);
assert_eq!(res.max_power, None);
assert_eq!(res.min_duration, None);
assert_eq!(res.max_duration, None);
assert_eq!(res.day_of_week, None);
}
#[test]
fn null_restriction_fields_are_none() {
const ALL_NULL: &str = r#"{
"start_time": null,
"end_time": null,
"start_date": null,
"end_date": null,
"min_kwh": null,
"max_kwh": null,
"min_power": null,
"max_power": null,
"min_duration": null,
"max_duration": null,
"day_of_week": null
}"#;
let src = VALID.replace(
r#""price_components": ["#,
&format!(r#""restrictions": {ALL_NULL}, "price_components": ["#),
);
let doc = json::parse(src.as_str().into()).unwrap();
let tariff = build_tariff(&doc);
let (res, warnings) = Restrictions::from_schema(restrictions(&tariff))
.unwrap()
.into_parts();
assert_eq!(res.start_time, None);
assert_eq!(res.end_time, None);
assert_eq!(res.start_date, None);
assert_eq!(res.end_date, None);
assert_eq!(res.min_kwh, None);
assert_eq!(res.max_kwh, None);
assert_eq!(res.min_power, None);
assert_eq!(res.max_power, None);
assert_eq!(res.min_duration, None);
assert_eq!(res.max_duration, None);
assert_eq!(res.day_of_week, None);
assert!(all_warnings(&warnings).is_empty());
}
#[test]
fn string_encoded_price_lowers_cleanly() {
let src = VALID.replace(r#""price": 0.25"#, r#""price": "3.0000""#);
let doc = json::parse(src.as_str().into()).unwrap();
let built = build_tariff(&doc);
let (tariff, warnings) = Tariff::from_schema(&built).unwrap().into_parts();
assert_eq!(
Decimal::from(tariff.elements[0].price_components[0].price),
dec!(3.0000)
);
assert!(all_warnings(&warnings).is_empty());
}
#[test]
fn unusable_restriction_field_is_rejected() {
let src = VALID_RESTRICTIONS.replace(r#""min_kwh": 1.0"#, r#""min_kwh": true"#);
let doc = json::parse(src.as_str().into()).unwrap();
let tariff = build_tariff(&doc);
let err = Restrictions::from_schema(restrictions(&tariff)).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn unknown_weekday_entry_is_rejected() {
let src = VALID_RESTRICTIONS.replace(r#""TUESDAY""#, r#""FUNDAY""#);
let doc = json::parse(src.as_str().into()).unwrap();
let tariff = build_tariff(&doc);
let err = Restrictions::from_schema(restrictions(&tariff)).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn element_lowers_from_schema() {
let doc = json::parse(VALID.into()).unwrap();
let tariff = build_tariff(&doc);
let (elem, warnings) = Element::from_schema(element(&tariff)).unwrap().into_parts();
assert_eq!(elem.price_components.len(), 1);
assert_eq!(
elem.price_components[0].dimension_type,
DimensionType::Energy
);
assert!(elem.restrictions.is_none());
assert!(all_warnings(&warnings).is_empty());
}
#[test]
fn element_lowers_its_restrictions() {
let doc = json::parse(VALID_RESTRICTIONS.into()).unwrap();
let tariff = build_tariff(&doc);
let elem = Element::from_schema(element(&tariff)).unwrap().unwrap();
let restrictions = elem.restrictions.expect("the restrictions should build");
assert_eq!(restrictions.start_time, NaiveTime::from_hms_opt(9, 0, 0));
}
#[test]
fn null_restrictions_leaves_the_element_unrestricted() {
let src = VALID.replace(
r#""price_components": ["#,
r#""restrictions": null, "price_components": ["#,
);
let doc = json::parse(src.as_str().into()).unwrap();
let tariff = build_tariff(&doc);
let elem = Element::from_schema(element(&tariff)).unwrap().unwrap();
assert!(elem.restrictions.is_none());
}
#[test]
fn missing_price_components_is_rejected() {
let src = VALID.replace(
r#""price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]"#,
"",
);
let doc = json::parse(src.as_str().into()).unwrap();
let tariff = build_tariff(&doc);
let err = Element::from_schema(element(&tariff)).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn unbuildable_price_component_entry_is_rejected() {
let src = VALID.replace(r#"{"price": 0.25, "step_size": 1, "type": "ENERGY"}"#, "5");
let doc = json::parse(src.as_str().into()).unwrap();
let tariff = build_tariff(&doc);
let err = Element::from_schema(element(&tariff)).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn tariff_lowers_from_schema() {
let doc = json::parse(VALID.into()).unwrap();
let built = build_tariff(&doc);
let (tariff, warnings) = Tariff::from_schema(&built).unwrap().into_parts();
assert_eq!(&*tariff.id, "ID");
assert_eq!(tariff.currency, currency::Code::Eur);
assert_eq!(tariff.elements.len(), 1);
assert_eq!(
tariff.elements[0].price_components[0].dimension_type,
DimensionType::Energy
);
assert!(all_warnings(&warnings).is_empty());
}
#[test]
fn each_missing_required_field_is_rejected() {
for field in [r#""currency": "EUR","#, r#""id": "ID","#] {
let src = VALID.replace(field, "");
let doc = json::parse(src.as_str().into()).unwrap();
let built = build_tariff(&doc);
let err = Tariff::from_schema(&built)
.err()
.unwrap_or_else(|| panic!("a tariff without `{field}` should be rejected"));
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
}
#[test]
fn missing_elements_is_rejected() {
const NO_ELEMENTS: &str = r#"{
"currency": "EUR",
"id": "ID",
"last_updated": "2024-01-01T00:00:00Z"
}"#;
let doc = json::parse(NO_ELEMENTS.into()).unwrap();
let built = build_tariff(&doc);
let err = Tariff::from_schema(&built).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn unbuildable_element_entry_is_rejected() {
let src = VALID.replace(
r#"{"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]}"#,
"5",
);
let doc = json::parse(src.as_str().into()).unwrap();
let built = build_tariff(&doc);
let err = Tariff::from_schema(&built).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn empty_elements_lowers_to_no_elements() {
let src = VALID.replace(
r#"[
{"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]}
]"#,
"[]",
);
let doc = json::parse(src.as_str().into()).unwrap();
let (built, schema_warnings) = v211::build_tariff(&doc).into_parts();
let cardinality_warnings = schema_warnings
.path_map()
.into_values()
.flatten()
.filter(|warning| matches!(warning, SchemaWarning::Cardinality { .. }))
.count();
assert_eq!(cardinality_warnings, 1);
let tariff = Tariff::from_schema(&built).unwrap().unwrap();
assert!(tariff.elements.is_empty());
}