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_start_end(
42 start_date_time_elem: Option<&json::Element<'_>>,
43 end_date_time_elem: Option<&json::Element<'_>>,
44 ) -> Verdict<(), WarningKind> {
45 let mut warnings = warning::Set::<WarningKind>::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(WarningKind::StartDateTimeIsAfterEndDateTime, start_elem);
60 }
61 }
62
63 Ok(().into_caveat(warnings))
64 }
65}
66
67pub mod time {
68 use std::{borrow::Cow, 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 WarningKind {
88 ContainsEntireDay,
91
92 EndTimeIsNearEndOfDay,
98
99 NeverValid,
101
102 DateTime(datetime::WarningKind),
104 }
105
106 impl fmt::Display for WarningKind {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 WarningKind::ContainsEntireDay => f.write_str("Both `start_time` and `end_time` are defined and contain the entire day."),
110 WarningKind::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 WarningKind::NeverValid => f.write_str("The `start_time` and `end_time` are equal and so the element is never valid."),
117 WarningKind::DateTime(kind) => fmt::Display::fmt(kind, f),
118 }
119 }
120 }
121
122 impl warning::Kind for WarningKind {
123 fn id(&self) -> Cow<'static, str> {
124 match self {
125 WarningKind::ContainsEntireDay => "contains_entire_day".into(),
126 WarningKind::EndTimeIsNearEndOfDay => "end_time_is_near_end_of_day".into(),
127 WarningKind::NeverValid => "never_valid".into(),
128 WarningKind::DateTime(kind) => kind.id(),
129 }
130 }
131 }
132
133 from_warning_all!(datetime::WarningKind => WarningKind::DateTime);
134
135 pub(crate) fn lint(
137 start_time_elem: Option<&json::Element<'_>>,
138 end_time_elem: Option<&json::Element<'_>>,
139 ) -> Verdict<(), WarningKind> {
140 let mut warnings = warning::Set::<WarningKind>::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(WarningKind::EndTimeIsNearEndOfDay, end_elem);
149 }
150
151 if start_time == DAY_BOUNDARY && is_day_end(end_time) {
152 warnings.with_elem(WarningKind::ContainsEntireDay, start_elem);
153 } else if start_time == end_time {
154 warnings.with_elem(WarningKind::NeverValid, start_elem);
155 }
156 } else if let Some((start_time, start_elem)) = start {
157 if start_time == DAY_BOUNDARY {
158 warnings.with_elem(WarningKind::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(WarningKind::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<WarningKind>,
195 ) -> Result<Option<(HourMin, &'a json::Element<'bin>)>, warning::Set<WarningKind>> {
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::{borrow::Cow, 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 WarningKind {
228 Empty,
230
231 InvalidType,
233
234 RequiredField,
236
237 Restrictions(restrictions::WarningKind),
239 }
240
241 impl fmt::Display for WarningKind {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 match self {
244 WarningKind::Empty => write!(
245 f,
246 "An empty list of days means that no day is allowed. Is this what you want?"
247 ),
248 WarningKind::InvalidType => write!(f, "The value should be an array."),
249 WarningKind::RequiredField => write!(f, "The `$.elements` field is required."),
250 WarningKind::Restrictions(kind) => fmt::Display::fmt(kind, f),
251 }
252 }
253 }
254
255 impl warning::Kind for WarningKind {
256 fn id(&self) -> Cow<'static, str> {
257 match self {
258 WarningKind::Empty => "empty".into(),
259 WarningKind::InvalidType => "invalid_type".into(),
260 WarningKind::RequiredField => "required".into(),
261 WarningKind::Restrictions(kind) => kind.id(),
262 }
263 }
264 }
265
266 from_warning_all!(restrictions::WarningKind => WarningKind::Restrictions);
267
268 #[instrument(skip_all)]
272 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), WarningKind> {
273 let mut warnings = warning::Set::<WarningKind>::new();
274
275 let Some(items) = elem.as_array() else {
277 return warnings.bail(WarningKind::InvalidType, elem);
278 };
279
280 if items.is_empty() {
282 return warnings.bail(WarningKind::Empty, elem);
283 }
284
285 for ocpi_element in items {
286 let Some(fields) = ocpi_element.as_object_fields() else {
287 return warnings.bail(WarningKind::InvalidType, ocpi_element);
288 };
289
290 let restrictions = fields.find_field("restrictions");
291
292 if let Some(field) = restrictions {
294 let _drop = 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::{borrow::Cow, fmt};
309
310 use tracing::instrument;
311
312 use crate::{
313 from_warning_all,
314 json::{self, FieldsAsExt as _},
315 warning::{self, GatherWarnings as _, IntoCaveat as _},
316 Verdict,
317 };
318
319 use super::{time, weekday};
320
321 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
322 pub enum WarningKind {
323 Weekday(weekday::WarningKind),
325
326 InvalidType,
328
329 Time(time::WarningKind),
331 }
332
333 impl fmt::Display for WarningKind {
334 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 match self {
336 WarningKind::Weekday(kind) => fmt::Display::fmt(kind, f),
337 WarningKind::InvalidType => write!(f, "The value should be an object."),
338 WarningKind::Time(kind) => fmt::Display::fmt(kind, f),
339 }
340 }
341 }
342
343 impl warning::Kind for WarningKind {
344 fn id(&self) -> Cow<'static, str> {
345 match self {
346 WarningKind::Weekday(kind) => kind.id(),
347 WarningKind::InvalidType => "invalid_type".into(),
348 WarningKind::Time(kind) => kind.id(),
349 }
350 }
351 }
352
353 from_warning_all!(
354 weekday::WarningKind => WarningKind::Weekday,
355 time::WarningKind => WarningKind::Time
356 );
357
358 #[instrument(skip_all)]
360 pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), WarningKind> {
361 let mut warnings = warning::Set::<WarningKind>::new();
362
363 let Some(fields) = elem.as_object_fields() else {
364 return warnings.bail(WarningKind::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 = time::lint(start_time, end_time).gather_warnings_into(&mut warnings);
374 }
375
376 {
377 let day_of_week = fields.get("day_of_week").map(|e| &**e);
378
379 let _drop = weekday::lint(day_of_week).gather_warnings_into(&mut warnings);
380 }
381
382 Ok(().into_caveat(warnings))
383 }
384}
385
386pub mod weekday {
387 use std::{borrow::Cow, collections::BTreeSet, fmt, sync::LazyLock};
395
396 use crate::{
397 from_warning_all,
398 json::{self, FromJson},
399 warning::{self, GatherWarnings as _, IntoCaveat as _},
400 Verdict, Weekday,
401 };
402
403 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
404 pub enum WarningKind {
405 ContainsEntireWeek,
407
408 Weekday(crate::weekday::WarningKind),
410
411 Duplicates,
413
414 Empty,
416
417 InvalidType,
419
420 Unsorted,
422 }
423
424 impl fmt::Display for WarningKind {
425 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426 match self {
427 WarningKind::ContainsEntireWeek => write!(f, "All days of the week are defined. You can simply leave out the `day_of_week` field."),
428 WarningKind::Weekday(kind) => fmt::Display::fmt(kind, f),
429 WarningKind::Duplicates => write!(f, "There's at least one duplicate day."),
430 WarningKind::Empty => write!(
431 f,
432 "An empty list of days means that no day is allowed. Is this what you want?"
433 ),
434 WarningKind::InvalidType => write!(f, "The value should be an array."),
435 WarningKind::Unsorted => write!(f, "The days are unsorted."),
436 }
437 }
438 }
439
440 impl warning::Kind for WarningKind {
441 fn id(&self) -> Cow<'static, str> {
442 match self {
443 WarningKind::ContainsEntireWeek => "contains_entire_week".into(),
444 WarningKind::Weekday(kind) => kind.id(),
445 WarningKind::Duplicates => "duplicates".into(),
446 WarningKind::Empty => "empty".into(),
447 WarningKind::InvalidType => "invalid_type".into(),
448 WarningKind::Unsorted => "unsorted".into(),
449 }
450 }
451 }
452
453 from_warning_all!(crate::weekday::WarningKind => WarningKind::Weekday);
454
455 pub(crate) fn lint(elem: Option<&json::Element<'_>>) -> Verdict<(), WarningKind> {
457 static ALL_DAYS_OF_WEEK: LazyLock<BTreeSet<Weekday>> = LazyLock::new(|| {
459 BTreeSet::from([
460 Weekday::Monday,
461 Weekday::Tuesday,
462 Weekday::Wednesday,
463 Weekday::Thursday,
464 Weekday::Friday,
465 Weekday::Saturday,
466 Weekday::Sunday,
467 ])
468 });
469
470 let mut warnings = warning::Set::<WarningKind>::new();
471
472 let Some(elem) = elem else {
474 return Ok(().into_caveat(warnings));
475 };
476
477 let Some(items) = elem.as_array() else {
479 return warnings.bail(WarningKind::InvalidType, elem);
480 };
481
482 if items.is_empty() {
485 warnings.with_elem(WarningKind::Empty, elem);
486 return Ok(().into_caveat(warnings));
487 }
488
489 let days = items
491 .iter()
492 .map(Weekday::from_json)
493 .collect::<Result<Vec<_>, _>>()?;
494
495 let days = days.gather_warnings_into(&mut warnings);
497
498 if !days.is_sorted() {
500 warnings.with_elem(WarningKind::Unsorted, elem);
501 }
502
503 let day_set: BTreeSet<_> = days.iter().copied().collect();
504
505 if day_set.len() != days.len() {
508 warnings.with_elem(WarningKind::Duplicates, elem);
509 }
510
511 if day_set == *ALL_DAYS_OF_WEEK {
514 warnings.with_elem(WarningKind::ContainsEntireWeek, elem);
515 }
516
517 Ok(().into_caveat(warnings))
518 }
519}