projectx_client/
timestamp.rs1use std::{fmt, str::FromStr};
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use thiserror::Error;
10
11#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
16#[repr(transparent)]
17pub struct Timestamp(jiff::Timestamp);
18
19impl Timestamp {
20 pub fn new(value: &str) -> Result<Self, TimestampError> {
27 value.parse()
28 }
29
30 #[must_use]
32 pub const fn as_jiff(&self) -> &jiff::Timestamp {
33 &self.0
34 }
35}
36
37impl From<jiff::Timestamp> for Timestamp {
38 fn from(value: jiff::Timestamp) -> Self {
39 Self(value)
40 }
41}
42
43impl From<Timestamp> for jiff::Timestamp {
44 fn from(value: Timestamp) -> Self {
45 value.0
46 }
47}
48
49impl FromStr for Timestamp {
50 type Err = TimestampError;
51
52 fn from_str(value: &str) -> Result<Self, Self::Err> {
53 value
54 .parse::<jiff::Timestamp>()
55 .map(Self)
56 .map_err(|_| TimestampError)
57 }
58}
59
60impl TryFrom<String> for Timestamp {
61 type Error = TimestampError;
62
63 fn try_from(value: String) -> Result<Self, Self::Error> {
64 value.parse()
65 }
66}
67
68impl fmt::Display for Timestamp {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 self.0.fmt(formatter)
71 }
72}
73
74impl Serialize for Timestamp {
75 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
76 where
77 S: Serializer,
78 {
79 self.0.serialize(serializer)
80 }
81}
82
83impl<'de> Deserialize<'de> for Timestamp {
84 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
85 where
86 D: Deserializer<'de>,
87 {
88 let timestamp = jiff::Timestamp::deserialize(deserializer)?;
89 Ok(Self(timestamp))
90 }
91}
92
93#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
95#[error("timestamp must be an absolute ISO-8601/RFC-3339 value with an offset")]
96pub struct TimestampError;
97
98#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
100#[repr(transparent)]
101pub struct ProviderDate(jiff::civil::Date);
102
103impl ProviderDate {
104 pub fn new(value: &str) -> Result<Self, ProviderDateError> {
111 value.parse()
112 }
113
114 #[must_use]
116 pub const fn as_jiff(&self) -> &jiff::civil::Date {
117 &self.0
118 }
119}
120
121impl From<jiff::civil::Date> for ProviderDate {
122 fn from(value: jiff::civil::Date) -> Self {
123 Self(value)
124 }
125}
126
127impl From<ProviderDate> for jiff::civil::Date {
128 fn from(value: ProviderDate) -> Self {
129 value.0
130 }
131}
132
133impl FromStr for ProviderDate {
134 type Err = ProviderDateError;
135
136 fn from_str(value: &str) -> Result<Self, Self::Err> {
137 value
138 .parse::<jiff::civil::Date>()
139 .map(Self)
140 .map_err(|_| ProviderDateError)
141 }
142}
143
144impl TryFrom<String> for ProviderDate {
145 type Error = ProviderDateError;
146
147 fn try_from(value: String) -> Result<Self, Self::Error> {
148 value.parse()
149 }
150}
151
152impl fmt::Display for ProviderDate {
153 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154 self.0.fmt(formatter)
155 }
156}
157
158impl Serialize for ProviderDate {
159 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
160 where
161 S: Serializer,
162 {
163 self.0.serialize(serializer)
164 }
165}
166
167impl<'de> Deserialize<'de> for ProviderDate {
168 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
169 where
170 D: Deserializer<'de>,
171 {
172 let date = jiff::civil::Date::deserialize(deserializer)?;
173 Ok(Self(date))
174 }
175}
176
177#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
179#[error("provider date must be a valid ISO-8601 calendar date")]
180pub struct ProviderDateError;
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn timestamp_rejects_malformed_or_offset_free_values() {
188 assert!(Timestamp::new("not-a-date").is_err());
189 assert!(Timestamp::new("2026-01-01T00:00:00").is_err());
190 }
191
192 #[test]
193 fn timestamp_round_trips_a_canonical_instant() {
194 let timestamp = Timestamp::new("2026-01-01T08:00:00+08:00")
195 .unwrap_or_else(|error| panic!("fixture timestamp must be valid: {error}"));
196 let encoded = serde_json::to_string(×tamp)
197 .unwrap_or_else(|error| panic!("fixture timestamp must serialize: {error}"));
198 assert_eq!(encoded, "\"2026-01-01T00:00:00Z\"");
199 let decoded: Timestamp = serde_json::from_str(&encoded)
200 .unwrap_or_else(|error| panic!("fixture timestamp must deserialize: {error}"));
201 assert_eq!(decoded, timestamp);
202
203 let jiff_timestamp: jiff::Timestamp = timestamp.into();
204 assert_eq!(Timestamp::from(jiff_timestamp), timestamp);
205 }
206
207 #[test]
208 fn provider_date_validates_and_round_trips() {
209 let date = ProviderDate::new("2026-02-28")
210 .unwrap_or_else(|error| panic!("fixture date must be valid: {error}"));
211 assert_eq!(date.to_string(), "2026-02-28");
212 assert!(ProviderDate::new("2026-02-29").is_err());
213 assert!(serde_json::from_str::<ProviderDate>("\"not-a-date\"").is_err());
214 let jiff_date: jiff::civil::Date = date.into();
215 assert_eq!(ProviderDate::from(jiff_date), date);
216 }
217}