1pub mod currency {
4 use tracing::{debug, instrument};
5
6 use crate::{
7 currency, json,
8 warning::{self, IntoCaveat as _},
9 Verdict,
10 };
11
12 #[instrument(skip_all)]
14 pub 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 mod datetime {
25 use tracing::instrument;
26
27 use crate::{
28 datetime::DateTime,
29 json::{self, FromJson as _},
30 lint::tariff::WarningKind,
31 warning::{self, IntoCaveat as _, IntoWarning as _},
32 Verdict,
33 };
34
35 #[instrument(skip_all)]
40 pub 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::from_json(start_elem)?.gather_warnings_into(&mut warnings);
48 let end = DateTime::from_json(end_elem)?.gather_warnings_into(&mut warnings);
49
50 if start > end {
51 warnings
52 .push(WarningKind::StartDateTimeIsAfterEndDateTime.into_warning(start_elem));
53 }
54 } else if let Some(elem) = start_date_time {
55 let _ = DateTime::from_json(elem)?.gather_warnings_into(&mut warnings);
56 } else if let Some(elem) = end_date_time {
57 let _ = DateTime::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::{self, Time},
75 from_warning_set_to,
76 json::{self, FromJson as _},
77 warning::{self, IntoCaveat as _, IntoWarning, OptionCaveat as _},
78 Verdict,
79 };
80
81 const DAY_BOUNDARY: HourMin = HourMin::new(0, 0);
82 const NEAR_END_OF_DAY: HourMin = HourMin::new(23, 59);
83
84 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
85 pub enum WarningKind {
86 ContainsEntireDay,
89
90 EndTimeIsNearEndOfDay,
96
97 NeverValid,
99
100 Time(datetime::time::WarningKind),
102 }
103
104 impl fmt::Display for WarningKind {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 WarningKind::ContainsEntireDay => f.write_str("Both `start_time` and `end_time` are defined and contain the entire day."),
108 WarningKind::EndTimeIsNearEndOfDay => f.write_str(r#"
109The `end_time` restriction is set to `23::59`.
110
111The spec states: "To stop at end of the day use: 00:00.".
112
113See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#146-tariffrestrictions-class>"#),
114 WarningKind::NeverValid => f.write_str("The `start_time` and `end_time` are equal and so the element is never valid."),
115 WarningKind::Time(kind) => fmt::Display::fmt(kind, f),
116 }
117 }
118 }
119
120 impl warning::Kind for WarningKind {
121 fn id(&self) -> Cow<'static, str> {
122 match self {
123 WarningKind::ContainsEntireDay => "contains_entire_day".into(),
124 WarningKind::EndTimeIsNearEndOfDay => "end_time_is_near_end_of_day".into(),
125 WarningKind::NeverValid => "never_valid".into(),
126 WarningKind::Time(kind) => format!("time.{}", kind.id()).into(),
127 }
128 }
129 }
130
131 impl From<datetime::time::WarningKind> for WarningKind {
132 fn from(kind: datetime::time::WarningKind) -> Self {
133 Self::Time(kind)
134 }
135 }
136
137 from_warning_set_to!(datetime::time::WarningKind => WarningKind);
138
139 pub fn lint(
141 start_time_elem: Option<&json::Element<'_>>,
142 end_time_elem: Option<&json::Element<'_>>,
143 ) -> Verdict<(), WarningKind> {
144 let mut warnings = warning::Set::<WarningKind>::new();
145
146 let start = elem_to_time_hm(start_time_elem, &mut warnings)?;
147 let end = elem_to_time_hm(end_time_elem, &mut warnings)?;
148
149 if let Some(((start_time, start_elem), (end_time, end_elem))) = start.zip(end) {
151 if end_time == NEAR_END_OF_DAY {
152 warnings.push(WarningKind::EndTimeIsNearEndOfDay.into_warning(end_elem));
153 }
154
155 if start_time == DAY_BOUNDARY && is_day_end(end_time) {
156 warnings.push(WarningKind::ContainsEntireDay.into_warning(start_elem));
157 } else if start_time == end_time {
158 warnings.push(WarningKind::NeverValid.into_warning(start_elem));
159 }
160 } else if let Some((start_time, start_elem)) = start {
161 if start_time == DAY_BOUNDARY {
162 warnings.push(WarningKind::ContainsEntireDay.into_warning(start_elem));
163 }
164 } else if let Some((end_time, end_elem)) = end {
165 if is_day_end(end_time) {
166 warnings.push(WarningKind::ContainsEntireDay.into_warning(end_elem));
167 }
168 }
169
170 Ok(().into_caveat(warnings))
171 }
172
173 #[derive(Copy, Clone, Eq, PartialEq)]
175 struct HourMin {
176 hour: u32,
178
179 min: u32,
181 }
182
183 impl HourMin {
184 const fn new(hour: u32, min: u32) -> Self {
186 Self { hour, min }
187 }
188 }
189
190 fn is_day_end(time: HourMin) -> bool {
192 time == NEAR_END_OF_DAY || time == DAY_BOUNDARY
193 }
194
195 fn elem_to_time_hm<'a, 'bin>(
197 time_elem: Option<&'a json::Element<'bin>>,
198 warnings: &mut warning::Set<WarningKind>,
199 ) -> Result<Option<(HourMin, &'a json::Element<'bin>)>, warning::Set<WarningKind>> {
200 let v = time_elem.map(Time::from_json).transpose()?;
201
202 Ok(v.gather_warnings_into(warnings)
203 .map(|t| {
204 let t = NaiveTime::from(t);
205 HourMin {
206 hour: t.hour(),
207 min: t.minute(),
208 }
209 })
210 .zip(time_elem))
211 }
212}
213
214pub mod elements {
215 use std::{borrow::Cow, fmt};
221
222 use tracing::instrument;
223
224 use crate::{
225 from_warning_set_to,
226 json::{self, FieldsAsExt as _},
227 warning::{self, IntoCaveat as _, IntoWarning},
228 Verdict, VerdictExt,
229 };
230
231 use super::restrictions;
232
233 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
234 pub enum WarningKind {
235 Empty,
237
238 InvalidType,
240
241 RequiredField,
243
244 Restrictions(restrictions::WarningKind),
246 }
247
248 impl fmt::Display for WarningKind {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 match self {
251 WarningKind::Empty => write!(
252 f,
253 "An empty list of days means that no day is allowed. Is this what you want?"
254 ),
255 WarningKind::InvalidType => write!(f, "The value should be an array."),
256 WarningKind::RequiredField => write!(f, "The `$.elements` field is required."),
257 WarningKind::Restrictions(kind) => fmt::Display::fmt(kind, f),
258 }
259 }
260 }
261
262 impl warning::Kind for WarningKind {
263 fn id(&self) -> Cow<'static, str> {
264 match self {
265 WarningKind::Empty => "empty".into(),
266 WarningKind::InvalidType => "invalid_type".into(),
267 WarningKind::RequiredField => "required".into(),
268 WarningKind::Restrictions(kind) => format!("restrictions.{}", kind.id()).into(),
269 }
270 }
271 }
272
273 impl From<restrictions::WarningKind> for WarningKind {
274 fn from(kind: restrictions::WarningKind) -> Self {
275 Self::Restrictions(kind)
276 }
277 }
278
279 from_warning_set_to!(restrictions::WarningKind => WarningKind);
280
281 #[instrument(skip_all)]
285 pub fn lint(elem: &json::Element<'_>) -> Verdict<(), WarningKind> {
286 let mut warnings = warning::Set::<WarningKind>::new();
287
288 let Some(items) = elem.as_array() else {
290 warnings.push(WarningKind::InvalidType.into_warning(elem));
291 return Err(warnings);
292 };
293
294 if items.is_empty() {
296 warnings.push(WarningKind::Empty.into_warning(elem));
297 return Err(warnings);
298 }
299
300 for ocpi_element in items {
301 let Some(fields) = ocpi_element.as_object_fields() else {
302 warnings.push(WarningKind::InvalidType.into_warning(ocpi_element));
303 return Err(warnings);
304 };
305
306 let restrictions = fields.find_field("restrictions");
307
308 if let Some(field) = restrictions {
310 restrictions::lint(field.element())
311 .ok_caveat()
312 .gather_warnings_into(&mut warnings);
313 }
314 }
315
316 Ok(().into_caveat(warnings))
317 }
318}
319
320pub mod restrictions {
321 use std::{borrow::Cow, fmt};
327
328 use tracing::instrument;
329
330 use crate::{
331 from_warning_set_to,
332 json::{self, FieldsAsExt as _},
333 warning::{self, IntoCaveat as _, IntoWarning},
334 Verdict, VerdictExt as _,
335 };
336
337 use super::{day_of_week, time};
338
339 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
340 pub enum WarningKind {
341 DayOfWeek(day_of_week::WarningKind),
343
344 InvalidType,
346
347 Time(time::WarningKind),
349 }
350
351 impl fmt::Display for WarningKind {
352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353 match self {
354 WarningKind::DayOfWeek(kind) => fmt::Display::fmt(kind, f),
355 WarningKind::InvalidType => write!(f, "The value should be an object."),
356 WarningKind::Time(kind) => fmt::Display::fmt(kind, f),
357 }
358 }
359 }
360
361 impl warning::Kind for WarningKind {
362 fn id(&self) -> Cow<'static, str> {
363 match self {
364 WarningKind::DayOfWeek(kind) => format!("day_of_week.{}", kind.id()).into(),
365 WarningKind::InvalidType => "invalid_type".into(),
366 WarningKind::Time(kind) => format!("time.{}", kind.id()).into(),
367 }
368 }
369 }
370
371 from_warning_set_to!(day_of_week::WarningKind => WarningKind);
372 from_warning_set_to!(time::WarningKind => WarningKind);
373
374 impl From<day_of_week::WarningKind> for WarningKind {
375 fn from(warn_kind: day_of_week::WarningKind) -> Self {
376 Self::DayOfWeek(warn_kind)
377 }
378 }
379
380 impl From<time::WarningKind> for WarningKind {
381 fn from(warn_kind: time::WarningKind) -> Self {
382 Self::Time(warn_kind)
383 }
384 }
385
386 #[instrument(skip_all)]
388 pub fn lint(elem: &json::Element<'_>) -> Verdict<(), WarningKind> {
389 let mut warnings = warning::Set::<WarningKind>::new();
390
391 let Some(fields) = elem.as_object_fields() else {
392 warnings.push(WarningKind::InvalidType.into_warning(elem));
393 return Err(warnings);
394 };
395
396 let fields = fields.as_raw_map();
397
398 {
399 let start_time = fields.get("start_time").map(|e| &**e);
400 let end_time = fields.get("end_time").map(|e| &**e);
401
402 time::lint(start_time, end_time)
403 .ok_caveat()
404 .gather_warnings_into(&mut warnings);
405 }
406
407 {
408 let day_of_week = fields.get("day_of_week").map(|e| &**e);
409
410 day_of_week::lint(day_of_week)
411 .ok_caveat()
412 .gather_warnings_into(&mut warnings);
413 }
414
415 Ok(().into_caveat(warnings))
416 }
417}
418
419pub mod day_of_week {
420 use std::{borrow::Cow, collections::BTreeSet, fmt, sync::LazyLock};
428
429 use crate::{
430 from_warning_set_to,
431 json::{self, FromJson},
432 types::{day_of_week, DayOfWeek},
433 warning::{self, IntoCaveat as _, IntoWarning},
434 Verdict,
435 };
436
437 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
438 pub enum WarningKind {
439 ContainsEntireWeek,
441
442 DayOfWeek(day_of_week::WarningKind),
444
445 Duplicates,
447
448 Empty,
450
451 InvalidType,
453
454 Unsorted,
456 }
457
458 impl fmt::Display for WarningKind {
459 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460 match self {
461 WarningKind::ContainsEntireWeek => write!(f, "All days of the week are defined."),
462 WarningKind::DayOfWeek(kind) => fmt::Display::fmt(kind, f),
463 WarningKind::Duplicates => write!(f, "There's at least one duplicate day."),
464 WarningKind::Empty => write!(
465 f,
466 "An empty list of days means that no day is allowed. Is this what you want?"
467 ),
468 WarningKind::InvalidType => write!(f, "The value should be an array."),
469 WarningKind::Unsorted => write!(f, "The days are unsorted."),
470 }
471 }
472 }
473
474 impl warning::Kind for WarningKind {
475 fn id(&self) -> Cow<'static, str> {
476 match self {
477 WarningKind::ContainsEntireWeek => "contains_entire_week".into(),
478 WarningKind::DayOfWeek(kind) => format!("day_of_week.{}", kind.id()).into(),
479 WarningKind::Duplicates => "duplicates".into(),
480 WarningKind::Empty => "empty".into(),
481 WarningKind::InvalidType => "invalid_type".into(),
482 WarningKind::Unsorted => "unsorted".into(),
483 }
484 }
485 }
486
487 impl From<day_of_week::WarningKind> for WarningKind {
488 fn from(kind: day_of_week::WarningKind) -> Self {
489 Self::DayOfWeek(kind)
490 }
491 }
492
493 from_warning_set_to!(day_of_week::WarningKind => WarningKind);
494
495 pub fn lint(elem: Option<&json::Element<'_>>) -> Verdict<(), WarningKind> {
497 static ALL_DAYS_OF_WEEK: LazyLock<BTreeSet<DayOfWeek>> = LazyLock::new(|| {
499 BTreeSet::from([
500 DayOfWeek::Monday,
501 DayOfWeek::Tuesday,
502 DayOfWeek::Wednesday,
503 DayOfWeek::Thursday,
504 DayOfWeek::Friday,
505 DayOfWeek::Saturday,
506 DayOfWeek::Sunday,
507 ])
508 });
509
510 let mut warnings = warning::Set::<WarningKind>::new();
511
512 let Some(elem) = elem else {
514 return Ok(().into_caveat(warnings));
515 };
516
517 let Some(items) = elem.as_array() else {
519 warnings.push(WarningKind::InvalidType.into_warning(elem));
520 return Err(warnings);
521 };
522
523 if items.is_empty() {
526 warnings.push(WarningKind::Empty.into_warning(elem));
527 return Ok(().into_caveat(warnings));
528 }
529
530 let days = items
532 .iter()
533 .map(DayOfWeek::from_json)
534 .collect::<Result<Vec<_>, _>>()?;
535
536 let days = days
538 .into_iter()
539 .map(|v| v.gather_warnings_into(&mut warnings))
540 .collect::<Vec<_>>();
541
542 if !days.is_sorted() {
544 warnings.push(WarningKind::Unsorted.into_warning(elem));
545 }
546
547 let day_set: BTreeSet<_> = days.iter().copied().collect();
548
549 if day_set.len() != days.len() {
552 warnings.push(WarningKind::Duplicates.into_warning(elem));
553 }
554
555 if day_set == *ALL_DAYS_OF_WEEK {
558 warnings.push(WarningKind::ContainsEntireWeek.into_warning(elem));
559 }
560
561 Ok(().into_caveat(warnings))
562 }
563}