1#![forbid(unsafe_code)]
7
8use super::error::DomainError;
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use std::fmt;
12
13#[repr(transparent)]
17#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct Rfc3339Utc(DateTime<Utc>);
19
20pub type AddedAt = Rfc3339Utc;
22
23pub type CreatedAt = Rfc3339Utc;
25
26impl Rfc3339Utc {
27 #[must_use]
29 pub fn now() -> Self {
30 Self(Utc::now())
31 }
32
33 pub fn try_new(raw: impl AsRef<str>) -> Result<Self, DomainError> {
35 let s = raw.as_ref().trim();
36 if s.is_empty() {
37 return Err(DomainError::new("rfc3339", "timestamp must not be empty"));
38 }
39 if s.len() > 64 {
40 return Err(DomainError::new("rfc3339", "timestamp too long (max 64)"));
41 }
42 DateTime::parse_from_rfc3339(s)
43 .map(|d| Self(d.with_timezone(&Utc)))
44 .map_err(|e| DomainError::new("rfc3339", e.to_string()))
45 }
46
47 #[must_use]
49 pub const fn from_utc(dt: DateTime<Utc>) -> Self {
50 Self(dt)
51 }
52
53 #[must_use]
55 pub const fn as_datetime(&self) -> &DateTime<Utc> {
56 &self.0
57 }
58
59 #[must_use]
61 pub const fn into_inner(self) -> DateTime<Utc> {
62 self.0
63 }
64
65 #[must_use]
67 pub fn to_rfc3339(&self) -> String {
68 self.0.to_rfc3339()
69 }
70}
71
72impl fmt::Display for Rfc3339Utc {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.write_str(&self.0.to_rfc3339())
75 }
76}
77
78impl Serialize for Rfc3339Utc {
79 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
80 s.serialize_str(&self.0.to_rfc3339())
81 }
82}
83
84impl<'de> Deserialize<'de> for Rfc3339Utc {
85 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
86 let s = String::deserialize(d)?;
87 Self::try_new(s).map_err(serde::de::Error::custom)
88 }
89}
90
91impl From<DateTime<Utc>> for Rfc3339Utc {
92 fn from(dt: DateTime<Utc>) -> Self {
93 Self(dt)
94 }
95}
96
97impl From<Rfc3339Utc> for DateTime<Utc> {
98 fn from(v: Rfc3339Utc) -> Self {
99 v.0
100 }
101}
102
103impl TryFrom<String> for Rfc3339Utc {
104 type Error = DomainError;
105
106 fn try_from(value: String) -> Result<Self, Self::Error> {
107 Self::try_new(value)
108 }
109}
110
111impl TryFrom<&str> for Rfc3339Utc {
112 type Error = DomainError;
113
114 fn try_from(value: &str) -> Result<Self, Self::Error> {
115 Self::try_new(value)
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn parses_rfc3339_and_normalizes_to_utc() {
125 let t = Rfc3339Utc::try_new("2014-11-28T21:00:09+09:00").unwrap();
126 assert_eq!(t.as_datetime().to_rfc3339(), "2014-11-28T12:00:09+00:00");
127 }
128
129 #[test]
130 fn rejects_garbage() {
131 assert!(Rfc3339Utc::try_new("not-a-date").is_err());
132 assert!(Rfc3339Utc::try_new("").is_err());
133 assert!(Rfc3339Utc::try_new("x".repeat(65)).is_err());
134 }
135
136 #[test]
137 fn now_is_utc() {
138 let n = Rfc3339Utc::now();
139 assert!(n.to_rfc3339().contains('T') || n.to_rfc3339().contains('+'));
140 }
141
142 #[test]
143 fn serde_roundtrip() {
144 let t = Rfc3339Utc::try_new("2024-01-02T03:04:05Z").unwrap();
145 let j = serde_json::to_string(&t).unwrap();
146 assert_eq!(j, "\"2024-01-02T03:04:05+00:00\"");
147 let back: Rfc3339Utc = serde_json::from_str(&j).unwrap();
148 assert_eq!(back, t);
149 }
150}