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(
42 start_date_time: Option<&json::Element<'_>>,
43 end_date_time: Option<&json::Element<'_>>,
44 ) -> Verdict<(), WarningKind> {
45 let mut warnings = warning::Set::<WarningKind>::new();
46
47 if let Some((start_elem, end_elem)) = start_date_time.zip(end_date_time) {
48 let start = DateTime::<Utc>::from_json(start_elem)?.gather_warnings_into(&mut warnings);
49 let end = DateTime::<Utc>::from_json(end_elem)?.gather_warnings_into(&mut warnings);
50
51 if start > end {
52 warnings.with_elem(WarningKind::StartDateTimeIsAfterEndDateTime, start_elem);
53 }
54 } else if let Some(elem) = start_date_time {
55 let _ = DateTime::<Utc>::from_json(elem)?.gather_warnings_into(&mut warnings);
56 } else if let Some(elem) = end_date_time {
57 let _ = DateTime::<Utc>::from_json(elem)?.gather_warnings_into(&mut warnings);
58 }
59
60 Ok(().into_caveat(warnings))
61 }
62}
63
64pub mod time {
65 use std::{borrow::Cow, fmt};
70
71 use chrono::{NaiveTime, Timelike as _};
72
73 use crate::{
74 datetime, from_warning_set_to,
75 json::{self, FromJson as _},
76 warning::{self, GatherWarnings as _, IntoCaveat as _},
77 Verdict,
78 };
79
80 const DAY_BOUNDARY: HourMin = HourMin::new(0, 0);
81 const NEAR_END_OF_DAY: HourMin = HourMin::new(23, 59);
82
83 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
84 pub enum WarningKind {
85 ContainsEntireDay,
88
89 EndTimeIsNearEndOfDay,
95
96 NeverValid,
98
99 Time(datetime::WarningKind),
101 }
102
103 impl fmt::Display for WarningKind {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 WarningKind::ContainsEntireDay => f.write_str("Both `start_time` and `end_time` are defined and contain the entire day."),
107 WarningKind::EndTimeIsNearEndOfDay => f.write_str(r#"
108The `end_time` restriction is set to `23::59`.
109
110The spec states: "To stop at end of the day use: 00:00.".
111
112See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#146-tariffrestrictions-class>"#),
113 WarningKind::NeverValid => f.write_str("The `start_time` and `end_time` are equal and so the element is never valid."),
114 WarningKind::Time(kind) => fmt::Display::fmt(kind, f),
115 }
116 }
117 }
118
119 impl warning::Kind for WarningKind {
120 fn id(&self) -> Cow<'static, str> {
121 match self {
122 WarningKind::ContainsEntireDay => "contains_entire_day".into(),
123 WarningKind::EndTimeIsNearEndOfDay => "end_time_is_near_end_of_day".into(),
124 WarningKind::NeverValid => "never_valid".into(),
125 WarningKind::Time(kind) => kind.id(),
126 }
127 }
128 }
129
130 impl From<datetime::WarningKind> for WarningKind {
131 fn from(kind: datetime::WarningKind) -> Self {
132 Self::Time(kind)
133 }
134 }
135
136 from_warning_set_to!(datetime::WarningKind => WarningKind);
137
138 pub(crate) fn lint(
140 start_time_elem: Option<&json::Element<'_>>,
141 end_time_elem: Option<&json::Element<'_>>,
142 ) -> Verdict<(), WarningKind> {
143 let mut warnings = warning::Set::<WarningKind>::new();
144
145 let start = elem_to_time_hm(start_time_elem, &mut warnings)?;
146 let end = elem_to_time_hm(end_time_elem, &mut warnings)?;
147
148 if let Some(((start_time, start_elem), (end_time, end_elem))) = start.zip(end) {
150 if end_time == NEAR_END_OF_DAY {
151 warnings.with_elem(WarningKind::EndTimeIsNearEndOfDay, end_elem);
152 }
153
154 if start_time == DAY_BOUNDARY && is_day_end(end_time) {
155 warnings.with_elem(WarningKind::ContainsEntireDay, start_elem);
156 } else if start_time == end_time {
157 warnings.with_elem(WarningKind::NeverValid, start_elem);
158 }
159 } else if let Some((start_time, start_elem)) = start {
160 if start_time == DAY_BOUNDARY {
161 warnings.with_elem(WarningKind::ContainsEntireDay, start_elem);
162 }
163 } else if let Some((end_time, end_elem)) = end {
164 if is_day_end(end_time) {
165 warnings.with_elem(WarningKind::ContainsEntireDay, end_elem);
166 }
167 }
168
169 Ok(().into_caveat(warnings))
170 }
171
172 #[derive(Copy, Clone, Eq, PartialEq)]
174 struct HourMin {
175 hour: u32,
177
178 min: u32,
180 }
181
182 impl HourMin {
183 const fn new(hour: u32, min: u32) -> Self {
185 Self { hour, min }
186 }
187 }
188
189 fn is_day_end(time: HourMin) -> bool {
191 time == NEAR_END_OF_DAY || time == DAY_BOUNDARY
192 }
193
194 fn elem_to_time_hm<'a, 'bin>(
196 time_elem: Option<&'a json::Element<'bin>>,
197 warnings: &mut warning::Set<WarningKind>,
198 ) -> Result<Option<(HourMin, &'a json::Element<'bin>)>, warning::Set<WarningKind>> {
199 let v = time_elem.map(NaiveTime::from_json).transpose()?;
200
201 Ok(v.gather_warnings_into(warnings)
202 .map(|t| HourMin {
203 hour: t.hour(),
204 min: t.minute(),
205 })
206 .zip(time_elem))
207 }
208}
209
210pub mod elements {
211 use std::{borrow::Cow, fmt};
217
218 use tracing::instrument;
219
220 use crate::{
221 from_warning_set_to,
222 json::{self, FieldsAsExt as _},
223 warning::{self, GatherWarnings as _, IntoCaveat as _},
224 Verdict, VerdictExt,
225 };
226
227 use super::restrictions;
228
229 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
230 pub enum WarningKind {
231 Empty,
233
234 InvalidType,
236
237 RequiredField,
239
240 Restrictions(restrictions::WarningKind),
242 }
243
244 impl fmt::Display for WarningKind {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 match self {
247 WarningKind::Empty => write!(
248 f,
249 "An empty list of days means that no day is allowed. Is this what you want?"
250 ),
251 WarningKind::InvalidType => write!(f, "The value should be an array."),
252 WarningKind::RequiredField => write!(f, "The `$.elements` field is required."),
253 WarningKind::Restrictions(kind) => fmt::Display::fmt(kind, f),
254 }
255 }
256 }
257
258 impl warning::Kind for WarningKind {
259 fn id(&self) -> Cow<'static, str> {
260 match self {
261 WarningKind::Empty => "empty".into(),
262 WarningKind::InvalidType => "invalid_type".into(),
263 WarningKind::RequiredField => "required".into(),
264 WarningKind::Restrictions(kind) => kind.id(),
265 }
266 }
267 }
268
269 impl From<restrictions::WarningKind> for WarningKind {
270 fn from(kind: restrictions::WarningKind) -> Self {
271 Self::Restrictions(kind)
272 }
273 }
274
275 from_warning_set_to!(restrictions::WarningKind => WarningKind);
276
277 #[instrument(skip_all)]
281 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), WarningKind> {
282 let mut warnings = warning::Set::<WarningKind>::new();
283
284 let Some(items) = elem.as_array() else {
286 warnings.with_elem(WarningKind::InvalidType, elem);
287 return Err(warnings);
288 };
289
290 if items.is_empty() {
292 warnings.with_elem(WarningKind::Empty, elem);
293 return Err(warnings);
294 }
295
296 for ocpi_element in items {
297 let Some(fields) = ocpi_element.as_object_fields() else {
298 warnings.with_elem(WarningKind::InvalidType, ocpi_element);
299 return Err(warnings);
300 };
301
302 let restrictions = fields.find_field("restrictions");
303
304 if let Some(field) = restrictions {
306 restrictions::lint(field.element())
307 .ok_caveat()
308 .gather_warnings_into(&mut warnings);
309 }
310 }
311
312 Ok(().into_caveat(warnings))
313 }
314}
315
316pub mod restrictions {
317 use std::{borrow::Cow, fmt};
323
324 use tracing::instrument;
325
326 use crate::{
327 from_warning_set_to,
328 json::{self, FieldsAsExt as _},
329 warning::{self, GatherWarnings as _, IntoCaveat as _},
330 Verdict, VerdictExt as _,
331 };
332
333 use super::{time, weekday};
334
335 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
336 pub enum WarningKind {
337 Weekday(weekday::WarningKind),
339
340 InvalidType,
342
343 Time(time::WarningKind),
345 }
346
347 impl fmt::Display for WarningKind {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 match self {
350 WarningKind::Weekday(kind) => fmt::Display::fmt(kind, f),
351 WarningKind::InvalidType => write!(f, "The value should be an object."),
352 WarningKind::Time(kind) => fmt::Display::fmt(kind, f),
353 }
354 }
355 }
356
357 impl warning::Kind for WarningKind {
358 fn id(&self) -> Cow<'static, str> {
359 match self {
360 WarningKind::Weekday(kind) => kind.id(),
361 WarningKind::InvalidType => "invalid_type".into(),
362 WarningKind::Time(kind) => kind.id(),
363 }
364 }
365 }
366
367 from_warning_set_to!(weekday::WarningKind => WarningKind);
368 from_warning_set_to!(time::WarningKind => WarningKind);
369
370 impl From<weekday::WarningKind> for WarningKind {
371 fn from(warn_kind: weekday::WarningKind) -> Self {
372 Self::Weekday(warn_kind)
373 }
374 }
375
376 impl From<time::WarningKind> for WarningKind {
377 fn from(warn_kind: time::WarningKind) -> Self {
378 Self::Time(warn_kind)
379 }
380 }
381
382 #[instrument(skip_all)]
384 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), WarningKind> {
385 let mut warnings = warning::Set::<WarningKind>::new();
386
387 let Some(fields) = elem.as_object_fields() else {
388 warnings.with_elem(WarningKind::InvalidType, elem);
389 return Err(warnings);
390 };
391
392 let fields = fields.as_raw_map();
393
394 {
395 let start_time = fields.get("start_time").map(|e| &**e);
396 let end_time = fields.get("end_time").map(|e| &**e);
397
398 time::lint(start_time, end_time)
399 .ok_caveat()
400 .gather_warnings_into(&mut warnings);
401 }
402
403 {
404 let day_of_week = fields.get("day_of_week").map(|e| &**e);
405
406 weekday::lint(day_of_week)
407 .ok_caveat()
408 .gather_warnings_into(&mut warnings);
409 }
410
411 Ok(().into_caveat(warnings))
412 }
413}
414
415pub mod weekday {
416 use std::{borrow::Cow, collections::BTreeSet, fmt, sync::LazyLock};
424
425 use crate::{
426 from_warning_set_to,
427 json::{self, FromJson},
428 warning::{self, GatherWarnings as _, IntoCaveat as _},
429 Verdict, Weekday,
430 };
431
432 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
433 pub enum WarningKind {
434 ContainsEntireWeek,
436
437 Weekday(crate::weekday::WarningKind),
439
440 Duplicates,
442
443 Empty,
445
446 InvalidType,
448
449 Unsorted,
451 }
452
453 impl fmt::Display for WarningKind {
454 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455 match self {
456 WarningKind::ContainsEntireWeek => write!(f, "All days of the week are defined."),
457 WarningKind::Weekday(kind) => fmt::Display::fmt(kind, f),
458 WarningKind::Duplicates => write!(f, "There's at least one duplicate day."),
459 WarningKind::Empty => write!(
460 f,
461 "An empty list of days means that no day is allowed. Is this what you want?"
462 ),
463 WarningKind::InvalidType => write!(f, "The value should be an array."),
464 WarningKind::Unsorted => write!(f, "The days are unsorted."),
465 }
466 }
467 }
468
469 impl warning::Kind for WarningKind {
470 fn id(&self) -> Cow<'static, str> {
471 match self {
472 WarningKind::ContainsEntireWeek => "contains_entire_week".into(),
473 WarningKind::Weekday(kind) => kind.id(),
474 WarningKind::Duplicates => "duplicates".into(),
475 WarningKind::Empty => "empty".into(),
476 WarningKind::InvalidType => "invalid_type".into(),
477 WarningKind::Unsorted => "unsorted".into(),
478 }
479 }
480 }
481
482 impl From<crate::weekday::WarningKind> for WarningKind {
483 fn from(kind: crate::weekday::WarningKind) -> Self {
484 Self::Weekday(kind)
485 }
486 }
487
488 from_warning_set_to!(crate::weekday::WarningKind => WarningKind);
489
490 pub(crate) fn lint(elem: Option<&json::Element<'_>>) -> Verdict<(), WarningKind> {
492 static ALL_DAYS_OF_WEEK: LazyLock<BTreeSet<Weekday>> = LazyLock::new(|| {
494 BTreeSet::from([
495 Weekday::Monday,
496 Weekday::Tuesday,
497 Weekday::Wednesday,
498 Weekday::Thursday,
499 Weekday::Friday,
500 Weekday::Saturday,
501 Weekday::Sunday,
502 ])
503 });
504
505 let mut warnings = warning::Set::<WarningKind>::new();
506
507 let Some(elem) = elem else {
509 return Ok(().into_caveat(warnings));
510 };
511
512 let Some(items) = elem.as_array() else {
514 warnings.with_elem(WarningKind::InvalidType, elem);
515 return Err(warnings);
516 };
517
518 if items.is_empty() {
521 warnings.with_elem(WarningKind::Empty, elem);
522 return Ok(().into_caveat(warnings));
523 }
524
525 let days = items
527 .iter()
528 .map(Weekday::from_json)
529 .collect::<Result<Vec<_>, _>>()?;
530
531 let days = days.gather_warnings_into(&mut warnings);
533
534 if !days.is_sorted() {
536 warnings.with_elem(WarningKind::Unsorted, elem);
537 }
538
539 let day_set: BTreeSet<_> = days.iter().copied().collect();
540
541 if day_set.len() != days.len() {
544 warnings.with_elem(WarningKind::Duplicates, elem);
545 }
546
547 if day_set == *ALL_DAYS_OF_WEEK {
550 warnings.with_elem(WarningKind::ContainsEntireWeek, elem);
551 }
552
553 Ok(().into_caveat(warnings))
554 }
555}