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::fmt;
use tracing::instrument;
use crate::{
from_warning_all,
json::{self, FieldsAsExt as _},
warning::{self, GatherWarnings as _, IntoCaveat as _},
Verdict,
};
use super::restrictions;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
Empty,
InvalidType { type_found: json::ValueKind },
RequiredField,
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::InvalidType { type_found } => {
write!(f, "The value should be an array but is `{type_found}`")
}
Self::RequiredField => write!(f, "The `$.elements` field is required."),
Self::Restrictions(kind) => fmt::Display::fmt(kind, f),
}
}
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::Empty => warning::Id::from_static("empty"),
Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
Self::RequiredField => warning::Id::from_static("required"),
Self::Restrictions(kind) => kind.id(),
}
}
}
from_warning_all!(restrictions::Warning => Warning::Restrictions);
#[instrument(skip_all)]
pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), Warning> {
let mut warnings = warning::Set::<Warning>::new();
let Some(items) = elem.as_array() else {
return warnings.bail(Warning::invalid_type(elem), elem);
};
if items.is_empty() {
return warnings.bail(Warning::Empty, elem);
}
for ocpi_element in items {
let Some(fields) = ocpi_element.as_object_fields() else {
return warnings.bail(Warning::invalid_type(elem), ocpi_element);
};
let restrictions = fields.find_field("restrictions");
if let Some(field) = restrictions {
restrictions::lint(field.element()).gather_warnings_into(&mut warnings)?;
}
}
Ok(().into_caveat(warnings))
}
}
pub mod restrictions {
use std::fmt;
use tracing::instrument;
use crate::{
from_warning_all,
json::{self, FieldsAsExt as _},
warning::{self, DeescalateError, GatherWarnings as _, IntoCaveat as _},
Verdict,
};
use super::{time, weekday};
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
Weekday(weekday::Warning),
InvalidType { type_found: json::ValueKind },
Time(time::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::Weekday(kind) => fmt::Display::fmt(kind, f),
Self::InvalidType { type_found } => {
write!(f, "The value should be an object but is `{type_found}`")
}
Self::Time(kind) => fmt::Display::fmt(kind, f),
}
}
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::Weekday(kind) => kind.id(),
Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
Self::Time(kind) => kind.id(),
}
}
}
from_warning_all!(
weekday::Warning => Warning::Weekday,
time::Warning => Warning::Time
);
#[instrument(skip_all)]
pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), 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)
.gather_warnings_into(&mut warnings)
.deescalate_error_into(&mut warnings);
}
{
let day_of_week = fields.get("day_of_week").map(|e| &**e);
let _drop: Option<()> = weekday::lint(day_of_week)
.gather_warnings_into(&mut warnings)
.deescalate_error_into(&mut warnings);
}
Ok(().into_caveat(warnings))
}
}
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,
Weekday(crate::weekday::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::Weekday(kind) => fmt::Display::fmt(kind, 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::Weekday(kind) => kind.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::weekday::Warning => Warning::Weekday);
pub(crate) 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))
}
}