1pub(crate) mod currency {
4 use tracing::{debug, instrument};
5
6 use crate::{
7 currency,
8 json::{self, FromJson as _},
9 warning::{self, GatherWarnings as _, IntoCaveat as _},
10 Verdict,
11 };
12
13 #[instrument(skip_all)]
15 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), currency::Warning> {
16 let mut warnings = warning::Set::<currency::Warning>::new();
17 let code = currency::Code::from_json(elem)?.gather_warnings_into(&mut warnings);
18
19 debug!("code: {code:?}");
20
21 Ok(().into_caveat(warnings))
22 }
23}
24
25pub(crate) mod datetime {
26 use chrono::{DateTime, Utc};
27 use tracing::instrument;
28
29 use crate::{
30 json::{self, FromJson as _},
31 lint::tariff::Warning,
32 warning::{self, GatherWarnings as _, IntoCaveat as _},
33 Verdict,
34 };
35
36 #[instrument(skip_all)]
41 pub(crate) fn lint_start_end(
42 start_date_time_elem: Option<&json::Element<'_>>,
43 end_date_time_elem: Option<&json::Element<'_>>,
44 ) -> Verdict<(), Warning> {
45 let mut warnings = warning::Set::<Warning>::new();
46
47 let start_date = start_date_time_elem
48 .map(DateTime::<Utc>::from_json)
49 .transpose()?
50 .gather_warnings_into(&mut warnings);
51 let end_date = end_date_time_elem
52 .map(DateTime::<Utc>::from_json)
53 .transpose()?
54 .gather_warnings_into(&mut warnings);
55
56 if let Some(((start, start_elem), end)) = start_date.zip(start_date_time_elem).zip(end_date)
57 {
58 if start > end {
59 warnings.with_elem(Warning::StartDateTimeIsAfterEndDateTime, start_elem);
60 }
61 }
62
63 Ok(().into_caveat(warnings))
64 }
65}
66
67pub mod time {
68 use std::fmt;
73
74 use chrono::{NaiveTime, Timelike as _};
75
76 use crate::{
77 datetime, from_warning_all,
78 json::{self, FromJson as _},
79 warning::{self, GatherWarnings as _, IntoCaveat as _},
80 Verdict,
81 };
82
83 const DAY_BOUNDARY: HourMin = HourMin::new(0, 0);
84 const NEAR_END_OF_DAY: HourMin = HourMin::new(23, 59);
85
86 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
87 pub enum Warning {
88 ContainsEntireDay,
91
92 EndTimeIsNearEndOfDay,
98
99 NeverValid,
101
102 DateTime(datetime::Warning),
104 }
105
106 impl fmt::Display for Warning {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 Warning::ContainsEntireDay => f.write_str("Both `start_time` and `end_time` are defined and contain the entire day."),
110 Warning::EndTimeIsNearEndOfDay => f.write_str(r#"
111The `end_time` restriction is set to `23::59`.
112
113The spec states: "To stop at end of the day use: 00:00.".
114
115See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#146-tariffrestrictions-class>"#),
116 Warning::NeverValid => f.write_str("The `start_time` and `end_time` are equal and so the element is never valid."),
117 Warning::DateTime(kind) => fmt::Display::fmt(kind, f),
118 }
119 }
120 }
121
122 impl crate::Warning for Warning {
123 fn id(&self) -> warning::Id {
124 match self {
125 Warning::ContainsEntireDay => warning::Id::from_static("contains_entire_day"),
126 Warning::EndTimeIsNearEndOfDay => {
127 warning::Id::from_static("end_time_is_near_end_of_day")
128 }
129 Warning::NeverValid => warning::Id::from_static("never_valid"),
130 Warning::DateTime(kind) => kind.id(),
131 }
132 }
133 }
134
135 from_warning_all!(datetime::Warning => Warning::DateTime);
136
137 pub(crate) fn lint(
139 start_time_elem: Option<&json::Element<'_>>,
140 end_time_elem: Option<&json::Element<'_>>,
141 ) -> Verdict<(), Warning> {
142 let mut warnings = warning::Set::<Warning>::new();
143
144 let start = elem_to_time_hm(start_time_elem, &mut warnings)?;
145 let end = elem_to_time_hm(end_time_elem, &mut warnings)?;
146
147 if let Some(((start_time, start_elem), (end_time, end_elem))) = start.zip(end) {
149 if end_time == NEAR_END_OF_DAY {
150 warnings.with_elem(Warning::EndTimeIsNearEndOfDay, end_elem);
151 }
152
153 if start_time == DAY_BOUNDARY && is_day_end(end_time) {
154 warnings.with_elem(Warning::ContainsEntireDay, start_elem);
155 } else if start_time == end_time {
156 warnings.with_elem(Warning::NeverValid, start_elem);
157 }
158 } else if let Some((start_time, start_elem)) = start {
159 if start_time == DAY_BOUNDARY {
160 warnings.with_elem(Warning::ContainsEntireDay, start_elem);
161 }
162 } else if let Some((end_time, end_elem)) = end {
163 if is_day_end(end_time) {
164 warnings.with_elem(Warning::ContainsEntireDay, end_elem);
165 }
166 }
167
168 Ok(().into_caveat(warnings))
169 }
170
171 #[derive(Copy, Clone, Eq, PartialEq)]
173 struct HourMin {
174 hour: u32,
176
177 min: u32,
179 }
180
181 impl HourMin {
182 const fn new(hour: u32, min: u32) -> Self {
184 Self { hour, min }
185 }
186 }
187
188 fn is_day_end(time: HourMin) -> bool {
190 time == NEAR_END_OF_DAY || time == DAY_BOUNDARY
191 }
192
193 fn elem_to_time_hm<'a, 'bin>(
195 time_elem: Option<&'a json::Element<'bin>>,
196 warnings: &mut warning::Set<Warning>,
197 ) -> Result<Option<(HourMin, &'a json::Element<'bin>)>, warning::ErrorSet<Warning>> {
198 let v = time_elem.map(NaiveTime::from_json).transpose()?;
199
200 Ok(v.gather_warnings_into(warnings)
201 .map(|t| HourMin {
202 hour: t.hour(),
203 min: t.minute(),
204 })
205 .zip(time_elem))
206 }
207}
208
209pub mod elements {
210 use std::fmt;
216
217 use tracing::instrument;
218
219 use crate::{
220 from_warning_all,
221 json::{self, FieldsAsExt as _},
222 warning::{self, GatherWarnings as _, IntoCaveat as _},
223 Verdict,
224 };
225
226 use super::restrictions;
227
228 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
229 pub enum Warning {
230 Empty,
232
233 InvalidType,
235
236 RequiredField,
238
239 Restrictions(restrictions::Warning),
241 }
242
243 impl fmt::Display for Warning {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 match self {
246 Warning::Empty => write!(
247 f,
248 "An empty list of days means that no day is allowed. Is this what you want?"
249 ),
250 Warning::InvalidType => write!(f, "The value should be an array."),
251 Warning::RequiredField => write!(f, "The `$.elements` field is required."),
252 Warning::Restrictions(kind) => fmt::Display::fmt(kind, f),
253 }
254 }
255 }
256
257 impl crate::Warning for Warning {
258 fn id(&self) -> warning::Id {
259 match self {
260 Warning::Empty => warning::Id::from_static("empty"),
261 Warning::InvalidType => warning::Id::from_static("invalid_type"),
262 Warning::RequiredField => warning::Id::from_static("required"),
263 Warning::Restrictions(kind) => kind.id(),
264 }
265 }
266 }
267
268 from_warning_all!(restrictions::Warning => Warning::Restrictions);
269
270 #[instrument(skip_all)]
274 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), Warning> {
275 let mut warnings = warning::Set::<Warning>::new();
276
277 let Some(items) = elem.as_array() else {
279 return warnings.bail(Warning::InvalidType, elem);
280 };
281
282 if items.is_empty() {
284 return warnings.bail(Warning::Empty, elem);
285 }
286
287 for ocpi_element in items {
288 let Some(fields) = ocpi_element.as_object_fields() else {
289 return warnings.bail(Warning::InvalidType, ocpi_element);
290 };
291
292 let restrictions = fields.find_field("restrictions");
293
294 if let Some(field) = restrictions {
296 restrictions::lint(field.element()).gather_warnings_into(&mut warnings)?;
297 }
298 }
299
300 Ok(().into_caveat(warnings))
301 }
302}
303
304pub mod restrictions {
305 use std::fmt;
311
312 use tracing::instrument;
313
314 use crate::{
315 from_warning_all,
316 json::{self, FieldsAsExt as _},
317 warning::{self, DeescalateError, GatherWarnings as _, IntoCaveat as _},
318 Verdict,
319 };
320
321 use super::{time, weekday};
322
323 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
324 pub enum Warning {
325 Weekday(weekday::Warning),
327
328 InvalidType,
330
331 Time(time::Warning),
333 }
334
335 impl fmt::Display for Warning {
336 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337 match self {
338 Warning::Weekday(kind) => fmt::Display::fmt(kind, f),
339 Warning::InvalidType => write!(f, "The value should be an object."),
340 Warning::Time(kind) => fmt::Display::fmt(kind, f),
341 }
342 }
343 }
344
345 impl crate::Warning for Warning {
346 fn id(&self) -> warning::Id {
347 match self {
348 Warning::Weekday(kind) => kind.id(),
349 Warning::InvalidType => warning::Id::from_static("invalid_type"),
350 Warning::Time(kind) => kind.id(),
351 }
352 }
353 }
354
355 from_warning_all!(
356 weekday::Warning => Warning::Weekday,
357 time::Warning => Warning::Time
358 );
359
360 #[instrument(skip_all)]
362 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), Warning> {
363 let mut warnings = warning::Set::<Warning>::new();
364
365 let Some(fields) = elem.as_object_fields() else {
366 return warnings.bail(Warning::InvalidType, elem);
367 };
368
369 let fields = fields.as_raw_map();
370
371 {
372 let start_time = fields.get("start_time").map(|e| &**e);
373 let end_time = fields.get("end_time").map(|e| &**e);
374
375 let _drop: Option<()> = time::lint(start_time, end_time)
376 .gather_warnings_into(&mut warnings)
377 .deescalate_error_into(&mut warnings);
378 }
379
380 {
381 let day_of_week = fields.get("day_of_week").map(|e| &**e);
382
383 let _drop: Option<()> = weekday::lint(day_of_week)
384 .gather_warnings_into(&mut warnings)
385 .deescalate_error_into(&mut warnings);
386 }
387
388 Ok(().into_caveat(warnings))
389 }
390}
391
392pub mod weekday {
393 use std::{collections::BTreeSet, fmt, sync::LazyLock};
401
402 use crate::{
403 from_warning_all,
404 json::{self, FromJson},
405 warning::{self, GatherWarnings as _, IntoCaveat as _},
406 Verdict, Weekday,
407 };
408
409 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
410 pub enum Warning {
411 ContainsEntireWeek,
413
414 Weekday(crate::weekday::Warning),
416
417 Duplicates,
419
420 Empty,
422
423 InvalidType,
425
426 Unsorted,
428 }
429
430 impl fmt::Display for Warning {
431 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432 match self {
433 Warning::ContainsEntireWeek => write!(f, "All days of the week are defined. You can simply leave out the `day_of_week` field."),
434 Warning::Weekday(kind) => fmt::Display::fmt(kind, f),
435 Warning::Duplicates => write!(f, "There's at least one duplicate day."),
436 Warning::Empty => write!(
437 f,
438 "An empty list of days means that no day is allowed. Is this what you want?"
439 ),
440 Warning::InvalidType => write!(f, "The value should be an array."),
441 Warning::Unsorted => write!(f, "The days are unsorted."),
442 }
443 }
444 }
445
446 impl crate::Warning for Warning {
447 fn id(&self) -> warning::Id {
448 match self {
449 Warning::ContainsEntireWeek => warning::Id::from_static("contains_entire_week"),
450 Warning::Weekday(kind) => kind.id(),
451 Warning::Duplicates => warning::Id::from_static("duplicates"),
452 Warning::Empty => warning::Id::from_static("empty"),
453 Warning::InvalidType => warning::Id::from_static("invalid_type"),
454 Warning::Unsorted => warning::Id::from_static("unsorted"),
455 }
456 }
457 }
458
459 from_warning_all!(crate::weekday::Warning => Warning::Weekday);
460
461 pub(crate) fn lint(elem: Option<&json::Element<'_>>) -> Verdict<(), Warning> {
463 static ALL_DAYS_OF_WEEK: LazyLock<BTreeSet<Weekday>> = LazyLock::new(|| {
465 BTreeSet::from([
466 Weekday::Monday,
467 Weekday::Tuesday,
468 Weekday::Wednesday,
469 Weekday::Thursday,
470 Weekday::Friday,
471 Weekday::Saturday,
472 Weekday::Sunday,
473 ])
474 });
475
476 let mut warnings = warning::Set::<Warning>::new();
477
478 let Some(elem) = elem else {
480 return Ok(().into_caveat(warnings));
481 };
482
483 let Some(items) = elem.as_array() else {
485 return warnings.bail(Warning::InvalidType, elem);
486 };
487
488 if items.is_empty() {
491 warnings.with_elem(Warning::Empty, elem);
492 return Ok(().into_caveat(warnings));
493 }
494
495 let days = items
497 .iter()
498 .map(Weekday::from_json)
499 .collect::<Result<Vec<_>, _>>()?;
500
501 let days = days.gather_warnings_into(&mut warnings);
503
504 if !days.is_sorted() {
506 warnings.with_elem(Warning::Unsorted, elem);
507 }
508
509 let day_set: BTreeSet<_> = days.iter().copied().collect();
510
511 if day_set.len() != days.len() {
514 warnings.with_elem(Warning::Duplicates, elem);
515 }
516
517 if day_set == *ALL_DAYS_OF_WEEK {
520 warnings.with_elem(Warning::ContainsEntireWeek, elem);
521 }
522
523 Ok(().into_caveat(warnings))
524 }
525}