Skip to main content

nv_redfish_core/
edm_date_time_offset.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! `Edm.DateTimeOffset` primitive wrapper
17//!
18//! Represents Redfish/OData `Edm.DateTimeOffset` values. Internally wraps
19//! `time::OffsetDateTime` and (de)serializes using RFC 3339. Display always
20//! uses canonical RFC 3339 formatting; `+00:00` is rendered as `Z` while
21//! non‑UTC offsets are preserved.
22//!
23//! References:
24//! - OASIS OData 4.01 CSDL, Primitive Types: Edm.DateTimeOffset — `https://docs.oasis-open.org/odata/`
25//! - DMTF Redfish Specification DSP0266 — `https://www.dmtf.org/standards/redfish`
26//! - RFC 3339: Date and Time on the Internet — `https://datatracker.ietf.org/doc/html/rfc3339`
27//!
28//! Examples
29//! ```rust
30//! use nv_redfish_core::EdmDateTimeOffset;
31//! use std::str::FromStr;
32//!
33//! let z = EdmDateTimeOffset::from_str("2021-03-04T05:06:07Z").unwrap();
34//! assert_eq!(z.to_string(), "2021-03-04T05:06:07Z".to_string());
35//!
36//! let plus = EdmDateTimeOffset::from_str("2021-03-04T10:36:07+05:30").unwrap();
37//! assert_eq!(plus.to_string(), "2021-03-04T10:36:07+05:30");
38//! ```
39//!
40//! ```rust
41//! use nv_redfish_core::EdmDateTimeOffset;
42//!
43//! // Serde JSON uses RFC3339 strings; +00:00 canonicalizes to Z
44//! let v: EdmDateTimeOffset = "2021-03-04T05:06:07+00:00".parse().unwrap();
45//! let s = serde_json::to_string(&v).unwrap();
46//! assert_eq!(s, r#""2021-03-04T05:06:07Z""#);
47//! ```
48//!
49
50use core::str::FromStr;
51use serde::{Deserialize, Serialize};
52use std::fmt::Display;
53use std::fmt::Error as FmtError;
54use std::fmt::Formatter;
55use std::fmt::Result as FmtResult;
56use std::time::Duration;
57use std::time::SystemTime;
58use time::format_description::well_known::Rfc3339;
59use time::OffsetDateTime;
60
61/// Type corresponding to `Edm.DateTimeOffset`.
62#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
63#[serde(transparent)]
64pub struct EdmDateTimeOffset(#[serde(with = "time::serde::rfc3339")] OffsetDateTime);
65
66impl From<OffsetDateTime> for EdmDateTimeOffset {
67    fn from(dt: OffsetDateTime) -> Self {
68        Self(dt)
69    }
70}
71
72impl From<EdmDateTimeOffset> for OffsetDateTime {
73    fn from(w: EdmDateTimeOffset) -> Self {
74        w.0
75    }
76}
77
78impl From<EdmDateTimeOffset> for SystemTime {
79    fn from(w: EdmDateTimeOffset) -> Self {
80        let unix_timestamp = w.0.unix_timestamp();
81        let nanos = w.0.nanosecond();
82
83        let duration = Duration::new(unix_timestamp.unsigned_abs(), nanos);
84        if unix_timestamp >= 0 {
85            Self::UNIX_EPOCH + duration
86        } else {
87            Self::UNIX_EPOCH - duration
88        }
89    }
90}
91
92impl Display for EdmDateTimeOffset {
93    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
94        let s = self.0.format(&Rfc3339).map_err(|_| FmtError)?;
95        f.write_str(&s)
96    }
97}
98
99#[allow(clippy::absolute_paths)]
100impl FromStr for EdmDateTimeOffset {
101    type Err = time::error::Parse;
102
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        let dt = OffsetDateTime::parse(s, &Rfc3339)?;
105        Ok(Self(dt))
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use time::UtcOffset;
113
114    #[test]
115    fn parses_and_displays_utc_z() {
116        let s = "2021-03-04T05:06:07Z";
117        let w: EdmDateTimeOffset = s.parse().unwrap();
118        assert_eq!(w.to_string(), s);
119
120        let dt: OffsetDateTime = w.into();
121        assert_eq!(dt.offset(), UtcOffset::UTC);
122    }
123
124    #[test]
125    fn parses_utc_plus00_canonicalizes_to_z_on_display() {
126        let s = "2021-03-04T05:06:07+00:00";
127        let w: EdmDateTimeOffset = s.parse().unwrap();
128        let displayed = w.to_string();
129        assert!(displayed.ends_with('Z'));
130    }
131
132    #[test]
133    fn parses_and_displays_positive_offset() {
134        let s = "2021-03-04T10:36:07+05:30"; // same instant as 05:06:07Z
135        let w: EdmDateTimeOffset = s.parse().unwrap();
136        assert_eq!(w.to_string(), s);
137
138        let dt: OffsetDateTime = w.into();
139        assert_eq!(dt.offset(), UtcOffset::from_hms(5, 30, 0).unwrap());
140    }
141
142    #[test]
143    fn parses_and_displays_fractional_seconds() {
144        let s = "2021-03-04T05:06:07.123456789Z";
145        let w: EdmDateTimeOffset = s.parse().unwrap();
146        assert_eq!(w.to_string(), s);
147    }
148
149    #[test]
150    fn rejects_invalid_inputs() {
151        assert!("not-a-date".parse::<EdmDateTimeOffset>().is_err());
152        // RFC3339 requires an explicit offset
153        assert!("2021-03-04T05:06:07".parse::<EdmDateTimeOffset>().is_err());
154    }
155
156    #[test]
157    fn serde_serializes_conformant_strings() {
158        // UTC Z
159        let w_z: EdmDateTimeOffset = "2021-03-04T05:06:07Z".parse().unwrap();
160        let json_z = serde_json::to_string(&w_z).unwrap();
161        assert_eq!(json_z, r#""2021-03-04T05:06:07Z""#);
162
163        // Non-UTC offset preserved
164        let w_pos: EdmDateTimeOffset = "2021-03-04T10:36:07+05:30".parse().unwrap();
165        let json_pos = serde_json::to_string(&w_pos).unwrap();
166        assert_eq!(json_pos, r#""2021-03-04T10:36:07+05:30""#);
167
168        // Fractional seconds retained
169        let w_frac: EdmDateTimeOffset = "2021-03-04T05:06:07.123456789Z".parse().unwrap();
170        let json_frac = serde_json::to_string(&w_frac).unwrap();
171        assert_eq!(json_frac, r#""2021-03-04T05:06:07.123456789Z""#);
172
173        // Canonicalize +00:00 to Z
174        let w_plus00: EdmDateTimeOffset = "2021-03-04T05:06:07+00:00".parse().unwrap();
175        let json_plus00 = serde_json::to_string(&w_plus00).unwrap();
176        assert_eq!(json_plus00, r#""2021-03-04T05:06:07Z""#);
177    }
178
179    #[test]
180    fn serde_deserializes_from_conformant_strings() {
181        // UTC Z
182        let s_z = r#""2021-03-04T05:06:07Z""#;
183        let w_z: EdmDateTimeOffset = serde_json::from_str(s_z).unwrap();
184        assert_eq!(w_z.to_string(), "2021-03-04T05:06:07Z");
185        let dt_z: OffsetDateTime = w_z.into();
186        assert_eq!(dt_z.offset(), UtcOffset::UTC);
187
188        // Non-UTC offset preserved
189        let s_pos = r#""2021-03-04T10:36:07+05:30""#;
190        let w_pos: EdmDateTimeOffset = serde_json::from_str(s_pos).unwrap();
191        assert_eq!(w_pos.to_string(), "2021-03-04T10:36:07+05:30");
192        let dt_pos: OffsetDateTime = w_pos.into();
193        assert_eq!(dt_pos.offset(), UtcOffset::from_hms(5, 30, 0).unwrap());
194
195        // Fractional seconds retained
196        let s_frac = r#""2021-03-04T05:06:07.123456789Z""#;
197        let w_frac: EdmDateTimeOffset = serde_json::from_str(s_frac).unwrap();
198        assert_eq!(w_frac.to_string(), "2021-03-04T05:06:07.123456789Z");
199    }
200
201    #[test]
202    fn parses_and_displays_negative_offset() {
203        let s = "2021-03-04T00:06:07-05:00";
204        let w: EdmDateTimeOffset = s.parse().unwrap();
205        assert_eq!(w.to_string(), s);
206
207        let dt: OffsetDateTime = w.into();
208        assert_eq!(dt.offset(), UtcOffset::from_hms(-5, 0, 0).unwrap());
209    }
210
211    #[test]
212    fn parses_fractional_with_non_utc_offset() {
213        let s = "2021-03-04T05:06:07.5+01:00";
214        let w: EdmDateTimeOffset = s.parse().unwrap();
215        assert_eq!(w.to_string(), s);
216    }
217
218    #[test]
219    fn parses_boundary_offsets() {
220        // Commonly used extrema
221        let s_plus = "2021-03-04T12:00:00+14:00";
222        let w_plus: EdmDateTimeOffset = s_plus.parse().unwrap();
223        assert_eq!(w_plus.to_string(), s_plus);
224
225        let s_minus = "2021-03-04T12:00:00-12:00";
226        let w_minus: EdmDateTimeOffset = s_minus.parse().unwrap();
227        assert_eq!(w_minus.to_string(), s_minus);
228    }
229
230    #[test]
231    fn rejects_leap_second() {
232        assert!("2021-03-04T23:59:60Z".parse::<EdmDateTimeOffset>().is_err());
233    }
234
235    #[test]
236    fn canonicalizes_negative_zero_offset_to_z() {
237        let s = "2021-03-04T05:06:07-00:00";
238        let w: EdmDateTimeOffset = s.parse().unwrap();
239        assert_eq!("2021-03-04T05:06:07Z", w.to_string());
240    }
241
242    #[test]
243    fn converts_to_system_time() {
244        let normal: EdmDateTimeOffset = "2021-03-04T05:06:07-00:00".parse().unwrap();
245        let time: SystemTime = normal.into();
246        assert_eq!(
247            time.duration_since(SystemTime::UNIX_EPOCH)
248                .unwrap()
249                .as_secs(),
250            1614834367
251        );
252
253        let before_epoch: EdmDateTimeOffset = "1960-01-01T00:00:00-00:00".parse().unwrap();
254        let time: SystemTime = before_epoch.into();
255        assert_eq!(
256            SystemTime::UNIX_EPOCH
257                .duration_since(time)
258                .unwrap()
259                .as_secs(),
260            315619200
261        );
262
263        let very_old: EdmDateTimeOffset = "0001-01-01T00:00:00-00:00".parse().unwrap();
264        let time: SystemTime = very_old.into();
265        assert_eq!(
266            SystemTime::UNIX_EPOCH
267                .duration_since(time)
268                .unwrap()
269                .as_secs(),
270            62135596800
271        );
272
273        let far_future: EdmDateTimeOffset = "9999-12-31T23:59:59-00:00".parse().unwrap();
274        let time: SystemTime = far_future.into();
275        assert_eq!(
276            time.duration_since(SystemTime::UNIX_EPOCH)
277                .unwrap()
278                .as_secs(),
279            253402300799
280        );
281    }
282}