Skip to main content

ocpi_tariffs/
datetime.rs

1//! Timestamps are formatted as a string with a max length of 25 chars. Each timestamp follows RFC 3339,
2//! with some additional limitations. All timestamps are expected to be in UTC. The absence of the
3//! timezone designator implies a UTC timestamp. Fractional seconds may be used.
4//!
5//! # Examples
6//!
7//! Example of how timestamps should be formatted in OCPI, other formats/patterns are not allowed:
8//!
9//! - `"2015-06-29T20:39:09Z"`
10//! - `"2015-06-29T20:39:09"`
11//! - `"2016-12-29T17:45:09.2Z"`
12//! - `"2016-12-29T17:45:09.2"`
13//! - `"2018-01-01T01:08:01.123Z"`
14//! - `"2018-01-01T01:08:01.123"`
15
16#[cfg(test)]
17pub(crate) mod test;
18
19#[cfg(test)]
20mod test_datetime_from_json;
21
22use std::fmt;
23
24use chrono::{DateTime, NaiveDateTime, TimeZone as _, Utc};
25
26use crate::{
27    into_caveat, into_caveat_all, json,
28    warning::{self, GatherWarnings as _},
29    IntoCaveat, Verdict,
30};
31
32/// The warnings that can happen when parsing or linting a `NaiveDate`, `NaiveTime`, or `DateTime`.
33#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
34pub enum Warning {
35    /// The datetime does not need to contain escape codes.
36    ContainsEscapeCodes,
37
38    /// The field at the path could not be decoded.
39    Decode(json::decode::Warning),
40
41    /// The datetime is not valid.
42    ///
43    /// Timestamps are formatted as a string with a max length of 25 chars. Each timestamp follows RFC 3339,
44    /// with some additional limitations. All timestamps are expected to be in UTC. The absence of the
45    /// timezone designator implies a UTC timestamp. Fractional seconds may be used.
46    ///
47    /// # Examples
48    ///
49    /// Example of how timestamps should be formatted in OCPI, other formats/patterns are not allowed:
50    ///
51    /// - `"2015-06-29T20:39:09Z"`
52    /// - `"2015-06-29T20:39:09"`
53    /// - `"2016-12-29T17:45:09.2Z"`
54    /// - `"2016-12-29T17:45:09.2"`
55    /// - `"2018-01-01T01:08:01.123Z"`
56    /// - `"2018-01-01T01:08:01.123"`
57    Invalid(String),
58
59    /// The JSON value given is not a string.
60    InvalidType { type_found: json::ValueKind },
61}
62
63impl Warning {
64    fn invalid_type(elem: &json::Element<'_>) -> Self {
65        Self::InvalidType {
66            type_found: elem.value().kind(),
67        }
68    }
69}
70
71impl fmt::Display for Warning {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self {
74            Self::ContainsEscapeCodes => {
75                f.write_str("The value contains escape codes but it does not need them.")
76            }
77            Self::Decode(warning) => fmt::Display::fmt(warning, f),
78            Self::Invalid(err) => write!(f, "The value is not valid: {err}"),
79            Self::InvalidType { type_found } => {
80                write!(f, "The value should be a string but found `{type_found}`.")
81            }
82        }
83    }
84}
85
86impl crate::Warning for Warning {
87    fn id(&self) -> warning::Id {
88        match self {
89            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
90            Self::Decode(kind) => kind.id(),
91            Self::Invalid(_) => warning::Id::from_static("invalid"),
92            Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
93        }
94    }
95}
96
97impl From<json::decode::Warning> for Warning {
98    fn from(warn_kind: json::decode::Warning) -> Self {
99        Self::Decode(warn_kind)
100    }
101}
102
103into_caveat!(DateTime<Utc>);
104into_caveat_all!(chrono::NaiveTime, chrono::NaiveDate);
105
106impl json::FromJson<'_, '_> for DateTime<Utc> {
107    type Warning = Warning;
108
109    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
110        let mut warnings = warning::Set::new();
111        let Some(s) = elem.as_raw_str() else {
112            return warnings.bail(Warning::invalid_type(elem), elem);
113        };
114
115        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
116
117        let s = match pending_str {
118            json::decode::PendingStr::NoEscapes(s) => s,
119            json::decode::PendingStr::HasEscapes(_) => {
120                return warnings.bail(Warning::ContainsEscapeCodes, elem);
121            }
122        };
123
124        // First try parsing with a timezone, if that doesn't work try to parse without
125        let err = match s.parse::<DateTime<Utc>>() {
126            Ok(date) => return Ok(date.into_caveat(warnings)),
127            Err(err) => err,
128        };
129
130        let Ok(date) = s.parse::<NaiveDateTime>() else {
131            return warnings.bail(Warning::Invalid(err.to_string()), elem);
132        };
133
134        let datetime = Utc.from_utc_datetime(&date);
135        Ok(datetime.into_caveat(warnings))
136    }
137}
138
139impl json::FromJson<'_, '_> for chrono::NaiveDate {
140    type Warning = Warning;
141
142    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
143        let mut warnings = warning::Set::new();
144        let Some(s) = elem.as_raw_str() else {
145            return warnings.bail(Warning::invalid_type(elem), elem);
146        };
147
148        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
149
150        let s = match pending_str {
151            json::decode::PendingStr::NoEscapes(s) => s,
152            json::decode::PendingStr::HasEscapes(_) => {
153                return warnings.bail(Warning::ContainsEscapeCodes, elem);
154            }
155        };
156
157        let date = match s.parse::<chrono::NaiveDate>() {
158            Ok(v) => v,
159            Err(err) => {
160                return warnings.bail(Warning::Invalid(err.to_string()), elem);
161            }
162        };
163
164        Ok(date.into_caveat(warnings))
165    }
166}
167
168impl json::FromJson<'_, '_> for chrono::NaiveTime {
169    type Warning = Warning;
170
171    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
172        let mut warnings = warning::Set::new();
173        let value = elem.as_value();
174
175        let Some(s) = value.as_raw_str() else {
176            return warnings.bail(Warning::invalid_type(elem), elem);
177        };
178
179        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
180
181        let s = match pending_str {
182            json::decode::PendingStr::NoEscapes(s) => s,
183            json::decode::PendingStr::HasEscapes(_) => {
184                return warnings.bail(Warning::ContainsEscapeCodes, elem);
185            }
186        };
187
188        let date = match chrono::NaiveTime::parse_from_str(s, "%H:%M") {
189            Ok(v) => v,
190            Err(err) => {
191                return warnings.bail(Warning::Invalid(err.to_string()), elem);
192            }
193        };
194
195        Ok(date.into_caveat(warnings))
196    }
197}