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