Skip to main content

jmap_types/
id.rs

1//! RFC 8620 §1.2/§1.4 opaque string newtypes: [`Id`], [`UTCDate`], [`Date`], [`State`].
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Opaque non-empty server-assigned identifier (RFC 8620 §1.2).
7///
8/// Character set: URL-safe base64 alphabet (A-Za-z0-9, `-`, `_`), max 255 octets.
9/// Clients MUST treat Id values as opaque strings — no parsing of structure.
10// #[non_exhaustive] prevents callers from pattern-matching the inner field
11// (e.g. `let Id(s) = id;`), preserving semver freedom to add fields later.
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14#[non_exhaustive]
15pub struct Id(String);
16
17/// RFC 3339 UTC timestamp string (RFC 8620 §1.4).
18///
19/// Format: `YYYY-MM-DDTHH:MM:SSZ` — time-offset MUST be `Z`, letters uppercase,
20/// fractional seconds omitted if zero. Example: `"2014-10-30T06:12:00Z"`.
21// #[non_exhaustive] prevents callers from pattern-matching the inner field,
22// preserving semver freedom to add fields later.
23#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(transparent)]
25#[non_exhaustive]
26pub struct UTCDate(String);
27
28/// RFC 3339 date-time string with any timezone offset (RFC 8620 §1.4).
29///
30/// Format: `YYYY-MM-DDTHH:MM:SS±HH:MM` or `Z` suffix — any valid RFC 3339 offset,
31/// letters uppercase, fractional seconds omitted if zero.
32/// Example: `"2014-10-30T14:12:00+08:00"`.
33///
34/// Distinct from [`UTCDate`], which requires the time-offset to be `Z`.
35/// Use `Date` for fields derived from RFC 5322 email headers (e.g. `sentAt`),
36/// which commonly carry non-UTC offsets.
37// #[non_exhaustive] prevents callers from pattern-matching the inner field,
38// preserving semver freedom to add fields later.
39#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
40#[serde(transparent)]
41#[non_exhaustive]
42pub struct Date(String);
43
44/// Opaque server state token (RFC 8620 §1.2).
45///
46/// Returned by `/get` and `/changes` methods. Clients echo it back in
47/// `sinceState` / `ifInState` parameters. Treat as opaque — no structure assumed.
48// #[non_exhaustive] prevents callers from pattern-matching the inner field,
49// preserving semver freedom to add fields later.
50#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
51#[serde(transparent)]
52#[non_exhaustive]
53pub struct State(String);
54
55/// Generates `Display`, `From<String>`, `From<&str>`, `AsRef<str>`,
56/// `PartialEq<str>`, `PartialEq<&str>`, and `into_inner` for a transparent
57/// `String` newtype.
58macro_rules! impl_string_newtype {
59    ($T:ident) => {
60        impl fmt::Display for $T {
61            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62                f.write_str(&self.0)
63            }
64        }
65        impl From<String> for $T {
66            fn from(s: String) -> Self {
67                Self(s)
68            }
69        }
70        impl From<&str> for $T {
71            fn from(s: &str) -> Self {
72                Self(s.to_owned())
73            }
74        }
75        impl AsRef<str> for $T {
76            fn as_ref(&self) -> &str {
77                &self.0
78            }
79        }
80        impl PartialEq<str> for $T {
81            fn eq(&self, other: &str) -> bool {
82                self.0 == other
83            }
84        }
85        impl PartialEq<&str> for $T {
86            fn eq(&self, other: &&str) -> bool {
87                self.0 == *other
88            }
89        }
90        impl std::borrow::Borrow<str> for $T {
91            fn borrow(&self) -> &str {
92                &self.0
93            }
94        }
95        impl $T {
96            /// Consumes the value and returns the inner `String`.
97            pub fn into_inner(self) -> String {
98                self.0
99            }
100        }
101    };
102}
103
104impl_string_newtype!(Id);
105impl_string_newtype!(UTCDate);
106impl_string_newtype!(Date);
107impl_string_newtype!(State);
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    // Oracle: RFC 8620 §1.2 — Id is a plain JSON string, not a wrapped object.
114    #[test]
115    fn id_serializes_as_plain_string() {
116        let id = Id("abc123".to_owned());
117        let json = serde_json::to_string(&id).expect("serialize Id");
118        assert_eq!(json, "\"abc123\"");
119    }
120
121    // Oracle: RFC 8620 §1.2 — Id round-trips through JSON.
122    #[test]
123    fn id_deserializes_from_plain_string() {
124        let id: Id = serde_json::from_str("\"abc123\"").expect("deserialize Id");
125        assert_eq!(id.as_ref(), "abc123");
126    }
127
128    // Oracle: RFC 8620 §1.4 example — "2014-10-30T06:12:00Z".
129    #[test]
130    fn utcdate_serializes_as_plain_string() {
131        let d = UTCDate("2014-10-30T06:12:00Z".to_owned());
132        let json = serde_json::to_string(&d).expect("serialize UTCDate");
133        assert_eq!(json, "\"2014-10-30T06:12:00Z\"");
134    }
135
136    // Oracle: RFC 8620 §3.4.1 fixture — sessionState value is "75128aab4b1b".
137    #[test]
138    fn state_serializes_as_plain_string() {
139        let s = State("75128aab4b1b".to_owned());
140        let json = serde_json::to_string(&s).expect("serialize State");
141        assert_eq!(json, "\"75128aab4b1b\"");
142    }
143
144    // Oracle: From<&str> trait contract.
145    #[test]
146    fn id_from_str() {
147        let id = Id::from("hello");
148        assert_eq!(id.as_ref(), "hello");
149    }
150
151    // Oracle: Display delegates to inner String.
152    #[test]
153    fn id_display() {
154        let id = Id("display-test".to_owned());
155        assert_eq!(id.to_string(), "display-test");
156    }
157
158    // Oracle: AsRef<str> returns the inner string.
159    #[test]
160    fn id_as_ref_str() {
161        let id = Id("ref-test".to_owned());
162        assert_eq!(id.as_ref(), "ref-test");
163    }
164
165    // Oracle: RFC 8620 §3.4.1 — State in sessionState field round-trips correctly.
166    #[test]
167    fn state_round_trip() {
168        let s = State("75128aab4b1b".to_owned());
169        let json = serde_json::to_string(&s).expect("serialize");
170        let s2: State = serde_json::from_str(&json).expect("deserialize");
171        assert_eq!(s, s2);
172    }
173
174    // Oracle: RFC 8620 §1.4 example — Date allows non-UTC offsets, unlike UTCDate.
175    #[test]
176    fn date_accepts_non_utc_offset() {
177        let d = Date("2014-10-30T14:12:00+08:00".to_owned());
178        let json = serde_json::to_string(&d).expect("serialize Date");
179        assert_eq!(json, "\"2014-10-30T14:12:00+08:00\"");
180        let d2: Date = serde_json::from_str(&json).expect("deserialize Date");
181        assert_eq!(d, d2);
182    }
183}