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