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
22#[cfg(test)]
23mod test_from_schema;
24
25use std::fmt;
26
27use chrono::{DateTime, NaiveDateTime, TimeZone as _, Utc};
28
29use crate::{
30    json, schema,
31    warning::{self, GatherWarnings as _},
32    FromSchema, IntoCaveat as _, Verdict,
33};
34
35/// The warnings that can happen when parsing or linting a `NaiveDate`, `NaiveTime`, or `DateTime`.
36#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
37pub enum Warning {
38    /// The datetime does not need to contain escape codes.
39    ContainsEscapeCodes,
40
41    /// The field at the path could not be decoded.
42    Decode(json::decode::Warning),
43
44    /// The datetime is not valid.
45    ///
46    /// Timestamps are formatted as a string with a max length of 25 chars. Each timestamp follows RFC 3339,
47    /// with some additional limitations. All timestamps are expected to be in UTC. The absence of the
48    /// timezone designator implies a UTC timestamp. Fractional seconds may be used.
49    ///
50    /// # Examples
51    ///
52    /// Example of how timestamps should be formatted in OCPI, other formats/patterns are not allowed:
53    ///
54    /// - `"2015-06-29T20:39:09Z"`
55    /// - `"2015-06-29T20:39:09"`
56    /// - `"2016-12-29T17:45:09.2Z"`
57    /// - `"2016-12-29T17:45:09.2"`
58    /// - `"2018-01-01T01:08:01.123Z"`
59    /// - `"2018-01-01T01:08:01.123"`
60    Invalid(String),
61
62    /// The JSON value given is not a string.
63    InvalidType { type_found: json::ValueKind },
64}
65
66impl Warning {
67    fn invalid_type(elem: &json::Element<'_>) -> Self {
68        Self::InvalidType {
69            type_found: elem.value().kind(),
70        }
71    }
72}
73
74impl fmt::Display for Warning {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            Self::ContainsEscapeCodes => {
78                f.write_str("The value contains escape codes but it does not need them.")
79            }
80            Self::Decode(warning) => fmt::Display::fmt(warning, f),
81            Self::Invalid(err) => write!(f, "The value is not valid: {err}"),
82            Self::InvalidType { type_found } => {
83                write!(f, "The value should be a string but found `{type_found}`.")
84            }
85        }
86    }
87}
88
89impl crate::Warning for Warning {
90    fn id(&self) -> warning::Id {
91        match self {
92            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
93            Self::Decode(kind) => kind.id(),
94            Self::Invalid(_) => warning::Id::from_static("invalid"),
95            Self::InvalidType { type_found } => {
96                warning::Id::from_string(format!("invalid_type({type_found})"))
97            }
98        }
99    }
100}
101
102impl From<json::decode::Warning> for Warning {
103    fn from(warn_kind: json::decode::Warning) -> Self {
104        Self::Decode(warn_kind)
105    }
106}
107
108impl json::FromJson<'_> for DateTime<Utc> {
109    type Warning = Warning;
110
111    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
112        let mut warnings = warning::Set::new();
113        let Some(s) = elem.to_raw_str() else {
114            return warnings.bail(elem, Warning::invalid_type(elem));
115        };
116
117        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
118
119        let s = match pending_str {
120            json::PendingStr::NoEscapes(s) => s,
121            json::PendingStr::HasEscapes(_) => {
122                return warnings.bail(elem, Warning::ContainsEscapeCodes);
123            }
124        };
125
126        // First try parsing with a timezone, if that doesn't work try to parse without
127        let err = match s.parse::<DateTime<Utc>>() {
128            Ok(date) => return Ok(date.into_caveat(warnings)),
129            Err(err) => err,
130        };
131
132        let Ok(date) = s.parse::<NaiveDateTime>() else {
133            return warnings.bail(elem, Warning::Invalid(err.to_string()));
134        };
135
136        let datetime = Utc.from_utc_datetime(&date);
137        Ok(datetime.into_caveat(warnings))
138    }
139}
140
141impl<'buf> FromSchema<'buf, schema::Str<'buf>> for DateTime<Utc> {
142    type Warning = Warning;
143
144    fn from_schema(source: &schema::Str<'buf>) -> Verdict<Self, Self::Warning> {
145        let mut warnings = warning::Set::new();
146        let elem = source.element();
147
148        let pending_str = source
149            .value()
150            .has_escapes(elem)
151            .gather_warnings_into(&mut warnings);
152
153        let s = match pending_str {
154            json::PendingStr::NoEscapes(s) => s,
155            json::PendingStr::HasEscapes(_) => {
156                return warnings.bail(elem, Warning::ContainsEscapeCodes);
157            }
158        };
159
160        // First try parsing with a timezone, if that doesn't work try to parse without
161        let err = match s.parse::<DateTime<Utc>>() {
162            Ok(date) => return Ok(date.into_caveat(warnings)),
163            Err(err) => err,
164        };
165
166        let Ok(date) = s.parse::<NaiveDateTime>() else {
167            return warnings.bail(elem, Warning::Invalid(err.to_string()));
168        };
169
170        let datetime = Utc.from_utc_datetime(&date);
171        Ok(datetime.into_caveat(warnings))
172    }
173}
174
175impl json::FromJson<'_> for chrono::NaiveDate {
176    type Warning = Warning;
177
178    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
179        let mut warnings = warning::Set::new();
180        let Some(s) = elem.to_raw_str() else {
181            return warnings.bail(elem, Warning::invalid_type(elem));
182        };
183
184        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
185
186        let s = match pending_str {
187            json::PendingStr::NoEscapes(s) => s,
188            json::PendingStr::HasEscapes(_) => {
189                return warnings.bail(elem, Warning::ContainsEscapeCodes);
190            }
191        };
192
193        let date = match s.parse::<chrono::NaiveDate>() {
194            Ok(v) => v,
195            Err(err) => {
196                return warnings.bail(elem, Warning::Invalid(err.to_string()));
197            }
198        };
199
200        Ok(date.into_caveat(warnings))
201    }
202}
203
204impl<'buf> FromSchema<'buf, schema::Str<'buf>> for chrono::NaiveDate {
205    type Warning = Warning;
206
207    fn from_schema(source: &schema::Str<'buf>) -> Verdict<Self, Self::Warning> {
208        let mut warnings = warning::Set::new();
209        let elem = source.element();
210
211        // The schema confirmed the value is a string, so there is no kind check; its
212        // content is read directly.
213        let pending_str = source
214            .value()
215            .has_escapes(elem)
216            .gather_warnings_into(&mut warnings);
217
218        let s = match pending_str {
219            json::PendingStr::NoEscapes(s) => s,
220            json::PendingStr::HasEscapes(_) => {
221                return warnings.bail(elem, Warning::ContainsEscapeCodes);
222            }
223        };
224
225        let date = match s.parse::<chrono::NaiveDate>() {
226            Ok(v) => v,
227            Err(err) => {
228                return warnings.bail(elem, Warning::Invalid(err.to_string()));
229            }
230        };
231
232        Ok(date.into_caveat(warnings))
233    }
234}
235
236impl json::FromJson<'_> for chrono::NaiveTime {
237    type Warning = Warning;
238
239    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
240        let mut warnings = warning::Set::new();
241        let value = elem.as_value();
242
243        let Some(s) = value.to_raw_str() else {
244            return warnings.bail(elem, Warning::invalid_type(elem));
245        };
246
247        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
248
249        let s = match pending_str {
250            json::PendingStr::NoEscapes(s) => s,
251            json::PendingStr::HasEscapes(_) => {
252                return warnings.bail(elem, Warning::ContainsEscapeCodes);
253            }
254        };
255
256        let date = match chrono::NaiveTime::parse_from_str(s, "%H:%M") {
257            Ok(v) => v,
258            Err(err) => {
259                return warnings.bail(elem, Warning::Invalid(err.to_string()));
260            }
261        };
262
263        Ok(date.into_caveat(warnings))
264    }
265}
266
267impl<'buf> FromSchema<'buf, schema::Str<'buf>> for chrono::NaiveTime {
268    type Warning = Warning;
269
270    fn from_schema(source: &schema::Str<'buf>) -> Verdict<Self, Self::Warning> {
271        let mut warnings = warning::Set::new();
272        let elem = source.element();
273
274        // The schema confirmed the value is a string, so there is no kind check; its
275        // content is read directly.
276        let pending_str = source
277            .value()
278            .has_escapes(elem)
279            .gather_warnings_into(&mut warnings);
280
281        let s = match pending_str {
282            json::PendingStr::NoEscapes(s) => s,
283            json::PendingStr::HasEscapes(_) => {
284                return warnings.bail(elem, Warning::ContainsEscapeCodes);
285            }
286        };
287
288        let date = match chrono::NaiveTime::parse_from_str(s, "%H:%M") {
289            Ok(v) => v,
290            Err(err) => {
291                return warnings.bail(elem, Warning::Invalid(err.to_string()));
292            }
293        };
294
295        Ok(date.into_caveat(warnings))
296    }
297}