pub(crate) mod currency {
use tracing::{debug, instrument};
use crate::{
currency,
json::{self, FromJson as _},
warning::{self, GatherWarnings as _, IntoCaveat as _},
Verdict,
};
#[instrument(skip_all)]
pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), currency::Warning> {
let mut warnings = warning::Set::<currency::Warning>::new();
let code = currency::Code::from_json(elem)?.gather_warnings_into(&mut warnings);
debug!("code: {code:?}");
Ok(().into_caveat(warnings))
}
}
pub(crate) mod datetime {
use chrono::{DateTime, Utc};
use tracing::instrument;
use crate::{
json::{self, FromJson as _},
lint::tariff::Warning,
warning::{self, GatherWarnings as _, IntoCaveat as _},
Verdict,
};
#[instrument(skip_all)]
pub(crate) fn lint_start_end(
start_date_time_elem: Option<&json::Element<'_>>,
end_date_time_elem: Option<&json::Element<'_>>,
) -> Verdict<(), Warning> {
let mut warnings = warning::Set::<Warning>::new();
let start_date = start_date_time_elem
.map(DateTime::<Utc>::from_json)
.transpose()?
.gather_warnings_into(&mut warnings);
let end_date = end_date_time_elem
.map(DateTime::<Utc>::from_json)
.transpose()?
.gather_warnings_into(&mut warnings);
if let Some(((start, start_elem), end)) = start_date.zip(start_date_time_elem).zip(end_date)
{
if start > end {
warnings.insert(Warning::StartDateTimeIsAfterEndDateTime, start_elem);
}
}
Ok(().into_caveat(warnings))
}
}
pub mod time {
use std::fmt;
use chrono::{NaiveTime, Timelike as _};
use crate::{
datetime, from_warning_all,
json::{self, FromJson as _},
warning::{self, GatherWarnings as _, IntoCaveat as _},
Verdict,
};
const DAY_BOUNDARY: HourMin = HourMin::new(0, 0);
const NEAR_END_OF_DAY: HourMin = HourMin::new(23, 59);
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
ContainsEntireDay,
EndTimeIsNearEndOfDay,
NeverValid,
DateTime(datetime::Warning),
}
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ContainsEntireDay => f.write_str("Both `start_time` and `end_time` are defined and contain the entire day."),
Self::EndTimeIsNearEndOfDay => f.write_str(r#"
The `end_time` restriction is set to `23::59`.
The spec states: "To stop at end of the day use: 00:00.".
See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#146-tariffrestrictions-class>"#),
Self::NeverValid => f.write_str("The `start_time` and `end_time` are equal and so the element is never valid."),
Self::DateTime(kind) => fmt::Display::fmt(kind, f),
}
}
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::ContainsEntireDay => warning::Id::from_static("contains_entire_day"),
Self::EndTimeIsNearEndOfDay => {
warning::Id::from_static("end_time_is_near_end_of_day")
}
Self::NeverValid => warning::Id::from_static("never_valid"),
Self::DateTime(kind) => kind.id(),
}
}
}
from_warning_all!(datetime::Warning => Warning::DateTime);
pub(crate) fn lint(
start_time_elem: Option<&json::Element<'_>>,
end_time_elem: Option<&json::Element<'_>>,
) -> Verdict<(), Warning> {
let mut warnings = warning::Set::<Warning>::new();
let start = elem_to_time_hm(start_time_elem, &mut warnings)?;
let end = elem_to_time_hm(end_time_elem, &mut warnings)?;
if let Some(((start_time, start_elem), (end_time, end_elem))) = start.zip(end) {
if end_time == NEAR_END_OF_DAY {
warnings.insert(Warning::EndTimeIsNearEndOfDay, end_elem);
}
if start_time == DAY_BOUNDARY && is_day_end(end_time) {
warnings.insert(Warning::ContainsEntireDay, start_elem);
} else if start_time == end_time {
warnings.insert(Warning::NeverValid, start_elem);
}
} else if let Some((start_time, start_elem)) = start {
if start_time == DAY_BOUNDARY {
warnings.insert(Warning::ContainsEntireDay, start_elem);
}
} else if let Some((end_time, end_elem)) = end {
if is_day_end(end_time) {
warnings.insert(Warning::ContainsEntireDay, end_elem);
}
}
Ok(().into_caveat(warnings))
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct HourMin {
hour: u32,
min: u32,
}
impl HourMin {
const fn new(hour: u32, min: u32) -> Self {
Self { hour, min }
}
}
fn is_day_end(time: HourMin) -> bool {
time == NEAR_END_OF_DAY || time == DAY_BOUNDARY
}
fn elem_to_time_hm<'a, 'buf>(
time_elem: Option<&'a json::Element<'buf>>,
warnings: &mut warning::Set<Warning>,
) -> Result<Option<(HourMin, &'a json::Element<'buf>)>, warning::ErrorSet<Warning>> {
let v = time_elem.map(NaiveTime::from_json).transpose()?;
Ok(v.gather_warnings_into(warnings)
.map(|t| HourMin {
hour: t.hour(),
min: t.minute(),
})
.zip(time_elem))
}
}
pub mod elements {
use std::{borrow::Cow, collections::HashSet, fmt};
use tracing::instrument;
use crate::{
from_warning_all,
json::{self, FieldsAsExt as _},
lint::Item,
required_field,
tariff::v2x::DimensionType,
warning::{self, DeescalateError as _, GatherWarnings as _, IntoCaveat as _},
Verdict,
};
use super::{price_components, restrictions};
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
Empty,
FieldRequired { field_name: Cow<'static, str> },
InvalidType { type_found: json::ValueKind },
MissingCatchAll,
PriceComponents(price_components::Warning),
Restrictions(restrictions::Warning),
}
impl Warning {
fn invalid_type(elem: &json::Element<'_>) -> Self {
Self::InvalidType {
type_found: elem.value().kind(),
}
}
}
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(
f,
"An empty list of days means that no day is allowed. Is this what you want?"
),
Self::FieldRequired { field_name } => {
write!(f, "Field is required: `{field_name}`")
}
Self::InvalidType { type_found } => {
write!(f, "The value should be an array but is `{type_found}`")
}
Self::MissingCatchAll => write!(
f,
"The last element should have no restrictions so that it catches all cases."
),
Self::PriceComponents(warning) => fmt::Display::fmt(warning, f),
Self::Restrictions(warning) => fmt::Display::fmt(warning, f),
}
}
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::Empty => warning::Id::from_static("empty"),
Self::FieldRequired { field_name } => {
warning::Id::from_string(format!("field_required({field_name})"))
}
Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
Self::MissingCatchAll => warning::Id::from_static("missing_catch_all"),
Self::PriceComponents(warning) => warning.id(),
Self::Restrictions(warning) => warning.id(),
}
}
}
from_warning_all!(
price_components::Warning => Warning::PriceComponents,
restrictions::Warning => Warning::Restrictions
);
#[instrument(skip_all)]
pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), Warning> {
#[expect(
dead_code,
reason = "The `ElementSummary` will be used in an upcoming analysis PR."
)]
struct ElementSummary {
dimensions: HashSet<DimensionType>,
has_restrictions: bool,
}
let mut warnings = warning::Set::<Warning>::new();
let Some(elements) = elem.as_array() else {
return warnings.bail(Warning::invalid_type(elem), elem);
};
if elements.is_empty() {
return warnings.bail(Warning::Empty, elem);
}
let _elements = elements
.iter()
.map(|elem| {
let Some(fields) = elem.as_object_fields() else {
warnings.insert(Warning::invalid_type(elem), elem);
return Item::Invalid;
};
let restrictions = fields.find_field("restrictions");
let mut has_restrictions = false;
if let Some(field) = restrictions {
let report = restrictions::lint(field.element())
.deescalate_error_into(&mut warnings)
.gather_warnings_into(&mut warnings);
if let Some(report) = report {
let restrictions::Report { is_empty } = report;
has_restrictions = is_empty;
}
}
let fields = fields.as_raw_map();
let dimensions = required_field!(elem, fields, "price_components", warnings)
.and_then(|elem| {
price_components::lint(elem)
.deescalate_error_into(&mut warnings)
.gather_warnings_into(&mut warnings)
})
.unwrap_or_default();
let dimensions = dimensions.into_iter().filter_map(Option::from).collect();
Item::Valid(ElementSummary {
dimensions,
has_restrictions,
})
})
.collect::<Vec<_>>();
Ok(().into_caveat(warnings))
}
}
pub mod price_components {
use std::{borrow::Cow, fmt};
use tracing::instrument;
use crate::{
enumeration, from_warning_all,
json::{self, FieldsAsExt as _, FromJson as _},
lint::Item,
number, required_field,
tariff::v2x::DimensionType,
warning::{self, DeescalateError, GatherWarnings as _, IntoCaveat as _},
Money, Verdict,
};
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
Empty,
FieldRequired {
field_name: Cow<'static, str>,
},
InvalidType,
Money(number::Warning),
Type(enumeration::Warning),
}
from_warning_all!(
enumeration::Warning => Warning::Type,
number::Warning => Warning::Money
);
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(
f,
"An empty list of days means that no day is allowed. Is this what you want?"
),
Self::FieldRequired { field_name } => {
write!(f, "Field is required: `{field_name}`")
}
Self::InvalidType => write!(f, "The value should be an object."),
Self::Money(w) => fmt::Display::fmt(w, f),
Self::Type(w) => fmt::Display::fmt(w, f),
}
}
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::Empty => warning::Id::from_static("empty"),
Self::FieldRequired { field_name } => {
warning::Id::from_string(format!("field_required({field_name})"))
}
Self::InvalidType => warning::Id::from_static("invalid_type"),
Self::Money(w) => w.id(),
Self::Type(w) => w.id(),
}
}
}
#[instrument(skip_all)]
pub(super) fn lint(elem: &json::Element<'_>) -> Verdict<Vec<Item<DimensionType>>, Warning> {
let mut warnings = warning::Set::<Warning>::new();
let Some(items) = elem.as_array() else {
return warnings.bail(Warning::InvalidType, elem);
};
if items.is_empty() {
return warnings.bail(Warning::Empty, elem);
}
let dimensions: Vec<Item<DimensionType>> = items
.iter()
.map(|elem| {
let Some(fields) = elem.as_object_fields() else {
warnings.insert(Warning::InvalidType, elem);
return Item::Invalid;
};
let fields = fields.as_raw_map();
{
let price_elem = fields.get("price");
if let Some(elem) = price_elem {
let _money = Money::from_json(elem)
.deescalate_error_into(&mut warnings)
.gather_warnings_into(&mut warnings);
}
}
{
let Some(type_elem) = required_field!(elem, fields, "type", warnings) else {
return Item::Invalid;
};
let dimension = DimensionType::from_json(type_elem)
.deescalate_error_into(&mut warnings)
.gather_warnings_into(&mut warnings);
Item::from(dimension)
}
})
.collect();
Ok(dimensions.into_caveat(warnings))
}
}
pub mod restrictions {
use std::fmt;
use tracing::instrument;
use crate::{
duration::{self, Seconds},
from_warning_all,
json::{self, FieldsAsExt as _},
number,
warning::{self, DeescalateError, GatherWarnings as _, IntoCaveat as _},
Ampere, Kw, Kwh, Verdict,
};
use super::{time, weekday};
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
Duration(duration::Warning),
InvalidType {
type_found: json::ValueKind,
},
Number(number::Warning),
MaxZeroNeverMatch,
Time(time::Warning),
Weekday(weekday::Warning),
}
impl Warning {
fn invalid_type(elem: &json::Element<'_>) -> Self {
Self::InvalidType {
type_found: elem.value().kind(),
}
}
}
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Duration(warning) => fmt::Display::fmt(warning, f),
Self::InvalidType { type_found } => {
write!(f, "The value should be an object but is `{type_found}`")
}
Self::MaxZeroNeverMatch => write!(f, "This element contains a `max_*` restriction and so will never match. This element can be removed"),
Self::Number(warning) => fmt::Display::fmt(warning, f),
Self::Time(warning) => fmt::Display::fmt(warning, f),
Self::Weekday(warning) => fmt::Display::fmt(warning, f),
}
}
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::Duration(warning) => warning.id(),
Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
Self::MaxZeroNeverMatch => warning::Id::from_static("max_zero_will_never_match"),
Self::Number(warning) => warning.id(),
Self::Time(warning) => warning.id(),
Self::Weekday(warning) => warning.id(),
}
}
}
from_warning_all!(
duration::Warning => Warning::Duration,
number::Warning => Warning::Number,
time::Warning => Warning::Time,
weekday::Warning => Warning::Weekday
);
pub struct Report {
pub is_empty: bool,
}
#[instrument(skip_all)]
pub(super) fn lint(elem: &json::Element<'_>) -> Verdict<Report, Warning> {
let mut warnings = warning::Set::<Warning>::new();
let Some(fields) = elem.as_object_fields() else {
return warnings.bail(Warning::invalid_type(elem), elem);
};
let fields = fields.as_raw_map();
{
let start_time = fields.get("start_time").map(|e| &**e);
let end_time = fields.get("end_time").map(|e| &**e);
let _drop: Option<()> = time::lint(start_time, end_time)
.deescalate_error_into(&mut warnings)
.gather_warnings_into(&mut warnings);
}
{
let day_of_week = fields.get("day_of_week").map(|e| &**e);
let _drop: Option<()> = weekday::lint(day_of_week)
.deescalate_error_into(&mut warnings)
.gather_warnings_into(&mut warnings);
}
{
fields
.get("max_current")
.map(|elem| from_json_lint_zero::<Ampere>(elem, &mut warnings));
fields
.get("max_duration")
.map(|elem| from_json_lint_zero::<Seconds>(elem, &mut warnings));
fields
.get("max_kwh")
.map(|elem| from_json_lint_zero::<Kwh>(elem, &mut warnings));
fields
.get("max_power")
.map(|elem| from_json_lint_zero::<Kw>(elem, &mut warnings));
}
Ok(Report {
is_empty: fields.is_empty(),
}
.into_caveat(warnings))
}
fn from_json_lint_zero<'elem, 'buf, T>(
element: &'elem json::Element<'buf>,
warnings: &mut warning::Set<Warning>,
) -> Option<T>
where
T: json::FromJson<'buf, Warning: Into<Warning>> + number::IsZero,
{
let value = T::from_json(element)
.deescalate_error_into(warnings)
.gather_warnings_into(warnings);
if value.as_ref().is_some_and(number::IsZero::is_zero) {
warnings.insert(Warning::MaxZeroNeverMatch, element);
}
value
}
}
pub mod weekday {
use std::{collections::BTreeSet, fmt, sync::LazyLock};
use crate::{
from_warning_all,
json::{self, FromJson},
warning::{self, GatherWarnings as _, IntoCaveat as _},
Verdict, Weekday,
};
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
ContainsEntireWeek,
Enum(crate::enumeration::Warning),
Duplicates,
Empty,
InvalidType { type_found: json::ValueKind },
Unsorted,
}
impl Warning {
fn invalid_type(elem: &json::Element<'_>) -> Self {
Self::InvalidType {
type_found: elem.value().kind(),
}
}
}
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ContainsEntireWeek => write!(f, "All days of the week are defined. You can simply leave out the `day_of_week` field."),
Self::Enum(warning) => fmt::Display::fmt(warning, f),
Self::Duplicates => write!(f, "There's at least one duplicate day."),
Self::Empty => write!(
f,
"An empty list of days means that no day is allowed. Is this what you want?"
),
Self::InvalidType { type_found } => {
write!(f, "The value should be an array but is `{type_found}`")
}
Self::Unsorted => write!(f, "The days are unsorted."),
}
}
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::ContainsEntireWeek => warning::Id::from_static("contains_entire_week"),
Self::Enum(warning) => warning.id(),
Self::Duplicates => warning::Id::from_static("duplicates"),
Self::Empty => warning::Id::from_static("empty"),
Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
Self::Unsorted => warning::Id::from_static("unsorted"),
}
}
}
from_warning_all!(crate::enumeration::Warning => Warning::Enum);
pub(super) fn lint(elem: Option<&json::Element<'_>>) -> Verdict<(), Warning> {
static ALL_DAYS_OF_WEEK: LazyLock<BTreeSet<Weekday>> = LazyLock::new(|| {
BTreeSet::from([
Weekday::Monday,
Weekday::Tuesday,
Weekday::Wednesday,
Weekday::Thursday,
Weekday::Friday,
Weekday::Saturday,
Weekday::Sunday,
])
});
let mut warnings = warning::Set::<Warning>::new();
let Some(elem) = elem else {
return Ok(().into_caveat(warnings));
};
let Some(items) = elem.as_array() else {
return warnings.bail(Warning::invalid_type(elem), elem);
};
if items.is_empty() {
warnings.insert(Warning::Empty, elem);
return Ok(().into_caveat(warnings));
}
let days = items
.iter()
.map(Weekday::from_json)
.collect::<Result<Vec<_>, _>>()?;
let days = days.gather_warnings_into(&mut warnings);
if !days.is_sorted() {
warnings.insert(Warning::Unsorted, elem);
}
let day_set: BTreeSet<_> = days.iter().copied().collect();
if day_set.len() != days.len() {
warnings.insert(Warning::Duplicates, elem);
}
if day_set == *ALL_DAYS_OF_WEEK {
warnings.insert(Warning::ContainsEntireWeek, elem);
}
Ok(().into_caveat(warnings))
}
}