1macro_rules! string_id {
16 ($(#[$doc:meta])* $name:ident) => {
17 $(#[$doc])*
18 #[derive(
19 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
20 serde::Serialize, serde::Deserialize, specta::Type,
21 )]
22 #[serde(transparent)]
23 pub struct $name(pub String);
24
25 impl $name {
26 pub fn new(value: impl Into<String>) -> Self {
27 Self(value.into())
28 }
29
30 pub fn as_str(&self) -> &str {
31 &self.0
32 }
33 }
34
35 impl std::fmt::Display for $name {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.write_str(&self.0)
38 }
39 }
40 };
41}
42
43macro_rules! u64_decimal_string {
45 ($(#[$doc:meta])* $name:ident) => {
46 $(#[$doc])*
47 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
48 pub struct $name(pub u64);
49
50 impl $name {
51 pub const fn new(value: u64) -> Self {
52 Self(value)
53 }
54
55 pub const fn value(self) -> u64 {
56 self.0
57 }
58 }
59
60 impl std::fmt::Display for $name {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 write!(f, "{}", self.0)
63 }
64 }
65
66 impl serde::Serialize for $name {
67 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
68 s.collect_str(&self.0)
69 }
70 }
71
72 impl<'de> serde::Deserialize<'de> for $name {
73 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
74 let raw = <std::borrow::Cow<'de, str>>::deserialize(d)?;
75 let canonical = !raw.is_empty()
76 && raw.len() <= 20
77 && raw.bytes().all(|b| b.is_ascii_digit())
78 && (raw.len() == 1 || !raw.starts_with('0'));
79 if !canonical {
80 return Err(serde::de::Error::custom(concat!(
81 stringify!($name),
82 " must be a canonical decimal u64 string"
83 )));
84 }
85 raw.parse::<u64>().map(Self).map_err(serde::de::Error::custom)
86 }
87 }
88
89 impl specta::Type for $name {
91 fn definition(types: &mut specta::Types) -> specta::datatype::DataType {
92 <String as specta::Type>::definition(types)
93 }
94 }
95 };
96}
97
98string_id!(
99 EventId
101);
102string_id!(
103 CommandId
105);
106string_id!(
107 ProjectId
109);
110string_id!(
111 AggregateId
113);
114string_id!(TaskId);
115string_id!(AttemptId);
116string_id!(MessageId);
117string_id!(LeaseId);
118string_id!(GateId);
119string_id!(ReceiptId);
120string_id!(WorktreeId);
121string_id!(DispatchNodeId);
122string_id!(AttentionItemId);
123string_id!(AuthorityGrantId);
124string_id!(EvidenceId);
125string_id!(
126 IngestedRecordId
131);
132string_id!(EngineSessionId);
133string_id!(
134 CorrelationId
136);
137string_id!(
138 IdempotencyKey
140);
141string_id!(
142 EngineId
145);
146string_id!(
147 RequestId
150);
151string_id!(
152 BlobUploadId
155);
156
157u64_decimal_string!(
158 Seq
161);
162u64_decimal_string!(
163 FenceToken
166);
167u64_decimal_string!(
168 ByteCount
170);
171u64_decimal_string!(
172 WriterEpoch
176);
177u64_decimal_string!(
178 EventCount
182);
183u64_decimal_string!(
184 CostMicros
186);
187
188#[derive(
191 Debug,
192 Clone,
193 PartialEq,
194 Eq,
195 PartialOrd,
196 Ord,
197 Hash,
198 serde::Serialize,
199 serde::Deserialize,
200 specta::Type,
201)]
202#[serde(transparent)]
203pub struct Timestamp(pub String);
204
205impl Timestamp {
206 pub fn new(value: impl Into<String>) -> Self {
207 Self(value.into())
208 }
209
210 pub fn as_str(&self) -> &str {
211 &self.0
212 }
213}
214
215impl std::fmt::Display for Timestamp {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 f.write_str(&self.0)
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn seq_round_trips_as_decimal_string() {
227 let seq = Seq::new(u64::MAX);
228 let json = serde_json::to_string(&seq).expect("serialize");
229 assert_eq!(json, "\"18446744073709551615\"");
230 let back: Seq = serde_json::from_str(&json).expect("deserialize");
231 assert_eq!(back, seq);
232 }
233
234 #[test]
235 fn seq_rejects_non_canonical_input() {
236 for bad in [
237 "\"\"",
238 "\"01\"",
239 "\"1e3\"",
240 "\"-1\"",
241 "\" 1\"",
242 "\"18446744073709551616\"",
243 "7",
244 ] {
245 assert!(
246 serde_json::from_str::<Seq>(bad).is_err(),
247 "accepted non-canonical {bad}"
248 );
249 }
250 let zero: Seq = serde_json::from_str("\"0\"").expect("bare zero is canonical");
251 assert_eq!(zero, Seq::new(0));
252 }
253}