raystack/
hs_types.rs

1//! # Haystack Types
2//! This module defines Haystack types which are not taken from the
3//! raystack_core dependency.
4
5use chrono::{NaiveDate, NaiveTime};
6use chrono_tz::Tz;
7use raystack_core::{FromHaysonError, Hayson};
8use serde_json::{json, Value};
9use std::convert::From;
10
11const KIND: &str = "_kind";
12
13/// A Haystack Date with no time zone.
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct Date(NaiveDate);
16
17impl Date {
18    pub fn new(naive_date: NaiveDate) -> Self {
19        Date(naive_date)
20    }
21
22    pub fn naive_date(&self) -> &NaiveDate {
23        &self.0
24    }
25
26    pub fn into_naive_date(self) -> NaiveDate {
27        self.0
28    }
29}
30
31impl Hayson for Date {
32    fn from_hayson(value: &Value) -> Result<Self, FromHaysonError> {
33        match &value {
34            Value::Object(obj) => {
35                if let Some(kind_err) = hayson_check_kind("date", value) {
36                    return Err(kind_err);
37                }
38                let val = obj.get("val");
39
40                if val.is_none() {
41                    return hayson_error("Date val is missing");
42                }
43
44                let val = val.unwrap().as_str();
45
46                if val.is_none() {
47                    return hayson_error("Date val is not a string");
48                }
49
50                let val = val.unwrap();
51
52                match val.parse() {
53                    Ok(naive_date) => Ok(Date::new(naive_date)),
54                    Err(_) => hayson_error(
55                        "Date val string could not be parsed as a NaiveDate",
56                    ),
57                }
58            }
59            _ => hayson_error("Date JSON value must be an object"),
60        }
61    }
62
63    fn to_hayson(&self) -> Value {
64        json!({
65            KIND: "date",
66            "val": self.naive_date().to_string(),
67        })
68    }
69}
70
71impl From<NaiveDate> for Date {
72    fn from(d: NaiveDate) -> Self {
73        Self::new(d)
74    }
75}
76
77/// A Haystack Time with no time zone.
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct Time(NaiveTime);
80
81impl Time {
82    pub fn new(naive_time: NaiveTime) -> Self {
83        Time(naive_time)
84    }
85
86    pub fn naive_time(&self) -> &NaiveTime {
87        &self.0
88    }
89
90    pub fn into_naive_time(self) -> NaiveTime {
91        self.0
92    }
93}
94
95impl Hayson for Time {
96    fn from_hayson(value: &Value) -> Result<Self, FromHaysonError> {
97        match &value {
98            Value::Object(obj) => {
99                if let Some(kind_err) = hayson_check_kind("time", value) {
100                    return Err(kind_err);
101                }
102                let val = obj.get("val");
103
104                if val.is_none() {
105                    return hayson_error("Time val is missing");
106                }
107
108                let val = val.unwrap().as_str();
109
110                if val.is_none() {
111                    return hayson_error("Time val is not a string");
112                }
113
114                let val = val.unwrap();
115
116                match val.parse() {
117                    Ok(naive_time) => Ok(Time::new(naive_time)),
118                    Err(_) => hayson_error(
119                        "Time val string could not be parsed as a NaiveTime",
120                    ),
121                }
122            }
123            _ => hayson_error("Time JSON value must be an object"),
124        }
125    }
126
127    fn to_hayson(&self) -> Value {
128        json!({
129            KIND: "time",
130            "val": self.naive_time().to_string(),
131        })
132    }
133}
134
135impl From<NaiveTime> for Time {
136    fn from(t: NaiveTime) -> Self {
137        Self::new(t)
138    }
139}
140
141/// A Haystack DateTime.
142#[derive(Clone, Debug, Eq, PartialEq)]
143pub struct DateTime {
144    date_time: chrono::DateTime<Tz>,
145}
146
147impl DateTime {
148    pub fn new(date_time: chrono::DateTime<Tz>) -> Self {
149        Self { date_time }
150    }
151
152    pub fn date_time(&self) -> &chrono::DateTime<Tz> {
153        &self.date_time
154    }
155
156    pub fn into_date_time(self) -> chrono::DateTime<Tz> {
157        self.date_time
158    }
159    /// Return the IANA TZDB identifier, for example  "America/New_York".
160    pub fn time_zone(&self) -> &str {
161        self.date_time.timezone().name()
162    }
163
164    /// Return the short name of the time zone. These
165    /// names should match with the shortened time zone names used in
166    /// SkySpark.
167    pub fn short_time_zone(&self) -> &str {
168        crate::tz::time_zone_name_to_short_name(self.time_zone())
169    }
170}
171
172impl Hayson for DateTime {
173    fn from_hayson(value: &Value) -> Result<Self, FromHaysonError> {
174        // Use this time zone when the time zone is missing in the Hayson
175        // encoding:
176        let default_tz = "GMT";
177
178        match &value {
179            Value::Object(obj) => {
180                if let Some(kind_err) = hayson_check_kind("dateTime", value) {
181                    return Err(kind_err);
182                }
183
184                let tz_value = obj.get("tz");
185                let mut tz_str = default_tz.to_owned();
186
187                if let Some(value) = tz_value {
188                    match value {
189                        Value::Null => {
190                            tz_str = default_tz.to_owned();
191                        }
192                        Value::String(tz_string) => {
193                            tz_str = tz_string.clone();
194                        }
195                        _ => {
196                            return hayson_error(
197                                "DateTime tz is not a null or a string",
198                            )
199                        }
200                    }
201                }
202
203                let dt = obj.get("val");
204
205                if dt.is_none() {
206                    return hayson_error("DateTime val is missing");
207                }
208
209                let dt = dt.unwrap().as_str();
210
211                if dt.is_none() {
212                    return hayson_error("DateTime val is not a string");
213                }
214
215                let dt = dt.unwrap();
216
217                match chrono::DateTime::parse_from_rfc3339(dt) {
218                    Ok(dt) => {
219                        let tz = crate::skyspark_tz_string_to_tz(&tz_str);
220                        if let Some(tz) = tz {
221                            let dt = dt.with_timezone(&tz);
222                            Ok(DateTime::new(dt))
223                        } else {
224                            hayson_error(format!("DateTime tz '{}' has no matching chrono_tz time zone", tz_str))
225                        }
226                    }
227                    Err(_) => hayson_error(
228                        "Time val string could not be parsed as a NaiveTime",
229                    ),
230                }
231            }
232            _ => hayson_error("Time JSON value must be an object"),
233        }
234    }
235
236    fn to_hayson(&self) -> Value {
237        json!({
238            KIND: "dateTime",
239            "val": self.date_time().to_rfc3339(),
240            "tz": self.short_time_zone(),
241        })
242    }
243}
244
245impl From<chrono::DateTime<Tz>> for DateTime {
246    fn from(dt: chrono::DateTime<Tz>) -> Self {
247        Self::new(dt)
248    }
249}
250
251fn hayson_error<T, M>(message: M) -> Result<T, FromHaysonError>
252where
253    M: AsRef<str>,
254{
255    Err(FromHaysonError::new(message.as_ref().to_owned()))
256}
257
258fn hayson_error_opt<M>(message: M) -> Option<FromHaysonError>
259where
260    M: AsRef<str>,
261{
262    Some(FromHaysonError::new(message.as_ref().to_owned()))
263}
264
265fn hayson_check_kind(
266    target_kind: &str,
267    value: &Value,
268) -> Option<FromHaysonError> {
269    match value.get(KIND) {
270        Some(kind) => match kind {
271            Value::String(kind) => {
272                if kind == target_kind {
273                    None
274                } else {
275                    hayson_error_opt(format!(
276                        "Expected '{}' = {} but found {}",
277                        KIND, kind, kind
278                    ))
279                }
280            }
281            _ => hayson_error_opt(format!("'{}' key is not a string", KIND)),
282        },
283        None => hayson_error_opt(format!("Missing '{}' key", KIND)),
284    }
285}
286
287#[cfg(test)]
288mod test {
289    use crate::{Date, DateTime, Time};
290    use chrono::{NaiveDate, NaiveTime};
291    use chrono_tz::Tz;
292    use raystack_core::Hayson;
293
294    #[test]
295    fn serde_date_works() {
296        let naive_date = NaiveDate::from_ymd(2021, 1, 1);
297        let x = Date::new(naive_date);
298        let value = x.to_hayson();
299        let deserialized = Date::from_hayson(&value).unwrap();
300        assert_eq!(x, deserialized);
301    }
302
303    #[test]
304    fn serde_time_works() {
305        let naive_time = NaiveTime::from_hms(2, 15, 59);
306        let x = Time::new(naive_time);
307        let value = x.to_hayson();
308        let deserialized = Time::from_hayson(&value).unwrap();
309        assert_eq!(x, deserialized);
310    }
311
312    #[test]
313    fn serde_date_time_works() {
314        let dt =
315            chrono::DateTime::parse_from_rfc3339("2021-01-01T18:30:09.453Z")
316                .unwrap()
317                .with_timezone(&Tz::GMT);
318        let x = DateTime::new(dt);
319        let value = x.to_hayson();
320        let deserialized = DateTime::from_hayson(&value).unwrap();
321        assert_eq!(x, deserialized);
322    }
323
324    #[test]
325    fn serde_date_time_with_one_slash_tz_works() {
326        let dt =
327            chrono::DateTime::parse_from_rfc3339("2021-01-01T18:30:09.453Z")
328                .unwrap()
329                .with_timezone(&Tz::Australia__Sydney);
330        let x = DateTime::new(dt);
331        let value = x.to_hayson();
332        let deserialized = DateTime::from_hayson(&value).unwrap();
333        assert_eq!(x, deserialized);
334    }
335
336    #[test]
337    fn serde_date_time_with_multiple_slashes_tz_works() {
338        let dt =
339            chrono::DateTime::parse_from_rfc3339("2021-01-01T18:30:09.453Z")
340                .unwrap()
341                .with_timezone(&Tz::America__North_Dakota__Beulah);
342        let x = DateTime::new(dt);
343        let value = x.to_hayson();
344        let deserialized = DateTime::from_hayson(&value).unwrap();
345        assert_eq!(x, deserialized);
346    }
347
348    #[test]
349    fn short_time_zone_works() {
350        let dt: DateTime =
351            chrono::DateTime::parse_from_rfc3339("2021-01-01T18:30:09.453Z")
352                .unwrap()
353                .with_timezone(&Tz::America__North_Dakota__Beulah)
354                .into();
355        assert_eq!(dt.short_time_zone(), "Beulah");
356
357        let dt: DateTime =
358            chrono::DateTime::parse_from_rfc3339("2021-01-01T18:30:09.453Z")
359                .unwrap()
360                .with_timezone(&Tz::GMT)
361                .into();
362        assert_eq!(dt.short_time_zone(), "GMT");
363
364        let dt: DateTime =
365            chrono::DateTime::parse_from_rfc3339("2021-01-01T18:30:09.453Z")
366                .unwrap()
367                .with_timezone(&Tz::Australia__Sydney)
368                .into();
369        assert_eq!(dt.short_time_zone(), "Sydney");
370    }
371}