ocpi_tariffs/
weekday.rs

1//! The Warning infrastructure for the OCPI `DayOfWeek` type.
2//!
3//! We rename the type of `Weekday` as this is the common way of expressing the concept in English.
4//!
5//! * See: [OCPI spec 2.2.1: Tariff DayOfWeek](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#mod_tariffs_dayofweek_enum>)
6//! * See: [OCPI spec 2.1.1: Tariff DayOfWeek](<https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#41-dayofweek-enum>)
7
8use std::{borrow::Cow, fmt};
9
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    into_caveat, json,
14    warning::{self, GatherWarnings as _, IntoCaveat as _},
15    OutOfRange, Verdict,
16};
17
18#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
19pub enum WarningKind {
20    /// Neither the day of the week does not need escape codes.
21    ContainsEscapeCodes,
22
23    /// The field at the path could not be decoded.
24    Decode(json::decode::WarningKind),
25
26    /// Each day of the week should be uppercase.
27    InvalidCase,
28
29    /// The value is not a valid day.
30    InvalidDay,
31
32    /// The JSON value given is not a string.
33    InvalidType,
34}
35
36impl fmt::Display for WarningKind {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            WarningKind::ContainsEscapeCodes => write!(
40                f,
41                "The value contains escape codes but it does not need them."
42            ),
43            WarningKind::Decode(warning) => fmt::Display::fmt(warning, f),
44            WarningKind::InvalidCase => write!(f, "The day should be uppercase."),
45            WarningKind::InvalidDay => {
46                write!(f, "The value is not a valid day.")
47            }
48            WarningKind::InvalidType => write!(f, "The value should be a string."),
49        }
50    }
51}
52
53impl warning::Kind for WarningKind {
54    fn id(&self) -> Cow<'static, str> {
55        match self {
56            WarningKind::ContainsEscapeCodes => "contains_escape_codes".into(),
57            WarningKind::Decode(kind) => format!("decode.{}", kind.id()).into(),
58            WarningKind::InvalidCase => "invalid_case".into(),
59            WarningKind::InvalidDay => "invalid_day".into(),
60            WarningKind::InvalidType => "invalid_type".into(),
61        }
62    }
63}
64
65impl From<json::decode::WarningKind> for WarningKind {
66    fn from(warn_kind: json::decode::WarningKind) -> Self {
67        Self::Decode(warn_kind)
68    }
69}
70
71/// A single day of the week.
72#[derive(Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Deserialize, Serialize)]
73#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
74pub(crate) enum Weekday {
75    /// Monday
76    Monday,
77    /// Tuesday
78    Tuesday,
79    /// Wednesday
80    Wednesday,
81    /// Thursday
82    Thursday,
83    /// Friday
84    Friday,
85    /// Saturday
86    Saturday,
87    /// Sunday
88    Sunday,
89}
90
91into_caveat!(Weekday);
92
93/// Convert a `chrono::Weekday` into an OCPI `Weekday`.
94impl From<chrono::Weekday> for Weekday {
95    fn from(value: chrono::Weekday) -> Self {
96        match value {
97            chrono::Weekday::Mon => Weekday::Monday,
98            chrono::Weekday::Tue => Weekday::Tuesday,
99            chrono::Weekday::Wed => Weekday::Wednesday,
100            chrono::Weekday::Thu => Weekday::Thursday,
101            chrono::Weekday::Fri => Weekday::Friday,
102            chrono::Weekday::Sat => Weekday::Saturday,
103            chrono::Weekday::Sun => Weekday::Sunday,
104        }
105    }
106}
107
108/// Convert a `Weekday` into a index.
109impl From<Weekday> for usize {
110    fn from(value: Weekday) -> Self {
111        match value {
112            Weekday::Monday => 0,
113            Weekday::Tuesday => 1,
114            Weekday::Wednesday => 2,
115            Weekday::Thursday => 3,
116            Weekday::Friday => 4,
117            Weekday::Saturday => 5,
118            Weekday::Sunday => 6,
119        }
120    }
121}
122
123/// Convert an index into a `Weekday`.
124impl TryFrom<usize> for Weekday {
125    type Error = OutOfRange;
126
127    fn try_from(value: usize) -> Result<Self, Self::Error> {
128        let day = match value {
129            0 => Weekday::Monday,
130            1 => Weekday::Tuesday,
131            2 => Weekday::Wednesday,
132            3 => Weekday::Thursday,
133            4 => Weekday::Friday,
134            5 => Weekday::Saturday,
135            6 => Weekday::Sunday,
136            _ => return Err(OutOfRange::new()),
137        };
138
139        Ok(day)
140    }
141}
142
143impl json::FromJson<'_, '_> for Weekday {
144    type WarningKind = WarningKind;
145
146    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
147        const NUM_DAYS: usize = 7;
148        const DAYS: [&str; NUM_DAYS] = [
149            "MONDAY",
150            "TUESDAY",
151            "WEDNESDAY",
152            "THURSDAY",
153            "FRIDAY",
154            "SATURDAY",
155            "SUNDAY",
156        ];
157
158        let mut warnings = warning::Set::new();
159        let value = elem.as_value();
160
161        let Some(s) = value.as_raw_str() else {
162            warnings.with_elem(WarningKind::InvalidType, elem);
163            return Err(warnings);
164        };
165
166        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
167
168        let s = match pending_str {
169            json::decode::PendingStr::NoEscapes(s) => s,
170            json::decode::PendingStr::HasEscapes(_) => {
171                warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
172                return Err(warnings);
173            }
174        };
175
176        if !s.chars().all(char::is_uppercase) {
177            warnings.with_elem(WarningKind::InvalidCase, elem);
178        }
179
180        let Some(index) = DAYS.iter().position(|day| day.eq_ignore_ascii_case(s)) else {
181            warnings.with_elem(WarningKind::InvalidDay, elem);
182            return Err(warnings);
183        };
184
185        let Ok(day) = Weekday::try_from(index) else {
186            warnings.with_elem(WarningKind::InvalidDay, elem);
187            return Err(warnings);
188        };
189
190        Ok(day.into_caveat(warnings))
191    }
192}
193
194impl From<Weekday> for chrono::Weekday {
195    fn from(day: Weekday) -> Self {
196        match day {
197            Weekday::Monday => Self::Mon,
198            Weekday::Tuesday => Self::Tue,
199            Weekday::Wednesday => Self::Wed,
200            Weekday::Thursday => Self::Thu,
201            Weekday::Friday => Self::Fri,
202            Weekday::Saturday => Self::Sat,
203            Weekday::Sunday => Self::Sun,
204        }
205    }
206}
207
208#[cfg(test)]
209mod test_day_of_week {
210    use assert_matches::assert_matches;
211
212    use crate::{
213        json::{self, FromJson as _},
214        test,
215    };
216
217    use super::{WarningKind, Weekday};
218
219    #[test]
220    fn should_create_from_json() {
221        const JSON: &str = r#""MONDAY""#;
222
223        test::setup();
224
225        let elem = json::parse(JSON).unwrap();
226        let day = Weekday::from_json(&elem).unwrap().unwrap();
227        assert_matches!(day, Weekday::Monday);
228    }
229
230    #[test]
231    fn should_fail_on_type_from_json() {
232        const JSON: &str = "[]";
233
234        test::setup();
235
236        let elem = json::parse(JSON).unwrap();
237        let warnings = Weekday::from_json(&elem).unwrap_err().into_kind_vec();
238        assert_matches!(*warnings, [WarningKind::InvalidType]);
239    }
240
241    #[test]
242    fn should_fail_on_value_from_json() {
243        const JSON: &str = r#""MOONDAY""#;
244
245        test::setup();
246
247        let elem = json::parse(JSON).unwrap();
248        let warnings = Weekday::from_json(&elem).unwrap_err().into_kind_vec();
249        assert_matches!(*warnings, [WarningKind::InvalidDay]);
250    }
251
252    #[test]
253    fn should_warn_about_case_from_json() {
254        const JSON: &str = r#""sunday""#;
255
256        test::setup();
257
258        let elem = json::parse(JSON).unwrap();
259        let (day, warnings) = Weekday::from_json(&elem).unwrap().into_parts();
260        let warnings = warnings.into_kind_vec();
261
262        assert_matches!(day, Weekday::Sunday);
263        assert_matches!(*warnings, [WarningKind::InvalidCase]);
264    }
265}