api_bones/header_id.rs
1//! Shared [`HeaderId`] trait for newtype wrappers that are transported via a
2//! dedicated HTTP header.
3//!
4//! All ID newtypes in this crate — [`crate::request_id::RequestId`],
5//! [`crate::correlation_id::CorrelationId`],
6//! [`crate::idempotency::IdempotencyKey`], and
7//! [`crate::traceparent::TraceContext`] — follow the same pattern:
8//!
9//! - They carry a fixed, well-known header name.
10//! - They expose their value as a string.
11//!
12//! `HeaderId` makes that pattern explicit and enables generic middleware or
13//! helper utilities that work over any of these types.
14//!
15//! # Example
16//!
17//! ```rust
18//! use api_bones::header_id::HeaderId;
19//! use api_bones::request_id::RequestId;
20//! use api_bones::correlation_id::CorrelationId;
21//! use api_bones::idempotency::IdempotencyKey;
22//! use api_bones::traceparent::TraceContext;
23//!
24//! fn header_name_of<T: HeaderId>() -> &'static str {
25//! T::HEADER_NAME
26//! }
27//!
28//! assert_eq!(header_name_of::<RequestId>(), "X-Request-Id");
29//! assert_eq!(header_name_of::<CorrelationId>(), "X-Correlation-Id");
30//! assert_eq!(header_name_of::<IdempotencyKey>(), "Idempotency-Key");
31//! assert_eq!(header_name_of::<TraceContext>(), "traceparent");
32//! ```
33
34#[cfg(all(not(feature = "std"), feature = "alloc"))]
35use alloc::borrow::Cow;
36#[cfg(feature = "std")]
37use std::borrow::Cow;
38
39// ---------------------------------------------------------------------------
40// HeaderId trait
41// ---------------------------------------------------------------------------
42
43/// A type that is transported as a single HTTP header value.
44///
45/// Implementors provide:
46/// - [`HEADER_NAME`](HeaderId::HEADER_NAME) — the canonical header name
47/// exactly as it appears in an HTTP request or response, e.g.
48/// `"X-Request-Id"`.
49/// - [`as_str`](HeaderId::as_str) — the string representation of the value,
50/// borrowed when the value is already stored as a `String`, or owned when
51/// derived on the fly (e.g. for UUID-backed or structured types).
52///
53/// # Generic middleware
54///
55/// The trait enables writing helpers that work uniformly over any header-ID
56/// type without duplicating the header-lookup logic:
57///
58/// ```rust
59/// use api_bones::header_id::HeaderId;
60///
61/// fn log_header<T: HeaderId>(value: &T) {
62/// println!("{}: {}", T::HEADER_NAME, value.as_str());
63/// }
64/// ```
65///
66/// In axum middleware you can use `T::HEADER_NAME` to look up the right header
67/// for any `T: HeaderId + FromStr`:
68///
69/// ```rust,ignore
70/// fn extract_header<T>(parts: &axum::http::request::Parts)
71/// -> Result<T, api_bones::ApiError>
72/// where
73/// T: HeaderId + core::str::FromStr,
74/// T::Err: core::fmt::Display,
75/// {
76/// let raw = parts
77/// .headers
78/// .get(T::HEADER_NAME)
79/// .ok_or_else(|| api_bones::ApiError::bad_request(
80/// format!("missing required header: {}", T::HEADER_NAME)
81/// ))?
82/// .to_str()
83/// .map_err(|_| api_bones::ApiError::bad_request(
84/// format!("header {} contains non-UTF-8 bytes", T::HEADER_NAME)
85/// ))?;
86/// raw.parse::<T>()
87/// .map_err(|e| api_bones::ApiError::bad_request(
88/// format!("invalid {}: {e}", T::HEADER_NAME)
89/// ))
90/// }
91/// ```
92#[cfg(any(feature = "std", feature = "alloc"))]
93pub trait HeaderId {
94 /// The canonical HTTP header name for this type.
95 ///
96 /// Examples: `"X-Request-Id"`, `"X-Correlation-Id"`,
97 /// `"Idempotency-Key"`, `"traceparent"`.
98 const HEADER_NAME: &'static str;
99
100 /// Return the string representation of this header value.
101 ///
102 /// Returns a [`Cow::Borrowed`] slice when the value is already stored as a
103 /// `String`, and a [`Cow::Owned`] string when the representation must be
104 /// computed (e.g. for UUID-backed or structured types).
105 fn as_str(&self) -> Cow<'_, str>;
106}
107
108// ---------------------------------------------------------------------------
109// Tests
110// ---------------------------------------------------------------------------
111
112#[cfg(all(test, feature = "uuid"))]
113mod tests {
114 use super::*;
115 use crate::correlation_id::CorrelationId;
116 use crate::idempotency::IdempotencyKey;
117 use crate::request_id::RequestId;
118 use crate::traceparent::TraceContext;
119
120 #[test]
121 fn header_names() {
122 assert_eq!(RequestId::HEADER_NAME, "X-Request-Id");
123 assert_eq!(CorrelationId::HEADER_NAME, "X-Correlation-Id");
124 assert_eq!(IdempotencyKey::HEADER_NAME, "Idempotency-Key");
125 assert_eq!(TraceContext::HEADER_NAME, "traceparent");
126 }
127
128 // Use a generic helper to force dispatch through the HeaderId trait,
129 // bypassing any inherent as_str() methods that would otherwise shadow it.
130 fn trait_as_str<T: HeaderId>(v: &T) -> Cow<'_, str> {
131 v.as_str()
132 }
133
134 #[test]
135 fn as_str_request_id() {
136 let id = RequestId::from_uuid(uuid::Uuid::nil());
137 assert_eq!(trait_as_str(&id), "00000000-0000-0000-0000-000000000000");
138 }
139
140 #[test]
141 fn as_str_correlation_id() {
142 let id = CorrelationId::new("corr-abc").unwrap();
143 assert_eq!(trait_as_str(&id), "corr-abc");
144 }
145
146 #[test]
147 fn as_str_idempotency_key() {
148 let key = IdempotencyKey::new("my-key").unwrap();
149 assert_eq!(trait_as_str(&key), "my-key");
150 }
151
152 #[test]
153 fn as_str_trace_context() {
154 let tc: TraceContext = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
155 .parse()
156 .unwrap();
157 assert_eq!(
158 trait_as_str(&tc),
159 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
160 );
161 }
162
163 #[test]
164 fn header_name_instance_methods() {
165 let key = IdempotencyKey::new("x").unwrap();
166 assert_eq!(key.header_name(), "Idempotency-Key");
167 let tc = TraceContext::new();
168 assert_eq!(tc.header_name(), "traceparent");
169 }
170
171 #[test]
172 fn generic_header_name_fn() {
173 fn header_name_of<T: HeaderId>() -> &'static str {
174 T::HEADER_NAME
175 }
176 assert_eq!(header_name_of::<RequestId>(), "X-Request-Id");
177 assert_eq!(header_name_of::<CorrelationId>(), "X-Correlation-Id");
178 assert_eq!(header_name_of::<IdempotencyKey>(), "Idempotency-Key");
179 assert_eq!(header_name_of::<TraceContext>(), "traceparent");
180 }
181}