1use serde::{Deserialize, Serialize};
9
10use crate::error::{AxCode, AxError};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct RunId(#[serde(deserialize_with = "read_run_id")] uuid::Uuid);
17
18impl RunId {
19 pub const CITY: RunId = RunId(uuid::Uuid::nil());
22
23 pub fn from_bytes(bytes: [u8; 16]) -> Self {
24 RunId(uuid::Uuid::from_bytes(bytes))
25 }
26
27 pub fn parse(raw: &str) -> Result<Self, AxError> {
28 let reject = || {
29 let err = AxError::failure(AxCode::InvalidArgs, "parse run id", raw).with_recovery(
30 "use the hyphenated lower-case uuid this city writes, as \
31 `0198f6a2-7c4a-7bbb-9d1e-000000000001`",
32 );
33 Err(err)
34 };
35 let parsed = match uuid::Uuid::parse_str(raw) {
36 Ok(parsed) => parsed,
37 Err(_) => return reject(),
38 };
39 if parsed.hyphenated().to_string() != raw {
43 return reject();
44 }
45 Ok(RunId(parsed))
46 }
47
48 pub fn as_bytes(&self) -> &[u8; 16] {
49 self.0.as_bytes()
50 }
51}
52
53fn read_run_id<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<uuid::Uuid, D::Error> {
62 if !deserializer.is_human_readable() {
63 return uuid::Uuid::deserialize(deserializer);
64 }
65 let raw = String::deserialize(deserializer)?;
66 RunId::parse(&raw)
67 .map(|run| run.0)
68 .map_err(serde::de::Error::custom)
69}
70
71impl std::fmt::Display for RunId {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 self.0.hyphenated().fmt(f)
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
79#[serde(transparent)]
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
81pub struct Seq(u64);
82
83impl Seq {
84 pub const FIRST: Seq = Seq(0);
86
87 pub fn new(value: u64) -> Self {
88 Seq(value)
89 }
90
91 pub fn value(&self) -> u64 {
92 self.0
93 }
94
95 pub fn next(self) -> Result<Seq, AxError> {
96 self.0.checked_add(1).map(Seq).ok_or_else(|| {
97 AxError::failure(AxCode::InvalidArgs, "advance seq", self.0.to_string())
98 .with_recovery("sequence space exhausted; this ledger cannot grow further")
99 })
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
106#[serde(transparent)]
107#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
108pub struct TimeMs(u64);
109
110impl TimeMs {
111 pub fn new(value: u64) -> Self {
112 TimeMs(value)
113 }
114
115 pub fn value(&self) -> u64 {
116 self.0
117 }
118}
119
120#[cfg(test)]
121#[allow(
122 clippy::unwrap_used,
123 clippy::expect_used,
124 clippy::panic,
125 clippy::indexing_slicing,
126 reason = "test code"
127)]
128mod tests {
129 use super::*;
130 #[test]
131 fn seq_and_time_are_checked_integers() {
132 assert_eq!(Seq::FIRST.value(), 0);
133 assert_eq!(Seq::FIRST.next().unwrap().value(), 1);
134 assert!(Seq::new(u64::MAX).next().is_err());
135 assert_eq!(TimeMs::new(1234).value(), 1234);
136 }
137
138 #[test]
139 fn a_run_id_is_read_in_the_one_spelling_the_city_writes() {
140 assert_eq!(
141 RunId::CITY.to_string(),
142 "00000000-0000-0000-0000-000000000000"
143 );
144 let run = RunId::parse("0198f6a2-7c4a-7bbb-9d1e-000000000001").unwrap();
145 assert_eq!(run.to_string(), "0198f6a2-7c4a-7bbb-9d1e-000000000001");
146 for refused in [
147 "not-a-uuid",
148 "0198f6a27c4a7bbb9d1e000000000001", "{0198f6a2-7c4a-7bbb-9d1e-000000000001}", "urn:uuid:0198f6a2-7c4a-7bbb-9d1e-000000000001", "0198F6A2-7C4A-7BBB-9D1E-000000000001", ] {
153 assert!(RunId::parse(refused).is_err(), "{refused}");
154 }
155 }
156
157 #[test]
161 fn serde_reads_a_run_id_through_its_sole_constructor() {
162 let written = "\"0198f6a2-7c4a-7bbb-9d1e-000000000001\"";
163 let run: RunId = serde_json::from_str(written).unwrap();
164 assert_eq!(serde_json::to_string(&run).unwrap(), written);
165 for refused in [
166 "\"0198f6a27c4a7bbb9d1e000000000001\"",
167 "\"{0198f6a2-7c4a-7bbb-9d1e-000000000001}\"",
168 "\"urn:uuid:0198f6a2-7c4a-7bbb-9d1e-000000000001\"",
169 ] {
170 assert!(serde_json::from_str::<RunId>(refused).is_err(), "{refused}");
171 }
172 }
173}