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