1pub(crate) mod currency {
4 use tracing::{debug, instrument};
5
6 use crate::{
7 currency, json,
8 warning::{self, GatherWarnings as _, IntoCaveat as _},
9 Verdict,
10 };
11
12 #[instrument(skip_all)]
14 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<currency::Code, currency::WarningKind> {
15 let mut warnings = warning::Set::<currency::WarningKind>::new();
16 let code = currency::Code::from_json(elem)?.gather_warnings_into(&mut warnings);
17
18 debug!("code: {code:?}");
19
20 Ok(code.into_caveat(warnings))
21 }
22}
23
24pub(crate) mod datetime {
25 use chrono::{DateTime, Utc};
26 use tracing::instrument;
27
28 use crate::{
29 json::{self, FromJson as _},
30 lint::tariff::WarningKind,
31 warning::{self, GatherWarnings as _, IntoCaveat as _},
32 Verdict,
33 };
34
35 #[instrument(skip_all)]
40 pub(crate) fn lint(
41 start_date_time: Option<&json::Element<'_>>,
42 end_date_time: Option<&json::Element<'_>>,
43 ) -> Verdict<(), WarningKind> {
44 let mut warnings = warning::Set::<WarningKind>::new();
45
46 if let Some((start_elem, end_elem)) = start_date_time.zip(end_date_time) {
47 let start = DateTime::<Utc>::from_json(start_elem)?.gather_warnings_into(&mut warnings);
48 let end = DateTime::<Utc>::from_json(end_elem)?.gather_warnings_into(&mut warnings);
49
50 if start > end {
51 warnings.with_elem(WarningKind::StartDateTimeIsAfterEndDateTime, start_elem);
52 }
53 } else if let Some(elem) = start_date_time {
54 let _ = DateTime::<Utc>::from_json(elem)?.gather_warnings_into(&mut warnings);
55 } else if let Some(elem) = end_date_time {
56 let _ = DateTime::<Utc>::from_json(elem)?.gather_warnings_into(&mut warnings);
57 }
58
59 Ok(().into_caveat(warnings))
60 }
61}
62
63pub mod time {
64 use std::{borrow::Cow, fmt};
69
70 use chrono::{NaiveTime, Timelike as _};
71
72 use crate::{
73 datetime, from_warning_set_to,
74 json::{self, FromJson as _},
75 warning::{self, GatherWarnings as _, IntoCaveat as _},
76 Verdict,
77 };
78
79 const DAY_BOUNDARY: HourMin = HourMin::new(0, 0);
80 const NEAR_END_OF_DAY: HourMin = HourMin::new(23, 59);
81
82 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
83 pub enum WarningKind {
84 ContainsEntireDay,
87
88 EndTimeIsNearEndOfDay,
94
95 NeverValid,
97
98 Time(datetime::WarningKind),
100 }
101
102 impl fmt::Display for WarningKind {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 WarningKind::ContainsEntireDay => f.write_str("Both `start_time` and `end_time` are defined and contain the entire day."),
106 WarningKind::EndTimeIsNearEndOfDay => f.write_str(r#"
107The `end_time` restriction is set to `23::59`.
108
109The spec states: "To stop at end of the day use: 00:00.".
110
111See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#146-tariffrestrictions-class>"#),
112 WarningKind::NeverValid => f.write_str("The `start_time` and `end_time` are equal and so the element is never valid."),
113 WarningKind::Time(kind) => fmt::Display::fmt(kind, f),
114 }
115 }
116 }
117
118 impl warning::Kind for WarningKind {
119 fn id(&self) -> Cow<'static, str> {
120 match self {
121 WarningKind::ContainsEntireDay => "contains_entire_day".into(),
122 WarningKind::EndTimeIsNearEndOfDay => "end_time_is_near_end_of_day".into(),
123 WarningKind::NeverValid => "never_valid".into(),
124 WarningKind::Time(kind) => format!("time.{}", kind.id()).into(),
125 }
126 }
127 }
128
129 impl From<datetime::WarningKind> for WarningKind {
130 fn from(kind: datetime::WarningKind) -> Self {
131 Self::Time(kind)
132 }
133 }
134
135 from_warning_set_to!(datetime::WarningKind => WarningKind);
136
137 pub(crate) fn lint(
139 start_time_elem: Option<&json::Element<'_>>,
140 end_time_elem: Option<&json::Element<'_>>,
141 ) -> Verdict<(), WarningKind> {
142 let mut warnings = warning::Set::<WarningKind>::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(WarningKind::EndTimeIsNearEndOfDay, end_elem);
151 }
152
153 if start_time == DAY_BOUNDARY && is_day_end(end_time) {
154 warnings.with_elem(WarningKind::ContainsEntireDay, start_elem);
155 } else if start_time == end_time {
156 warnings.with_elem(WarningKind::NeverValid, start_elem);
157 }
158 } else if let Some((start_time, start_elem)) = start {
159 if start_time == DAY_BOUNDARY {
160 warnings.with_elem(WarningKind::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(WarningKind::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<WarningKind>,
197 ) -> Result<Option<(HourMin, &'a json::Element<'bin>)>, warning::Set<WarningKind>> {
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::{borrow::Cow, fmt};
216
217 use tracing::instrument;
218
219 use crate::{
220 from_warning_set_to,
221 json::{self, FieldsAsExt as _},
222 warning::{self, GatherWarnings as _, IntoCaveat as _},
223 Verdict, VerdictExt,
224 };
225
226 use super::restrictions;
227
228 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
229 pub enum WarningKind {
230 Empty,
232
233 InvalidType,
235
236 RequiredField,
238
239 Restrictions(restrictions::WarningKind),
241 }
242
243 impl fmt::Display for WarningKind {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 match self {
246 WarningKind::Empty => write!(
247 f,
248 "An empty list of days means that no day is allowed. Is this what you want?"
249 ),
250 WarningKind::InvalidType => write!(f, "The value should be an array."),
251 WarningKind::RequiredField => write!(f, "The `$.elements` field is required."),
252 WarningKind::Restrictions(kind) => fmt::Display::fmt(kind, f),
253 }
254 }
255 }
256
257 impl warning::Kind for WarningKind {
258 fn id(&self) -> Cow<'static, str> {
259 match self {
260 WarningKind::Empty => "empty".into(),
261 WarningKind::InvalidType => "invalid_type".into(),
262 WarningKind::RequiredField => "required".into(),
263 WarningKind::Restrictions(kind) => format!("restrictions.{}", kind.id()).into(),
264 }
265 }
266 }
267
268 impl From<restrictions::WarningKind> for WarningKind {
269 fn from(kind: restrictions::WarningKind) -> Self {
270 Self::Restrictions(kind)
271 }
272 }
273
274 from_warning_set_to!(restrictions::WarningKind => WarningKind);
275
276 #[instrument(skip_all)]
280 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), WarningKind> {
281 let mut warnings = warning::Set::<WarningKind>::new();
282
283 let Some(items) = elem.as_array() else {
285 warnings.with_elem(WarningKind::InvalidType, elem);
286 return Err(warnings);
287 };
288
289 if items.is_empty() {
291 warnings.with_elem(WarningKind::Empty, elem);
292 return Err(warnings);
293 }
294
295 for ocpi_element in items {
296 let Some(fields) = ocpi_element.as_object_fields() else {
297 warnings.with_elem(WarningKind::InvalidType, ocpi_element);
298 return Err(warnings);
299 };
300
301 let restrictions = fields.find_field("restrictions");
302
303 if let Some(field) = restrictions {
305 restrictions::lint(field.element())
306 .ok_caveat()
307 .gather_warnings_into(&mut warnings);
308 }
309 }
310
311 Ok(().into_caveat(warnings))
312 }
313}
314
315pub mod restrictions {
316 use std::{borrow::Cow, fmt};
322
323 use tracing::instrument;
324
325 use crate::{
326 from_warning_set_to,
327 json::{self, FieldsAsExt as _},
328 warning::{self, GatherWarnings as _, IntoCaveat as _},
329 Verdict, VerdictExt as _,
330 };
331
332 use super::{time, weekday};
333
334 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
335 pub enum WarningKind {
336 Weekday(weekday::WarningKind),
338
339 InvalidType,
341
342 Time(time::WarningKind),
344 }
345
346 impl fmt::Display for WarningKind {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 match self {
349 WarningKind::Weekday(kind) => fmt::Display::fmt(kind, f),
350 WarningKind::InvalidType => write!(f, "The value should be an object."),
351 WarningKind::Time(kind) => fmt::Display::fmt(kind, f),
352 }
353 }
354 }
355
356 impl warning::Kind for WarningKind {
357 fn id(&self) -> Cow<'static, str> {
358 match self {
359 WarningKind::Weekday(kind) => format!("day_of_week.{}", kind.id()).into(),
360 WarningKind::InvalidType => "invalid_type".into(),
361 WarningKind::Time(kind) => format!("time.{}", kind.id()).into(),
362 }
363 }
364 }
365
366 from_warning_set_to!(weekday::WarningKind => WarningKind);
367 from_warning_set_to!(time::WarningKind => WarningKind);
368
369 impl From<weekday::WarningKind> for WarningKind {
370 fn from(warn_kind: weekday::WarningKind) -> Self {
371 Self::Weekday(warn_kind)
372 }
373 }
374
375 impl From<time::WarningKind> for WarningKind {
376 fn from(warn_kind: time::WarningKind) -> Self {
377 Self::Time(warn_kind)
378 }
379 }
380
381 #[instrument(skip_all)]
383 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), WarningKind> {
384 let mut warnings = warning::Set::<WarningKind>::new();
385
386 let Some(fields) = elem.as_object_fields() else {
387 warnings.with_elem(WarningKind::InvalidType, elem);
388 return Err(warnings);
389 };
390
391 let fields = fields.as_raw_map();
392
393 {
394 let start_time = fields.get("start_time").map(|e| &**e);
395 let end_time = fields.get("end_time").map(|e| &**e);
396
397 time::lint(start_time, end_time)
398 .ok_caveat()
399 .gather_warnings_into(&mut warnings);
400 }
401
402 {
403 let day_of_week = fields.get("day_of_week").map(|e| &**e);
404
405 weekday::lint(day_of_week)
406 .ok_caveat()
407 .gather_warnings_into(&mut warnings);
408 }
409
410 Ok(().into_caveat(warnings))
411 }
412}
413
414pub mod weekday {
415 use std::{borrow::Cow, collections::BTreeSet, fmt, sync::LazyLock};
423
424 use crate::{
425 from_warning_set_to,
426 json::{self, FromJson},
427 warning::{self, GatherWarnings as _, IntoCaveat as _},
428 Verdict, Weekday,
429 };
430
431 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
432 pub enum WarningKind {
433 ContainsEntireWeek,
435
436 Weekday(crate::weekday::WarningKind),
438
439 Duplicates,
441
442 Empty,
444
445 InvalidType,
447
448 Unsorted,
450 }
451
452 impl fmt::Display for WarningKind {
453 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454 match self {
455 WarningKind::ContainsEntireWeek => write!(f, "All days of the week are defined."),
456 WarningKind::Weekday(kind) => fmt::Display::fmt(kind, f),
457 WarningKind::Duplicates => write!(f, "There's at least one duplicate day."),
458 WarningKind::Empty => write!(
459 f,
460 "An empty list of days means that no day is allowed. Is this what you want?"
461 ),
462 WarningKind::InvalidType => write!(f, "The value should be an array."),
463 WarningKind::Unsorted => write!(f, "The days are unsorted."),
464 }
465 }
466 }
467
468 impl warning::Kind for WarningKind {
469 fn id(&self) -> Cow<'static, str> {
470 match self {
471 WarningKind::ContainsEntireWeek => "contains_entire_week".into(),
472 WarningKind::Weekday(kind) => format!("day_of_week.{}", kind.id()).into(),
473 WarningKind::Duplicates => "duplicates".into(),
474 WarningKind::Empty => "empty".into(),
475 WarningKind::InvalidType => "invalid_type".into(),
476 WarningKind::Unsorted => "unsorted".into(),
477 }
478 }
479 }
480
481 impl From<crate::weekday::WarningKind> for WarningKind {
482 fn from(kind: crate::weekday::WarningKind) -> Self {
483 Self::Weekday(kind)
484 }
485 }
486
487 from_warning_set_to!(crate::weekday::WarningKind => WarningKind);
488
489 pub(crate) fn lint(elem: Option<&json::Element<'_>>) -> Verdict<(), WarningKind> {
491 static ALL_DAYS_OF_WEEK: LazyLock<BTreeSet<Weekday>> = LazyLock::new(|| {
493 BTreeSet::from([
494 Weekday::Monday,
495 Weekday::Tuesday,
496 Weekday::Wednesday,
497 Weekday::Thursday,
498 Weekday::Friday,
499 Weekday::Saturday,
500 Weekday::Sunday,
501 ])
502 });
503
504 let mut warnings = warning::Set::<WarningKind>::new();
505
506 let Some(elem) = elem else {
508 return Ok(().into_caveat(warnings));
509 };
510
511 let Some(items) = elem.as_array() else {
513 warnings.with_elem(WarningKind::InvalidType, elem);
514 return Err(warnings);
515 };
516
517 if items.is_empty() {
520 warnings.with_elem(WarningKind::Empty, elem);
521 return Ok(().into_caveat(warnings));
522 }
523
524 let days = items
526 .iter()
527 .map(Weekday::from_json)
528 .collect::<Result<Vec<_>, _>>()?;
529
530 let days = days
532 .into_iter()
533 .map(|v| v.gather_warnings_into(&mut warnings))
534 .collect::<Vec<_>>();
535
536 if !days.is_sorted() {
538 warnings.with_elem(WarningKind::Unsorted, elem);
539 }
540
541 let day_set: BTreeSet<_> = days.iter().copied().collect();
542
543 if day_set.len() != days.len() {
546 warnings.with_elem(WarningKind::Duplicates, elem);
547 }
548
549 if day_set == *ALL_DAYS_OF_WEEK {
552 warnings.with_elem(WarningKind::ContainsEntireWeek, elem);
553 }
554
555 Ok(().into_caveat(warnings))
556 }
557}