api_bones/
correlation_id.rs1#[cfg(all(not(feature = "std"), feature = "alloc"))]
24use alloc::{
25 borrow::ToOwned,
26 string::{String, ToString},
27};
28use core::{fmt, ops::Deref, str::FromStr};
29#[cfg(feature = "serde")]
30use serde::{Deserialize, Deserializer, Serialize};
31use thiserror::Error;
32
33#[derive(Debug, Clone, PartialEq, Eq, Error)]
39pub enum CorrelationIdError {
40 #[error("correlation ID must not be empty")]
42 Empty,
43 #[error("correlation ID must not exceed 255 characters")]
45 TooLong,
46 #[error("correlation ID may only contain printable ASCII characters (0x20–0x7E)")]
48 InvalidChars,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
66#[cfg_attr(feature = "serde", derive(Serialize))]
67#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
68#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
69pub struct CorrelationId(String);
70
71impl CorrelationId {
72 pub fn new(s: impl AsRef<str>) -> Result<Self, CorrelationIdError> {
86 let s = s.as_ref();
87 if s.is_empty() {
88 return Err(CorrelationIdError::Empty);
89 }
90 if s.len() > 255 {
91 return Err(CorrelationIdError::TooLong);
92 }
93 if !s.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
94 return Err(CorrelationIdError::InvalidChars);
95 }
96 Ok(Self(s.to_owned()))
97 }
98
99 #[must_use]
108 pub fn new_uuid() -> Self {
109 Self(uuid::Uuid::new_v4().to_string())
111 }
112
113 #[must_use]
122 pub fn as_str(&self) -> &str {
123 &self.0
124 }
125
126 #[must_use]
135 pub fn into_string(self) -> String {
136 self.0
137 }
138
139 #[must_use]
148 pub fn header_name(&self) -> &'static str {
149 "X-Correlation-Id"
150 }
151}
152
153#[cfg(feature = "std")]
158impl crate::header_id::HeaderId for CorrelationId {
159 const HEADER_NAME: &'static str = "X-Correlation-Id";
160
161 fn as_str(&self) -> std::borrow::Cow<'_, str> {
162 std::borrow::Cow::Borrowed(&self.0)
163 }
164}
165
166#[cfg(all(not(feature = "std"), feature = "alloc"))]
167impl crate::header_id::HeaderId for CorrelationId {
168 const HEADER_NAME: &'static str = "X-Correlation-Id";
169
170 fn as_str(&self) -> alloc::borrow::Cow<'_, str> {
171 alloc::borrow::Cow::Borrowed(&self.0)
172 }
173}
174
175impl Deref for CorrelationId {
180 type Target = str;
181
182 fn deref(&self) -> &str {
183 &self.0
184 }
185}
186
187impl AsRef<str> for CorrelationId {
188 fn as_ref(&self) -> &str {
189 &self.0
190 }
191}
192
193impl fmt::Display for CorrelationId {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 f.write_str(&self.0)
196 }
197}
198
199impl FromStr for CorrelationId {
200 type Err = CorrelationIdError;
201
202 fn from_str(s: &str) -> Result<Self, Self::Err> {
203 Self::new(s)
204 }
205}
206
207impl TryFrom<String> for CorrelationId {
208 type Error = CorrelationIdError;
209
210 fn try_from(s: String) -> Result<Self, Self::Error> {
211 Self::new(s)
212 }
213}
214
215impl TryFrom<&str> for CorrelationId {
216 type Error = CorrelationIdError;
217
218 fn try_from(s: &str) -> Result<Self, Self::Error> {
219 Self::new(s)
220 }
221}
222
223#[cfg(feature = "serde")]
228impl<'de> Deserialize<'de> for CorrelationId {
229 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
230 let s = String::deserialize(deserializer)?;
231 Self::new(&s).map_err(serde::de::Error::custom)
232 }
233}
234
235#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn valid_id_is_accepted() {
245 assert!(CorrelationId::new("flow-abc").is_ok());
246 assert!(CorrelationId::new("x").is_ok());
247 assert!(CorrelationId::new("abc 123").is_ok()); }
249
250 #[test]
251 fn empty_is_rejected() {
252 assert_eq!(CorrelationId::new(""), Err(CorrelationIdError::Empty));
253 }
254
255 #[test]
256 fn too_long_is_rejected() {
257 let s: String = "a".repeat(256);
258 assert_eq!(CorrelationId::new(&s), Err(CorrelationIdError::TooLong));
259 }
260
261 #[test]
262 fn exactly_255_chars_is_accepted() {
263 let s: String = "a".repeat(255);
264 assert!(CorrelationId::new(&s).is_ok());
265 }
266
267 #[test]
268 fn control_char_is_rejected() {
269 assert_eq!(
270 CorrelationId::new("ab\x00c"),
271 Err(CorrelationIdError::InvalidChars)
272 );
273 }
274
275 #[test]
276 fn non_ascii_is_rejected() {
277 assert_eq!(
278 CorrelationId::new("héllo"),
279 Err(CorrelationIdError::InvalidChars)
280 );
281 }
282
283 #[test]
284 fn new_uuid_produces_valid_id() {
285 let id = CorrelationId::new_uuid();
286 assert_eq!(id.as_str().len(), 36);
287 assert!(CorrelationId::new(id.as_str()).is_ok());
288 }
289
290 #[test]
291 fn header_name() {
292 let id = CorrelationId::new("x").unwrap();
293 assert_eq!(id.header_name(), "X-Correlation-Id");
294 }
295
296 #[test]
297 fn display() {
298 let id = CorrelationId::new("corr-01").unwrap();
299 assert_eq!(format!("{id}"), "corr-01");
300 }
301
302 #[test]
303 fn deref_to_str() {
304 let id = CorrelationId::new("abc").unwrap();
305 let s: &str = &id;
306 assert_eq!(s, "abc");
307 }
308
309 #[test]
310 fn from_str() {
311 let id: CorrelationId = "corr-abc".parse().unwrap();
312 assert_eq!(id.as_str(), "corr-abc");
313 }
314
315 #[test]
316 fn try_from_str() {
317 assert!(CorrelationId::try_from("valid").is_ok());
318 assert!(CorrelationId::try_from("").is_err());
319 }
320
321 #[test]
322 fn try_from_string() {
323 assert!(CorrelationId::try_from("valid".to_owned()).is_ok());
324 }
325
326 #[test]
327 fn into_string() {
328 let id = CorrelationId::new("abc").unwrap();
329 assert_eq!(id.into_string(), "abc");
330 }
331
332 #[cfg(feature = "serde")]
333 #[test]
334 fn serde_roundtrip() {
335 let id = CorrelationId::new("corr-xyz-789").unwrap();
336 let json = serde_json::to_string(&id).unwrap();
337 assert_eq!(json, r#""corr-xyz-789""#);
338 let back: CorrelationId = serde_json::from_str(&json).unwrap();
339 assert_eq!(back, id);
340 }
341
342 #[cfg(feature = "serde")]
343 #[test]
344 fn serde_deserialize_invalid_rejects() {
345 let result: Result<CorrelationId, _> = serde_json::from_str(r#""""#);
346 assert!(result.is_err());
347 }
348
349 #[test]
350 fn as_ref_str() {
351 let id = CorrelationId::new("corr-ref").unwrap();
352 let s: &str = id.as_ref();
353 assert_eq!(s, "corr-ref");
354 }
355
356 #[test]
358 fn error_display_all_variants() {
359 assert!(!CorrelationIdError::Empty.to_string().is_empty());
360 assert!(!CorrelationIdError::TooLong.to_string().is_empty());
361 assert!(!CorrelationIdError::InvalidChars.to_string().is_empty());
362 }
363}