1use std::{fmt, str::FromStr, time::SystemTime};
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6const MAX_REFERENCE_CHARS: usize = 256;
7const MAX_TRACEPARENT_CHARS: usize = 512;
8const MAX_TRACESTATE_CHARS: usize = 1_024;
9
10macro_rules! uuid_identity {
11 ($name:ident, $kind:literal, $doc:literal) => {
12 #[doc = $doc]
13 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14 #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15 #[serde(transparent)]
16 pub struct $name(String);
17
18 impl $name {
19 #[must_use]
21 pub fn new() -> Self {
22 Self(Uuid::now_v7().to_string())
23 }
24
25 pub fn parse(value: impl AsRef<str>) -> Result<Self, IdentityError> {
27 let value = value.as_ref();
28 let parsed = Uuid::parse_str(value).map_err(|_| IdentityError::InvalidUuid {
29 kind: $kind,
30 value: value.to_owned(),
31 })?;
32 Ok(Self(parsed.hyphenated().to_string()))
33 }
34
35 #[must_use]
37 pub fn as_str(&self) -> &str {
38 &self.0
39 }
40
41 #[must_use]
43 pub fn into_string(self) -> String {
44 self.0
45 }
46 }
47
48 impl Default for $name {
49 fn default() -> Self {
50 Self::new()
51 }
52 }
53
54 impl fmt::Display for $name {
55 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56 formatter.write_str(&self.0)
57 }
58 }
59
60 impl FromStr for $name {
61 type Err = IdentityError;
62
63 fn from_str(value: &str) -> Result<Self, Self::Err> {
64 Self::parse(value)
65 }
66 }
67 };
68}
69
70uuid_identity!(
71 OperationId,
72 "operation",
73 "Unique identity for one operation execution."
74);
75uuid_identity!(
76 EventId,
77 "event",
78 "Unique identity for one operation lifecycle event."
79);
80uuid_identity!(
81 CorrelationId,
82 "correlation",
83 "Identity shared by operations participating in one workflow or incident."
84);
85uuid_identity!(
86 AuthorizationId,
87 "authorization",
88 "Opaque identity for authorization evidence issued by a product policy layer."
89);
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
93#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
94#[serde(transparent)]
95pub struct Timestamp(i64);
96
97impl Timestamp {
98 #[must_use]
100 pub const fn from_unix_millis(value: i64) -> Self {
101 Self(value)
102 }
103
104 #[must_use]
106 pub fn now() -> Self {
107 match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
108 Ok(duration) => Self(clamp_millis(duration.as_millis() as i128)),
109 Err(error) => Self(clamp_millis(-(error.duration().as_millis() as i128))),
110 }
111 }
112
113 #[must_use]
115 pub const fn unix_millis(self) -> i64 {
116 self.0
117 }
118}
119
120fn clamp_millis(value: i128) -> i64 {
121 value.clamp(i64::MIN as i128, i64::MAX as i128) as i64
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
126#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
127pub struct ActorRef {
128 namespace: String,
129 id: String,
130}
131
132impl ActorRef {
133 pub fn new(namespace: impl Into<String>, id: impl Into<String>) -> Result<Self, IdentityError> {
135 let namespace = namespace.into();
136 let id = id.into();
137 validate_reference("actor namespace", &namespace)?;
138 validate_reference("actor id", &id)?;
139 Ok(Self { namespace, id })
140 }
141
142 #[must_use]
144 pub fn namespace(&self) -> &str {
145 &self.namespace
146 }
147
148 #[must_use]
150 pub fn id(&self) -> &str {
151 &self.id
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
158pub struct ProducerRef {
159 name: String,
160 version: String,
161}
162
163impl ProducerRef {
164 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Result<Self, IdentityError> {
166 let name = name.into();
167 let version = version.into();
168 validate_reference("producer name", &name)?;
169 validate_reference("producer version", &version)?;
170 Ok(Self { name, version })
171 }
172
173 #[must_use]
175 pub fn name(&self) -> &str {
176 &self.name
177 }
178
179 #[must_use]
181 pub fn version(&self) -> &str {
182 &self.version
183 }
184}
185
186#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
188#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
189pub struct TraceContext {
190 traceparent: Option<String>,
191 tracestate: Option<String>,
192}
193
194impl TraceContext {
195 pub fn new(
197 traceparent: Option<impl Into<String>>,
198 tracestate: Option<impl Into<String>>,
199 ) -> Result<Self, IdentityError> {
200 let traceparent = traceparent.map(Into::into);
201 let tracestate = tracestate.map(Into::into);
202 validate_optional_trace("traceparent", traceparent.as_deref(), MAX_TRACEPARENT_CHARS)?;
203 validate_optional_trace("tracestate", tracestate.as_deref(), MAX_TRACESTATE_CHARS)?;
204 Ok(Self {
205 traceparent,
206 tracestate,
207 })
208 }
209
210 #[must_use]
212 pub fn traceparent(&self) -> Option<&str> {
213 self.traceparent.as_deref()
214 }
215
216 #[must_use]
218 pub fn tracestate(&self) -> Option<&str> {
219 self.tracestate.as_deref()
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
225#[non_exhaustive]
226pub enum IdentityError {
227 #[error("invalid {kind} UUID: {value}")]
229 InvalidUuid {
230 kind: &'static str,
232 value: String,
234 },
235 #[error("invalid {field}: expected 1..={max_chars} non-control characters")]
237 InvalidReference {
238 field: &'static str,
240 max_chars: usize,
242 },
243}
244
245fn validate_reference(field: &'static str, value: &str) -> Result<(), IdentityError> {
246 let chars = value.chars().count();
247 if chars == 0 || chars > MAX_REFERENCE_CHARS || value.chars().any(char::is_control) {
248 return Err(IdentityError::InvalidReference {
249 field,
250 max_chars: MAX_REFERENCE_CHARS,
251 });
252 }
253 Ok(())
254}
255
256fn validate_optional_trace(
257 field: &'static str,
258 value: Option<&str>,
259 max_chars: usize,
260) -> Result<(), IdentityError> {
261 let Some(value) = value else {
262 return Ok(());
263 };
264 let chars = value.chars().count();
265 if chars == 0 || chars > max_chars || value.chars().any(char::is_control) {
266 return Err(IdentityError::InvalidReference { field, max_chars });
267 }
268 Ok(())
269}
270
271#[cfg(test)]
272#[path = "identity_tests.rs"]
273mod tests;