Skip to main content

saddle_core/
context.rs

1use std::{fmt, sync::Arc};
2
3pub const MAX_TRACE_CORRELATION_ID_BYTES: usize = 256;
4
5macro_rules! string_id {
6    ($name:ident) => {
7        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8        pub struct $name(Arc<str>);
9
10        impl $name {
11            pub fn new(value: impl Into<Arc<str>>) -> Self {
12                Self(value.into())
13            }
14
15            pub fn as_str(&self) -> &str {
16                &self.0
17            }
18        }
19
20        impl From<&str> for $name {
21            fn from(value: &str) -> Self {
22                Self::new(value)
23            }
24        }
25
26        impl From<String> for $name {
27            fn from(value: String) -> Self {
28                Self::new(value)
29            }
30        }
31
32        impl fmt::Display for $name {
33            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34                formatter.write_str(&self.0)
35            }
36        }
37    };
38}
39
40string_id!(ApplicationId);
41string_id!(ModuleId);
42string_id!(ServiceId);
43string_id!(OperationId);
44
45/// Validated protocol RPC correlation, distinct from an internal span ID.
46/// This value carries identity only; it never allocates a child sequence.
47#[derive(Clone, Debug, Eq, Hash, PartialEq)]
48struct InlineCorrelation {
49    bytes: [u8; MAX_TRACE_CORRELATION_ID_BYTES],
50    len: u16,
51}
52impl InlineCorrelation {
53    fn copy(value: &str) -> Self {
54        let mut text = Self { bytes: [0; MAX_TRACE_CORRELATION_ID_BYTES], len: value.len() as u16 };
55        text.bytes[..value.len()].copy_from_slice(value.as_bytes());
56        text
57    }
58    fn as_str(&self) -> &str {
59        std::str::from_utf8(&self.bytes[..usize::from(self.len)]).expect("validated correlation")
60    }
61}
62
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct RpcCorrelationId(InlineCorrelation);
65
66impl RpcCorrelationId {
67    pub fn new(value: &str) -> Option<Self> {
68        if value.trim().is_empty() || value.len() > 256 || value.chars().any(char::is_control) {
69            return None;
70        }
71        Some(Self(InlineCorrelation::copy(value)))
72    }
73
74    pub fn as_str(&self) -> &str {
75        self.0.as_str()
76    }
77}
78
79#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
80pub struct TraceId(u128);
81
82impl TraceId {
83    pub const fn from_u128(value: u128) -> Self {
84        Self(value)
85    }
86
87    pub const fn as_u128(self) -> u128 {
88        self.0
89    }
90}
91
92impl fmt::Display for TraceId {
93    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(formatter, "{:032x}", self.0)
95    }
96}
97
98#[derive(Clone, Debug, Eq, Hash, PartialEq)]
99pub struct TraceCorrelationId(InlineCorrelation);
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub enum TraceCorrelationIdError {
103    Empty,
104    TooLong,
105    ControlCharacter,
106}
107
108impl TraceCorrelationId {
109    #[doc(hidden)]
110    pub fn from_generated(trace_id: TraceId) -> Self {
111        const HEX: &[u8; 16] = b"0123456789abcdef";
112        let mut text = InlineCorrelation { bytes: [0; MAX_TRACE_CORRELATION_ID_BYTES], len: 32 };
113        let mut value = trace_id.as_u128();
114        for index in (0..32).rev() {
115            text.bytes[index] = HEX[(value & 0x0f) as usize];
116            value >>= 4;
117        }
118        Self(text)
119    }
120    pub fn new(value: impl Into<Arc<str>>) -> Result<Self, TraceCorrelationIdError> {
121        let value = value.into();
122        Self::from_borrowed(&value)
123    }
124
125    #[doc(hidden)]
126    pub fn from_borrowed(value: &str) -> Result<Self, TraceCorrelationIdError> {
127        if value.is_empty() {
128            return Err(TraceCorrelationIdError::Empty);
129        }
130        if value.len() > MAX_TRACE_CORRELATION_ID_BYTES {
131            return Err(TraceCorrelationIdError::TooLong);
132        }
133        if value.chars().any(char::is_control) {
134            return Err(TraceCorrelationIdError::ControlCharacter);
135        }
136        Ok(Self(InlineCorrelation::copy(value)))
137    }
138
139    pub fn as_str(&self) -> &str {
140        self.0.as_str()
141    }
142}
143
144impl fmt::Display for TraceCorrelationId {
145    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146        formatter.write_str(self.as_str())
147    }
148}
149
150#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
151pub struct SpanId(u64);
152
153impl SpanId {
154    pub const fn from_u64(value: u64) -> Self {
155        Self(value)
156    }
157
158    pub const fn as_u64(self) -> u64 {
159        self.0
160    }
161}
162
163impl fmt::Display for SpanId {
164    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
165        write!(formatter, "{:016x}", self.0)
166    }
167}
168
169/// The minimum identity propagated through Service, DB and Observability in V1.
170///
171/// Deadline, cancellation and resource-budget fields are deliberately absent
172/// until a version explicitly defines their behavior.
173#[derive(Clone, Debug, Eq, PartialEq)]
174pub struct CallContext {
175    application: ApplicationId,
176    module: ModuleId,
177    service: ServiceId,
178    operation: OperationId,
179    trace_id: TraceId,
180    trace_correlation_id: TraceCorrelationId,
181    span_id: SpanId,
182    rpc_correlation_id: Option<RpcCorrelationId>,
183}
184
185impl CallContext {
186    pub fn new(
187        application: ApplicationId,
188        module: ModuleId,
189        service: ServiceId,
190        operation: OperationId,
191        trace_id: TraceId,
192        span_id: SpanId,
193    ) -> Self {
194        let trace_correlation_id = TraceCorrelationId::from_generated(trace_id);
195        Self {
196            application,
197            module,
198            service,
199            operation,
200            trace_id,
201            trace_correlation_id,
202            span_id,
203            rpc_correlation_id: None,
204        }
205    }
206
207    pub fn with_trace_correlation_id(mut self, trace_correlation_id: TraceCorrelationId) -> Self {
208        self.trace_correlation_id = trace_correlation_id;
209        self
210    }
211
212    pub fn application(&self) -> &ApplicationId {
213        &self.application
214    }
215
216    pub fn with_rpc_correlation_id(mut self, rpc: Option<RpcCorrelationId>) -> Self {
217        self.rpc_correlation_id = rpc;
218        self
219    }
220
221    pub fn rpc_correlation_id(&self) -> Option<&RpcCorrelationId> {
222        self.rpc_correlation_id.as_ref()
223    }
224
225    pub fn module(&self) -> &ModuleId {
226        &self.module
227    }
228
229    pub fn service(&self) -> &ServiceId {
230        &self.service
231    }
232
233    pub fn operation(&self) -> &OperationId {
234        &self.operation
235    }
236
237    pub const fn trace_id(&self) -> TraceId {
238        self.trace_id
239    }
240
241    pub fn trace_correlation_id(&self) -> &TraceCorrelationId {
242        &self.trace_correlation_id
243    }
244
245    pub const fn span_id(&self) -> SpanId {
246        self.span_id
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn protocol_rpc_validation_does_not_mint_or_replace_identity() {
256        for value in ["0", "0.1", "0.12.3"] {
257            assert_eq!(RpcCorrelationId::new(value).unwrap().as_str(), value);
258        }
259        for value in ["", " ", "0\n1", "0\u{7f}"] {
260            assert!(RpcCorrelationId::new(value).is_none());
261        }
262        assert!(RpcCorrelationId::new(&"1".repeat(256)).is_some());
263        assert!(RpcCorrelationId::new(&"1".repeat(257)).is_none());
264    }
265
266    #[test]
267    fn inline_correlations_preserve_generated_hex_and_bounded_utf8() {
268        let trace = TraceId::from_u128(0x1234_5678_90ab_cdef_0123_4567_89ab_cdef);
269        assert_eq!(TraceCorrelationId::from_generated(trace).as_str(), trace.to_string());
270        let inbound = format!("{}a", "界".repeat(85));
271        assert_eq!(TraceCorrelationId::from_borrowed(&inbound).unwrap().as_str(), inbound);
272        assert_eq!(RpcCorrelationId::new(&inbound).unwrap().as_str(), inbound);
273        assert!(!std::mem::needs_drop::<TraceCorrelationId>());
274        assert!(!std::mem::needs_drop::<RpcCorrelationId>());
275    }
276
277    #[test]
278    fn identifiers_have_stable_display_forms() {
279        assert_eq!(TraceId::from_u128(42).to_string().len(), 32);
280        assert_eq!(SpanId::from_u64(42).to_string().len(), 16);
281        assert_eq!(ServiceId::from("orders").to_string(), "orders");
282    }
283
284    #[test]
285    fn opaque_trace_correlation_id_is_bounded_and_rejects_controls() {
286        assert_eq!(
287            TraceCorrelationId::new("trace-1").unwrap().as_str(),
288            "trace-1"
289        );
290        assert_eq!(
291            TraceCorrelationId::new(""),
292            Err(TraceCorrelationIdError::Empty)
293        );
294        assert_eq!(
295            TraceCorrelationId::new("x".repeat(MAX_TRACE_CORRELATION_ID_BYTES + 1)),
296            Err(TraceCorrelationIdError::TooLong)
297        );
298        assert_eq!(
299            TraceCorrelationId::new("trace\n1"),
300            Err(TraceCorrelationIdError::ControlCharacter)
301        );
302    }
303}