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.with_elem(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 Warning::ContainsEntireDay => f.write_str("Both `start_time` and `end_time` are defined and contain the entire day."),
110 Warning::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 Warning::NeverValid => f.write_str("The `start_time` and `end_time` are equal and so the element is never valid."),
117 Warning::DateTime(kind) => fmt::Display::fmt(kind, f),
118 }
119 }
120 }
121
122 impl crate::Warning for Warning {
123 fn id(&self) -> crate::SmartString {
124 match self {
125 Warning::ContainsEntireDay => "contains_entire_day".into(),
126 Warning::EndTimeIsNearEndOfDay => "end_time_is_near_end_of_day".into(),
127 Warning::NeverValid => "never_valid".into(),
128 Warning::DateTime(kind) => kind.id(),
129 }
130 }
131 }
132
133 from_warning_all!(datetime::Warning => Warning::DateTime);
134
135 pub(crate) fn lint(
137 start_time_elem: Option<&json::Element<'_>>,
138 end_time_elem: Option<&json::Element<'_>>,
139 ) -> Verdict<(), Warning> {
140 let mut warnings = warning::Set::<Warning>::new();
141
142 let start = elem_to_time_hm(start_time_elem, &mut warnings)?;
143 let end = elem_to_time_hm(end_time_elem, &mut warnings)?;
144
145 if let Some(((start_time, start_elem), (end_time, end_elem))) = start.zip(end) {
147 if end_time == NEAR_END_OF_DAY {
148 warnings.with_elem(Warning::EndTimeIsNearEndOfDay, end_elem);
149 }
150
151 if start_time == DAY_BOUNDARY && is_day_end(end_time) {
152 warnings.with_elem(Warning::ContainsEntireDay, start_elem);
153 } else if start_time == end_time {
154 warnings.with_elem(Warning::NeverValid, start_elem);
155 }
156 } else if let Some((start_time, start_elem)) = start {
157 if start_time == DAY_BOUNDARY {
158 warnings.with_elem(Warning::ContainsEntireDay, start_elem);
159 }
160 } else if let Some((end_time, end_elem)) = end {
161 if is_day_end(end_time) {
162 warnings.with_elem(Warning::ContainsEntireDay, end_elem);
163 }
164 }
165
166 Ok(().into_caveat(warnings))
167 }
168
169 #[derive(Copy, Clone, Eq, PartialEq)]
171 struct HourMin {
172 hour: u32,
174
175 min: u32,
177 }
178
179 impl HourMin {
180 const fn new(hour: u32, min: u32) -> Self {
182 Self { hour, min }
183 }
184 }
185
186 fn is_day_end(time: HourMin) -> bool {
188 time == NEAR_END_OF_DAY || time == DAY_BOUNDARY
189 }
190
191 fn elem_to_time_hm<'a, 'bin>(
193 time_elem: Option<&'a json::Element<'bin>>,
194 warnings: &mut warning::Set<Warning>,
195 ) -> Result<Option<(HourMin, &'a json::Element<'bin>)>, warning::ErrorSet<Warning>> {
196 let v = time_elem.map(NaiveTime::from_json).transpose()?;
197
198 Ok(v.gather_warnings_into(warnings)
199 .map(|t| HourMin {
200 hour: t.hour(),
201 min: t.minute(),
202 })
203 .zip(time_elem))
204 }
205}
206
207pub mod elements {
208 use std::fmt;
214
215 use tracing::instrument;
216
217 use crate::{
218 from_warning_all,
219 json::{self, FieldsAsExt as _},
220 warning::{self, GatherWarnings as _, IntoCaveat as _},
221 Verdict,
222 };
223
224 use super::restrictions;
225
226 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
227 pub enum Warning {
228 Empty,
230
231 InvalidType,
233
234 RequiredField,
236
237 Restrictions(restrictions::Warning),
239 }
240
241 impl fmt::Display for Warning {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 match self {
244 Warning::Empty => write!(
245 f,
246 "An empty list of days means that no day is allowed. Is this what you want?"
247 ),
248 Warning::InvalidType => write!(f, "The value should be an array."),
249 Warning::RequiredField => write!(f, "The `$.elements` field is required."),
250 Warning::Restrictions(kind) => fmt::Display::fmt(kind, f),
251 }
252 }
253 }
254
255 impl crate::Warning for Warning {
256 fn id(&self) -> crate::SmartString {
257 match self {
258 Warning::Empty => "empty".into(),
259 Warning::InvalidType => "invalid_type".into(),
260 Warning::RequiredField => "required".into(),
261 Warning::Restrictions(kind) => kind.id(),
262 }
263 }
264 }
265
266 from_warning_all!(restrictions::Warning => Warning::Restrictions);
267
268 #[instrument(skip_all)]
272 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), Warning> {
273 let mut warnings = warning::Set::<Warning>::new();
274
275 let Some(items) = elem.as_array() else {
277 return warnings.bail(Warning::InvalidType, elem);
278 };
279
280 if items.is_empty() {
282 return warnings.bail(Warning::Empty, elem);
283 }
284
285 for ocpi_element in items {
286 let Some(fields) = ocpi_element.as_object_fields() else {
287 return warnings.bail(Warning::InvalidType, ocpi_element);
288 };
289
290 let restrictions = fields.find_field("restrictions");
291
292 if let Some(field) = restrictions {
294 restrictions::lint(field.element()).gather_warnings_into(&mut warnings)?;
295 }
296 }
297
298 Ok(().into_caveat(warnings))
299 }
300}
301
302pub mod restrictions {
303 use std::fmt;
309
310 use tracing::instrument;
311
312 use crate::{
313 from_warning_all,
314 json::{self, FieldsAsExt as _},
315 warning::{self, DeescalateError, GatherWarnings as _, IntoCaveat as _},
316 Verdict,
317 };
318
319 use super::{time, weekday};
320
321 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
322 pub enum Warning {
323 Weekday(weekday::Warning),
325
326 InvalidType,
328
329 Time(time::Warning),
331 }
332
333 impl fmt::Display for Warning {
334 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 match self {
336 Warning::Weekday(kind) => fmt::Display::fmt(kind, f),
337 Warning::InvalidType => write!(f, "The value should be an object."),
338 Warning::Time(kind) => fmt::Display::fmt(kind, f),
339 }
340 }
341 }
342
343 impl crate::Warning for Warning {
344 fn id(&self) -> crate::SmartString {
345 match self {
346 Warning::Weekday(kind) => kind.id(),
347 Warning::InvalidType => "invalid_type".into(),
348 Warning::Time(kind) => kind.id(),
349 }
350 }
351 }
352
353 from_warning_all!(
354 weekday::Warning => Warning::Weekday,
355 time::Warning => Warning::Time
356 );
357
358 #[instrument(skip_all)]
360 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), Warning> {
361 let mut warnings = warning::Set::<Warning>::new();
362
363 let Some(fields) = elem.as_object_fields() else {
364 return warnings.bail(Warning::InvalidType, elem);
365 };
366
367 let fields = fields.as_raw_map();
368
369 {
370 let start_time = fields.get("start_time").map(|e| &**e);
371 let end_time = fields.get("end_time").map(|e| &**e);
372
373 let _drop: Option<()> = time::lint(start_time, end_time)
374 .gather_warnings_into(&mut warnings)
375 .deescalate_error_into(&mut warnings);
376 }
377
378 {
379 let day_of_week = fields.get("day_of_week").map(|e| &**e);
380
381 let _drop: Option<()> = weekday::lint(day_of_week)
382 .gather_warnings_into(&mut warnings)
383 .deescalate_error_into(&mut warnings);
384 }
385
386 Ok(().into_caveat(warnings))
387 }
388}
389
390pub mod weekday {
391 use std::{collections::BTreeSet, fmt, sync::LazyLock};
399
400 use crate::{
401 from_warning_all,
402 json::{self, FromJson},
403 warning::{self, GatherWarnings as _, IntoCaveat as _},
404 Verdict, Weekday,
405 };
406
407 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
408 pub enum Warning {
409 ContainsEntireWeek,
411
412 Weekday(crate::weekday::Warning),
414
415 Duplicates,
417
418 Empty,
420
421 InvalidType,
423
424 Unsorted,
426 }
427
428 impl fmt::Display for Warning {
429 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430 match self {
431 Warning::ContainsEntireWeek => write!(f, "All days of the week are defined. You can simply leave out the `day_of_week` field."),
432 Warning::Weekday(kind) => fmt::Display::fmt(kind, f),
433 Warning::Duplicates => write!(f, "There's at least one duplicate day."),
434 Warning::Empty => write!(
435 f,
436 "An empty list of days means that no day is allowed. Is this what you want?"
437 ),
438 Warning::InvalidType => write!(f, "The value should be an array."),
439 Warning::Unsorted => write!(f, "The days are unsorted."),
440 }
441 }
442 }
443
444 impl crate::Warning for Warning {
445 fn id(&self) -> crate::SmartString {
446 match self {
447 Warning::ContainsEntireWeek => "contains_entire_week".into(),
448 Warning::Weekday(kind) => kind.id(),
449 Warning::Duplicates => "duplicates".into(),
450 Warning::Empty => "empty".into(),
451 Warning::InvalidType => "invalid_type".into(),
452 Warning::Unsorted => "unsorted".into(),
453 }
454 }
455 }
456
457 from_warning_all!(crate::weekday::Warning => Warning::Weekday);
458
459 pub(crate) fn lint(elem: Option<&json::Element<'_>>) -> Verdict<(), Warning> {
461 static ALL_DAYS_OF_WEEK: LazyLock<BTreeSet<Weekday>> = LazyLock::new(|| {
463 BTreeSet::from([
464 Weekday::Monday,
465 Weekday::Tuesday,
466 Weekday::Wednesday,
467 Weekday::Thursday,
468 Weekday::Friday,
469 Weekday::Saturday,
470 Weekday::Sunday,
471 ])
472 });
473
474 let mut warnings = warning::Set::<Warning>::new();
475
476 let Some(elem) = elem else {
478 return Ok(().into_caveat(warnings));
479 };
480
481 let Some(items) = elem.as_array() else {
483 return warnings.bail(Warning::InvalidType, elem);
484 };
485
486 if items.is_empty() {
489 warnings.with_elem(Warning::Empty, elem);
490 return Ok(().into_caveat(warnings));
491 }
492
493 let days = items
495 .iter()
496 .map(Weekday::from_json)
497 .collect::<Result<Vec<_>, _>>()?;
498
499 let days = days.gather_warnings_into(&mut warnings);
501
502 if !days.is_sorted() {
504 warnings.with_elem(Warning::Unsorted, elem);
505 }
506
507 let day_set: BTreeSet<_> = days.iter().copied().collect();
508
509 if day_set.len() != days.len() {
512 warnings.with_elem(Warning::Duplicates, elem);
513 }
514
515 if day_set == *ALL_DAYS_OF_WEEK {
518 warnings.with_elem(Warning::ContainsEntireWeek, elem);
519 }
520
521 Ok(().into_caveat(warnings))
522 }
523}