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.insert(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 Self::ContainsEntireDay => f.write_str("Both `start_time` and `end_time` are defined and contain the entire day."),
110 Self::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 Self::NeverValid => f.write_str("The `start_time` and `end_time` are equal and so the element is never valid."),
117 Self::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 Self::ContainsEntireDay => warning::Id::from_static("contains_entire_day"),
126 Self::EndTimeIsNearEndOfDay => {
127 warning::Id::from_static("end_time_is_near_end_of_day")
128 }
129 Self::NeverValid => warning::Id::from_static("never_valid"),
130 Self::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.insert(Warning::EndTimeIsNearEndOfDay, end_elem);
151 }
152
153 if start_time == DAY_BOUNDARY && is_day_end(end_time) {
154 warnings.insert(Warning::ContainsEntireDay, start_elem);
155 } else if start_time == end_time {
156 warnings.insert(Warning::NeverValid, start_elem);
157 }
158 } else if let Some((start_time, start_elem)) = start {
159 if start_time == DAY_BOUNDARY {
160 warnings.insert(Warning::ContainsEntireDay, start_elem);
161 }
162 } else if let Some((end_time, end_elem)) = end {
163 if is_day_end(end_time) {
164 warnings.insert(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, 'buf>(
195 time_elem: Option<&'a json::Element<'buf>>,
196 warnings: &mut warning::Set<Warning>,
197 ) -> Result<Option<(HourMin, &'a json::Element<'buf>)>, 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 { type_found: json::ValueKind },
235
236 RequiredField,
238
239 Restrictions(restrictions::Warning),
241 }
242
243 impl Warning {
244 fn invalid_type(elem: &json::Element<'_>) -> Self {
245 Self::InvalidType {
246 type_found: elem.value().kind(),
247 }
248 }
249 }
250
251 impl fmt::Display for Warning {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 match self {
254 Self::Empty => write!(
255 f,
256 "An empty list of days means that no day is allowed. Is this what you want?"
257 ),
258 Self::InvalidType { type_found } => {
259 write!(f, "The value should be an array but is `{type_found}`")
260 }
261 Self::RequiredField => write!(f, "The `$.elements` field is required."),
262 Self::Restrictions(kind) => fmt::Display::fmt(kind, f),
263 }
264 }
265 }
266
267 impl crate::Warning for Warning {
268 fn id(&self) -> warning::Id {
269 match self {
270 Self::Empty => warning::Id::from_static("empty"),
271 Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
272 Self::RequiredField => warning::Id::from_static("required"),
273 Self::Restrictions(kind) => kind.id(),
274 }
275 }
276 }
277
278 from_warning_all!(restrictions::Warning => Warning::Restrictions);
279
280 #[instrument(skip_all)]
284 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), Warning> {
285 let mut warnings = warning::Set::<Warning>::new();
286
287 let Some(items) = elem.as_array() else {
289 return warnings.bail(Warning::invalid_type(elem), elem);
290 };
291
292 if items.is_empty() {
294 return warnings.bail(Warning::Empty, elem);
295 }
296
297 for ocpi_element in items {
298 let Some(fields) = ocpi_element.as_object_fields() else {
299 return warnings.bail(Warning::invalid_type(elem), ocpi_element);
300 };
301
302 let restrictions = fields.find_field("restrictions");
303
304 if let Some(field) = restrictions {
306 restrictions::lint(field.element()).gather_warnings_into(&mut warnings)?;
307 }
308 }
309
310 Ok(().into_caveat(warnings))
311 }
312}
313
314pub mod restrictions {
315 use std::fmt;
321
322 use tracing::instrument;
323
324 use crate::{
325 from_warning_all,
326 json::{self, FieldsAsExt as _},
327 warning::{self, DeescalateError, GatherWarnings as _, IntoCaveat as _},
328 Verdict,
329 };
330
331 use super::{time, weekday};
332
333 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
334 pub enum Warning {
335 Weekday(weekday::Warning),
337
338 InvalidType { type_found: json::ValueKind },
340
341 Time(time::Warning),
343 }
344
345 impl Warning {
346 fn invalid_type(elem: &json::Element<'_>) -> Self {
347 Self::InvalidType {
348 type_found: elem.value().kind(),
349 }
350 }
351 }
352
353 impl fmt::Display for Warning {
354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355 match self {
356 Self::Weekday(kind) => fmt::Display::fmt(kind, f),
357 Self::InvalidType { type_found } => {
358 write!(f, "The value should be an object but is `{type_found}`")
359 }
360 Self::Time(kind) => fmt::Display::fmt(kind, f),
361 }
362 }
363 }
364
365 impl crate::Warning for Warning {
366 fn id(&self) -> warning::Id {
367 match self {
368 Self::Weekday(kind) => kind.id(),
369 Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
370 Self::Time(kind) => kind.id(),
371 }
372 }
373 }
374
375 from_warning_all!(
376 weekday::Warning => Warning::Weekday,
377 time::Warning => Warning::Time
378 );
379
380 #[instrument(skip_all)]
382 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), Warning> {
383 let mut warnings = warning::Set::<Warning>::new();
384
385 let Some(fields) = elem.as_object_fields() else {
386 return warnings.bail(Warning::invalid_type(elem), elem);
387 };
388
389 let fields = fields.as_raw_map();
390
391 {
392 let start_time = fields.get("start_time").map(|e| &**e);
393 let end_time = fields.get("end_time").map(|e| &**e);
394
395 let _drop: Option<()> = time::lint(start_time, end_time)
396 .gather_warnings_into(&mut warnings)
397 .deescalate_error_into(&mut warnings);
398 }
399
400 {
401 let day_of_week = fields.get("day_of_week").map(|e| &**e);
402
403 let _drop: Option<()> = weekday::lint(day_of_week)
404 .gather_warnings_into(&mut warnings)
405 .deescalate_error_into(&mut warnings);
406 }
407
408 Ok(().into_caveat(warnings))
409 }
410}
411
412pub mod weekday {
413 use std::{collections::BTreeSet, fmt, sync::LazyLock};
421
422 use crate::{
423 from_warning_all,
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 Warning {
431 ContainsEntireWeek,
433
434 Weekday(crate::weekday::Warning),
436
437 Duplicates,
439
440 Empty,
442
443 InvalidType { type_found: json::ValueKind },
445
446 Unsorted,
448 }
449
450 impl Warning {
451 fn invalid_type(elem: &json::Element<'_>) -> Self {
452 Self::InvalidType {
453 type_found: elem.value().kind(),
454 }
455 }
456 }
457
458 impl fmt::Display for Warning {
459 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460 match self {
461 Self::ContainsEntireWeek => write!(f, "All days of the week are defined. You can simply leave out the `day_of_week` field."),
462 Self::Weekday(kind) => fmt::Display::fmt(kind, f),
463 Self::Duplicates => write!(f, "There's at least one duplicate day."),
464 Self::Empty => write!(
465 f,
466 "An empty list of days means that no day is allowed. Is this what you want?"
467 ),
468 Self::InvalidType { type_found } => {
469 write!(f, "The value should be an array but is `{type_found}`")
470 }
471 Self::Unsorted => write!(f, "The days are unsorted."),
472 }
473 }
474 }
475
476 impl crate::Warning for Warning {
477 fn id(&self) -> warning::Id {
478 match self {
479 Self::ContainsEntireWeek => warning::Id::from_static("contains_entire_week"),
480 Self::Weekday(kind) => kind.id(),
481 Self::Duplicates => warning::Id::from_static("duplicates"),
482 Self::Empty => warning::Id::from_static("empty"),
483 Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
484 Self::Unsorted => warning::Id::from_static("unsorted"),
485 }
486 }
487 }
488
489 from_warning_all!(crate::weekday::Warning => Warning::Weekday);
490
491 pub(crate) fn lint(elem: Option<&json::Element<'_>>) -> Verdict<(), Warning> {
493 static ALL_DAYS_OF_WEEK: LazyLock<BTreeSet<Weekday>> = LazyLock::new(|| {
495 BTreeSet::from([
496 Weekday::Monday,
497 Weekday::Tuesday,
498 Weekday::Wednesday,
499 Weekday::Thursday,
500 Weekday::Friday,
501 Weekday::Saturday,
502 Weekday::Sunday,
503 ])
504 });
505
506 let mut warnings = warning::Set::<Warning>::new();
507
508 let Some(elem) = elem else {
510 return Ok(().into_caveat(warnings));
511 };
512
513 let Some(items) = elem.as_array() else {
515 return warnings.bail(Warning::invalid_type(elem), elem);
516 };
517
518 if items.is_empty() {
521 warnings.insert(Warning::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.insert(Warning::Unsorted, elem);
537 }
538
539 let day_set: BTreeSet<_> = days.iter().copied().collect();
540
541 if day_set.len() != days.len() {
544 warnings.insert(Warning::Duplicates, elem);
545 }
546
547 if day_set == *ALL_DAYS_OF_WEEK {
550 warnings.insert(Warning::ContainsEntireWeek, elem);
551 }
552
553 Ok(().into_caveat(warnings))
554 }
555}