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, PartialEq)]
48pub struct RpcCorrelationId(Arc<str>);
49
50impl RpcCorrelationId {
51    pub fn new(value: &str) -> Option<Self> {
52        if value.trim().is_empty() || value.len() > 256 || value.chars().any(char::is_control) {
53            return None;
54        }
55        Some(Self(Arc::from(value)))
56    }
57
58    pub fn as_str(&self) -> &str {
59        &self.0
60    }
61}
62
63#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
64pub struct TraceId(u128);
65
66impl TraceId {
67    pub const fn from_u128(value: u128) -> Self {
68        Self(value)
69    }
70
71    pub const fn as_u128(self) -> u128 {
72        self.0
73    }
74}
75
76impl fmt::Display for TraceId {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(formatter, "{:032x}", self.0)
79    }
80}
81
82#[derive(Clone, Debug, Eq, Hash, PartialEq)]
83pub struct TraceCorrelationId(Arc<str>);
84
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum TraceCorrelationIdError {
87    Empty,
88    TooLong,
89    ControlCharacter,
90}
91
92impl TraceCorrelationId {
93    pub fn new(value: impl Into<Arc<str>>) -> Result<Self, TraceCorrelationIdError> {
94        let value = value.into();
95        if value.is_empty() {
96            return Err(TraceCorrelationIdError::Empty);
97        }
98        if value.len() > MAX_TRACE_CORRELATION_ID_BYTES {
99            return Err(TraceCorrelationIdError::TooLong);
100        }
101        if value.chars().any(char::is_control) {
102            return Err(TraceCorrelationIdError::ControlCharacter);
103        }
104        Ok(Self(value))
105    }
106
107    pub fn as_str(&self) -> &str {
108        &self.0
109    }
110}
111
112impl fmt::Display for TraceCorrelationId {
113    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
114        formatter.write_str(&self.0)
115    }
116}
117
118#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
119pub struct SpanId(u64);
120
121impl SpanId {
122    pub const fn from_u64(value: u64) -> Self {
123        Self(value)
124    }
125
126    pub const fn as_u64(self) -> u64 {
127        self.0
128    }
129}
130
131impl fmt::Display for SpanId {
132    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(formatter, "{:016x}", self.0)
134    }
135}
136
137/// The minimum identity propagated through Service, DB and Observability in V1.
138///
139/// Deadline, cancellation and resource-budget fields are deliberately absent
140/// until a version explicitly defines their behavior.
141#[derive(Clone, Debug, Eq, PartialEq)]
142pub struct CallContext {
143    application: ApplicationId,
144    module: ModuleId,
145    service: ServiceId,
146    operation: OperationId,
147    trace_id: TraceId,
148    trace_correlation_id: TraceCorrelationId,
149    span_id: SpanId,
150    rpc_correlation_id: Option<RpcCorrelationId>,
151}
152
153impl CallContext {
154    pub fn new(
155        application: ApplicationId,
156        module: ModuleId,
157        service: ServiceId,
158        operation: OperationId,
159        trace_id: TraceId,
160        span_id: SpanId,
161    ) -> Self {
162        let trace_correlation_id = TraceCorrelationId(Arc::from(trace_id.to_string()));
163        Self {
164            application,
165            module,
166            service,
167            operation,
168            trace_id,
169            trace_correlation_id,
170            span_id,
171            rpc_correlation_id: None,
172        }
173    }
174
175    pub fn with_trace_correlation_id(mut self, trace_correlation_id: TraceCorrelationId) -> Self {
176        self.trace_correlation_id = trace_correlation_id;
177        self
178    }
179
180    pub fn application(&self) -> &ApplicationId {
181        &self.application
182    }
183
184    pub fn with_rpc_correlation_id(mut self, rpc: Option<RpcCorrelationId>) -> Self {
185        self.rpc_correlation_id = rpc;
186        self
187    }
188
189    pub fn rpc_correlation_id(&self) -> Option<&RpcCorrelationId> {
190        self.rpc_correlation_id.as_ref()
191    }
192
193    pub fn module(&self) -> &ModuleId {
194        &self.module
195    }
196
197    pub fn service(&self) -> &ServiceId {
198        &self.service
199    }
200
201    pub fn operation(&self) -> &OperationId {
202        &self.operation
203    }
204
205    pub const fn trace_id(&self) -> TraceId {
206        self.trace_id
207    }
208
209    pub fn trace_correlation_id(&self) -> &TraceCorrelationId {
210        &self.trace_correlation_id
211    }
212
213    pub const fn span_id(&self) -> SpanId {
214        self.span_id
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn protocol_rpc_validation_does_not_mint_or_replace_identity() {
224        for value in ["0", "0.1", "0.12.3"] {
225            assert_eq!(RpcCorrelationId::new(value).unwrap().as_str(), value);
226        }
227        for value in ["", " ", "0\n1", "0\u{7f}"] {
228            assert!(RpcCorrelationId::new(value).is_none());
229        }
230        assert!(RpcCorrelationId::new(&"1".repeat(256)).is_some());
231        assert!(RpcCorrelationId::new(&"1".repeat(257)).is_none());
232    }
233
234    #[test]
235    fn identifiers_have_stable_display_forms() {
236        assert_eq!(TraceId::from_u128(42).to_string().len(), 32);
237        assert_eq!(SpanId::from_u64(42).to_string().len(), 16);
238        assert_eq!(ServiceId::from("orders").to_string(), "orders");
239    }
240
241    #[test]
242    fn opaque_trace_correlation_id_is_bounded_and_rejects_controls() {
243        assert_eq!(
244            TraceCorrelationId::new("trace-1").unwrap().as_str(),
245            "trace-1"
246        );
247        assert_eq!(
248            TraceCorrelationId::new(""),
249            Err(TraceCorrelationIdError::Empty)
250        );
251        assert_eq!(
252            TraceCorrelationId::new("x".repeat(MAX_TRACE_CORRELATION_ID_BYTES + 1)),
253            Err(TraceCorrelationIdError::TooLong)
254        );
255        assert_eq!(
256            TraceCorrelationId::new("trace\n1"),
257            Err(TraceCorrelationIdError::ControlCharacter)
258        );
259    }
260}