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::Code, currency::WarningKind> {
16 let mut warnings = warning::Set::<currency::WarningKind>::new();
17 let code = currency::Code::from_json(elem)?.gather_warnings_into(&mut warnings);
18
19 debug!("code: {code:?}");
20
21 Ok(code.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::WarningKind,
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<(), WarningKind> {
45 let mut warnings = warning::Set::<WarningKind>::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(WarningKind::StartDateTimeIsAfterEndDateTime, start_elem);
60 }
61 }
62
63 Ok(().into_caveat(warnings))
64 }
65}
66
67pub mod time {
68 use std::{borrow::Cow, fmt};
73
74 use chrono::{NaiveTime, Timelike as _};
75
76 use crate::{
77 datetime, from_warning_set_to,
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 WarningKind {
88 ContainsEntireDay,
91
92 EndTimeIsNearEndOfDay,
98
99 NeverValid,
101
102 Time(datetime::WarningKind),
104 }
105
106 impl fmt::Display for WarningKind {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 WarningKind::ContainsEntireDay => f.write_str("Both `start_time` and `end_time` are defined and contain the entire day."),
110 WarningKind::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 WarningKind::NeverValid => f.write_str("The `start_time` and `end_time` are equal and so the element is never valid."),
117 WarningKind::Time(kind) => fmt::Display::fmt(kind, f),
118 }
119 }
120 }
121
122 impl warning::Kind for WarningKind {
123 fn id(&self) -> Cow<'static, str> {
124 match self {
125 WarningKind::ContainsEntireDay => "contains_entire_day".into(),
126 WarningKind::EndTimeIsNearEndOfDay => "end_time_is_near_end_of_day".into(),
127 WarningKind::NeverValid => "never_valid".into(),
128 WarningKind::Time(kind) => kind.id(),
129 }
130 }
131 }
132
133 impl From<datetime::WarningKind> for WarningKind {
134 fn from(kind: datetime::WarningKind) -> Self {
135 Self::Time(kind)
136 }
137 }
138
139 from_warning_set_to!(datetime::WarningKind => WarningKind);
140
141 pub(crate) fn lint(
143 start_time_elem: Option<&json::Element<'_>>,
144 end_time_elem: Option<&json::Element<'_>>,
145 ) -> Verdict<(), WarningKind> {
146 let mut warnings = warning::Set::<WarningKind>::new();
147
148 let start = elem_to_time_hm(start_time_elem, &mut warnings)?;
149 let end = elem_to_time_hm(end_time_elem, &mut warnings)?;
150
151 if let Some(((start_time, start_elem), (end_time, end_elem))) = start.zip(end) {
153 if end_time == NEAR_END_OF_DAY {
154 warnings.with_elem(WarningKind::EndTimeIsNearEndOfDay, end_elem);
155 }
156
157 if start_time == DAY_BOUNDARY && is_day_end(end_time) {
158 warnings.with_elem(WarningKind::ContainsEntireDay, start_elem);
159 } else if start_time == end_time {
160 warnings.with_elem(WarningKind::NeverValid, start_elem);
161 }
162 } else if let Some((start_time, start_elem)) = start {
163 if start_time == DAY_BOUNDARY {
164 warnings.with_elem(WarningKind::ContainsEntireDay, start_elem);
165 }
166 } else if let Some((end_time, end_elem)) = end {
167 if is_day_end(end_time) {
168 warnings.with_elem(WarningKind::ContainsEntireDay, end_elem);
169 }
170 }
171
172 Ok(().into_caveat(warnings))
173 }
174
175 #[derive(Copy, Clone, Eq, PartialEq)]
177 struct HourMin {
178 hour: u32,
180
181 min: u32,
183 }
184
185 impl HourMin {
186 const fn new(hour: u32, min: u32) -> Self {
188 Self { hour, min }
189 }
190 }
191
192 fn is_day_end(time: HourMin) -> bool {
194 time == NEAR_END_OF_DAY || time == DAY_BOUNDARY
195 }
196
197 fn elem_to_time_hm<'a, 'bin>(
199 time_elem: Option<&'a json::Element<'bin>>,
200 warnings: &mut warning::Set<WarningKind>,
201 ) -> Result<Option<(HourMin, &'a json::Element<'bin>)>, warning::Set<WarningKind>> {
202 let v = time_elem.map(NaiveTime::from_json).transpose()?;
203
204 Ok(v.gather_warnings_into(warnings)
205 .map(|t| HourMin {
206 hour: t.hour(),
207 min: t.minute(),
208 })
209 .zip(time_elem))
210 }
211}
212
213pub mod elements {
214 use std::{borrow::Cow, fmt};
220
221 use tracing::instrument;
222
223 use crate::{
224 from_warning_set_to,
225 json::{self, FieldsAsExt as _},
226 warning::{self, GatherWarnings as _, IntoCaveat as _},
227 Verdict,
228 };
229
230 use super::restrictions;
231
232 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
233 pub enum WarningKind {
234 Empty,
236
237 InvalidType,
239
240 RequiredField,
242
243 Restrictions(restrictions::WarningKind),
245 }
246
247 impl fmt::Display for WarningKind {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 match self {
250 WarningKind::Empty => write!(
251 f,
252 "An empty list of days means that no day is allowed. Is this what you want?"
253 ),
254 WarningKind::InvalidType => write!(f, "The value should be an array."),
255 WarningKind::RequiredField => write!(f, "The `$.elements` field is required."),
256 WarningKind::Restrictions(kind) => fmt::Display::fmt(kind, f),
257 }
258 }
259 }
260
261 impl warning::Kind for WarningKind {
262 fn id(&self) -> Cow<'static, str> {
263 match self {
264 WarningKind::Empty => "empty".into(),
265 WarningKind::InvalidType => "invalid_type".into(),
266 WarningKind::RequiredField => "required".into(),
267 WarningKind::Restrictions(kind) => kind.id(),
268 }
269 }
270 }
271
272 impl From<restrictions::WarningKind> for WarningKind {
273 fn from(kind: restrictions::WarningKind) -> Self {
274 Self::Restrictions(kind)
275 }
276 }
277
278 from_warning_set_to!(restrictions::WarningKind => WarningKind);
279
280 #[instrument(skip_all)]
284 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), WarningKind> {
285 let mut warnings = warning::Set::<WarningKind>::new();
286
287 let Some(items) = elem.as_array() else {
289 warnings.with_elem(WarningKind::InvalidType, elem);
290 return Err(warnings);
291 };
292
293 if items.is_empty() {
295 warnings.with_elem(WarningKind::Empty, elem);
296 return Err(warnings);
297 }
298
299 for ocpi_element in items {
300 let Some(fields) = ocpi_element.as_object_fields() else {
301 warnings.with_elem(WarningKind::InvalidType, ocpi_element);
302 return Err(warnings);
303 };
304
305 let restrictions = fields.find_field("restrictions");
306
307 if let Some(field) = restrictions {
309 let _drop = restrictions::lint(field.element()).gather_warnings_into(&mut warnings);
310 }
311 }
312
313 Ok(().into_caveat(warnings))
314 }
315}
316
317pub mod restrictions {
318 use std::{borrow::Cow, fmt};
324
325 use tracing::instrument;
326
327 use crate::{
328 from_warning_set_to,
329 json::{self, FieldsAsExt as _},
330 warning::{self, GatherWarnings as _, IntoCaveat as _},
331 Verdict,
332 };
333
334 use super::{time, weekday};
335
336 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
337 pub enum WarningKind {
338 Weekday(weekday::WarningKind),
340
341 InvalidType,
343
344 Time(time::WarningKind),
346 }
347
348 impl fmt::Display for WarningKind {
349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350 match self {
351 WarningKind::Weekday(kind) => fmt::Display::fmt(kind, f),
352 WarningKind::InvalidType => write!(f, "The value should be an object."),
353 WarningKind::Time(kind) => fmt::Display::fmt(kind, f),
354 }
355 }
356 }
357
358 impl warning::Kind for WarningKind {
359 fn id(&self) -> Cow<'static, str> {
360 match self {
361 WarningKind::Weekday(kind) => kind.id(),
362 WarningKind::InvalidType => "invalid_type".into(),
363 WarningKind::Time(kind) => kind.id(),
364 }
365 }
366 }
367
368 from_warning_set_to!(weekday::WarningKind => WarningKind);
369 from_warning_set_to!(time::WarningKind => WarningKind);
370
371 impl From<weekday::WarningKind> for WarningKind {
372 fn from(warn_kind: weekday::WarningKind) -> Self {
373 Self::Weekday(warn_kind)
374 }
375 }
376
377 impl From<time::WarningKind> for WarningKind {
378 fn from(warn_kind: time::WarningKind) -> Self {
379 Self::Time(warn_kind)
380 }
381 }
382
383 #[instrument(skip_all)]
385 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), WarningKind> {
386 let mut warnings = warning::Set::<WarningKind>::new();
387
388 let Some(fields) = elem.as_object_fields() else {
389 warnings.with_elem(WarningKind::InvalidType, elem);
390 return Err(warnings);
391 };
392
393 let fields = fields.as_raw_map();
394
395 {
396 let start_time = fields.get("start_time").map(|e| &**e);
397 let end_time = fields.get("end_time").map(|e| &**e);
398
399 let _drop = time::lint(start_time, end_time).gather_warnings_into(&mut warnings);
400 }
401
402 {
403 let day_of_week = fields.get("day_of_week").map(|e| &**e);
404
405 let _drop = weekday::lint(day_of_week).gather_warnings_into(&mut warnings);
406 }
407
408 Ok(().into_caveat(warnings))
409 }
410}
411
412pub mod weekday {
413 use std::{borrow::Cow, collections::BTreeSet, fmt, sync::LazyLock};
421
422 use crate::{
423 from_warning_set_to,
424 json::{self, FromJson},
425 warning::{self, GatherWarnings as _, IntoCaveat as _},
426 Verdict, Weekday,
427 };
428
429 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
430 pub enum WarningKind {
431 ContainsEntireWeek,
433
434 Weekday(crate::weekday::WarningKind),
436
437 Duplicates,
439
440 Empty,
442
443 InvalidType,
445
446 Unsorted,
448 }
449
450 impl fmt::Display for WarningKind {
451 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452 match self {
453 WarningKind::ContainsEntireWeek => write!(f, "All days of the week are defined. You can simply leave out the `day_of_week` field."),
454 WarningKind::Weekday(kind) => fmt::Display::fmt(kind, f),
455 WarningKind::Duplicates => write!(f, "There's at least one duplicate day."),
456 WarningKind::Empty => write!(
457 f,
458 "An empty list of days means that no day is allowed. Is this what you want?"
459 ),
460 WarningKind::InvalidType => write!(f, "The value should be an array."),
461 WarningKind::Unsorted => write!(f, "The days are unsorted."),
462 }
463 }
464 }
465
466 impl warning::Kind for WarningKind {
467 fn id(&self) -> Cow<'static, str> {
468 match self {
469 WarningKind::ContainsEntireWeek => "contains_entire_week".into(),
470 WarningKind::Weekday(kind) => kind.id(),
471 WarningKind::Duplicates => "duplicates".into(),
472 WarningKind::Empty => "empty".into(),
473 WarningKind::InvalidType => "invalid_type".into(),
474 WarningKind::Unsorted => "unsorted".into(),
475 }
476 }
477 }
478
479 impl From<crate::weekday::WarningKind> for WarningKind {
480 fn from(kind: crate::weekday::WarningKind) -> Self {
481 Self::Weekday(kind)
482 }
483 }
484
485 from_warning_set_to!(crate::weekday::WarningKind => WarningKind);
486
487 pub(crate) fn lint(elem: Option<&json::Element<'_>>) -> Verdict<(), WarningKind> {
489 static ALL_DAYS_OF_WEEK: LazyLock<BTreeSet<Weekday>> = LazyLock::new(|| {
491 BTreeSet::from([
492 Weekday::Monday,
493 Weekday::Tuesday,
494 Weekday::Wednesday,
495 Weekday::Thursday,
496 Weekday::Friday,
497 Weekday::Saturday,
498 Weekday::Sunday,
499 ])
500 });
501
502 let mut warnings = warning::Set::<WarningKind>::new();
503
504 let Some(elem) = elem else {
506 return Ok(().into_caveat(warnings));
507 };
508
509 let Some(items) = elem.as_array() else {
511 warnings.with_elem(WarningKind::InvalidType, elem);
512 return Err(warnings);
513 };
514
515 if items.is_empty() {
518 warnings.with_elem(WarningKind::Empty, elem);
519 return Ok(().into_caveat(warnings));
520 }
521
522 let days = items
524 .iter()
525 .map(Weekday::from_json)
526 .collect::<Result<Vec<_>, _>>()?;
527
528 let days = days.gather_warnings_into(&mut warnings);
530
531 if !days.is_sorted() {
533 warnings.with_elem(WarningKind::Unsorted, elem);
534 }
535
536 let day_set: BTreeSet<_> = days.iter().copied().collect();
537
538 if day_set.len() != days.len() {
541 warnings.with_elem(WarningKind::Duplicates, elem);
542 }
543
544 if day_set == *ALL_DAYS_OF_WEEK {
547 warnings.with_elem(WarningKind::ContainsEntireWeek, elem);
548 }
549
550 Ok(().into_caveat(warnings))
551 }
552}