1#[cfg(all(not(feature = "std"), feature = "alloc"))]
18use alloc::string::{String, ToString};
19use core::fmt;
20use core::str::FromStr;
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Deserializer, Serialize};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum RequestIdError {
31 InvalidUuid(uuid::Error),
33}
34
35impl fmt::Display for RequestIdError {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::InvalidUuid(e) => write!(f, "invalid request ID: {e}"),
39 }
40 }
41}
42
43#[cfg(feature = "std")]
44impl std::error::Error for RequestIdError {
45 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
46 match self {
47 Self::InvalidUuid(e) => Some(e),
48 }
49 }
50}
51
52pub type RequestIdParseError = RequestIdError;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68#[cfg_attr(feature = "serde", derive(Serialize))]
69#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
70#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
71pub struct RequestId(uuid::Uuid);
72
73impl RequestId {
74 #[must_use]
83 pub fn new() -> Self {
84 Self(uuid::Uuid::new_v4())
85 }
86
87 #[must_use]
96 pub fn from_uuid(id: uuid::Uuid) -> Self {
97 Self(id)
98 }
99
100 #[must_use]
102 pub fn as_uuid(&self) -> uuid::Uuid {
103 self.0
104 }
105
106 #[must_use]
108 pub fn header_name(&self) -> &'static str {
109 "X-Request-Id"
110 }
111
112 #[must_use]
114 pub fn as_str(&self) -> String {
115 self.0.to_string()
116 }
117}
118
119#[cfg(feature = "std")]
124impl crate::header_id::HeaderId for RequestId {
125 const HEADER_NAME: &'static str = "X-Request-Id";
126
127 fn as_str(&self) -> std::borrow::Cow<'_, str> {
128 std::borrow::Cow::Owned(self.0.to_string())
129 }
130}
131
132#[cfg(all(not(feature = "std"), feature = "alloc"))]
133impl crate::header_id::HeaderId for RequestId {
134 const HEADER_NAME: &'static str = "X-Request-Id";
135
136 fn as_str(&self) -> alloc::borrow::Cow<'_, str> {
137 alloc::borrow::Cow::Owned(self.0.to_string())
138 }
139}
140
141impl Default for RequestId {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149impl fmt::Display for RequestId {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 fmt::Display::fmt(&self.0, f)
152 }
153}
154
155impl From<uuid::Uuid> for RequestId {
156 fn from(id: uuid::Uuid) -> Self {
157 Self(id)
158 }
159}
160
161impl From<RequestId> for uuid::Uuid {
162 fn from(r: RequestId) -> Self {
163 r.0
164 }
165}
166
167impl FromStr for RequestId {
168 type Err = RequestIdError;
169
170 fn from_str(s: &str) -> Result<Self, Self::Err> {
171 uuid::Uuid::parse_str(s)
172 .map(Self)
173 .map_err(RequestIdError::InvalidUuid)
174 }
175}
176
177impl TryFrom<&str> for RequestId {
178 type Error = RequestIdError;
179
180 fn try_from(s: &str) -> Result<Self, Self::Error> {
181 s.parse()
182 }
183}
184
185impl TryFrom<String> for RequestId {
186 type Error = RequestIdError;
187
188 fn try_from(s: String) -> Result<Self, Self::Error> {
189 s.parse()
190 }
191}
192
193#[cfg(feature = "serde")]
198impl<'de> Deserialize<'de> for RequestId {
199 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
200 let s = String::deserialize(deserializer)?;
201 s.parse::<Self>().map_err(serde::de::Error::custom)
202 }
203}
204
205#[cfg(feature = "axum")]
210impl<S: Send + Sync> axum::extract::FromRequestParts<S> for RequestId {
211 type Rejection = crate::error::ApiError;
212
213 async fn from_request_parts(
214 parts: &mut axum::http::request::Parts,
215 _state: &S,
216 ) -> Result<Self, Self::Rejection> {
217 let raw = parts
218 .headers
219 .get("x-request-id")
220 .ok_or_else(|| {
221 crate::error::ApiError::bad_request("missing required header: x-request-id")
222 })?
223 .to_str()
224 .map_err(|_| {
225 crate::error::ApiError::bad_request("header x-request-id contains non-UTF-8 bytes")
226 })?;
227 raw.parse::<Self>()
228 .map_err(|e| crate::error::ApiError::bad_request(format!("invalid X-Request-Id: {e}")))
229 }
230}
231
232#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn new_generates_v4() {
242 let id = RequestId::new();
243 assert_eq!(id.as_uuid().get_version_num(), 4);
244 }
245
246 #[test]
247 fn from_uuid_roundtrip() {
248 let uuid = uuid::Uuid::nil();
249 let id = RequestId::from_uuid(uuid);
250 assert_eq!(id.as_uuid(), uuid);
251 }
252
253 #[test]
254 fn display_is_hyphenated_uuid() {
255 let id = RequestId::from_uuid(uuid::Uuid::nil());
256 assert_eq!(id.to_string(), "00000000-0000-0000-0000-000000000000");
257 }
258
259 #[test]
260 fn header_name() {
261 let id = RequestId::new();
262 assert_eq!(id.header_name(), "X-Request-Id");
263 }
264
265 #[test]
266 fn from_str_valid() {
267 let s = "550e8400-e29b-41d4-a716-446655440000";
268 let id: RequestId = s.parse().unwrap();
269 assert_eq!(id.to_string(), s);
270 }
271
272 #[test]
273 fn from_str_invalid() {
274 assert!("not-a-uuid".parse::<RequestId>().is_err());
275 }
276
277 #[test]
278 fn try_from_str() {
279 let s = "00000000-0000-0000-0000-000000000000";
280 let id = RequestId::try_from(s).unwrap();
281 assert_eq!(id.to_string(), s);
282 }
283
284 #[test]
285 fn from_into_uuid() {
286 let uuid = uuid::Uuid::new_v4();
287 let id = RequestId::from(uuid);
288 let back: uuid::Uuid = id.into();
289 assert_eq!(back, uuid);
290 }
291
292 #[test]
293 fn default_generates_new() {
294 let id = RequestId::default();
295 assert_eq!(id.as_uuid().get_version_num(), 4);
296 }
297
298 #[cfg(feature = "serde")]
299 #[test]
300 fn serde_roundtrip() {
301 let id = RequestId::from_uuid(uuid::Uuid::nil());
302 let json = serde_json::to_string(&id).unwrap();
303 assert_eq!(json, r#""00000000-0000-0000-0000-000000000000""#);
304 let back: RequestId = serde_json::from_str(&json).unwrap();
305 assert_eq!(back, id);
306 }
307
308 #[cfg(feature = "serde")]
309 #[test]
310 fn serde_deserialize_invalid_rejects() {
311 let result: Result<RequestId, _> = serde_json::from_str(r#""not-a-uuid""#);
312 assert!(result.is_err());
313 }
314
315 #[test]
320 fn request_id_as_str() {
321 let id = RequestId::from_uuid(uuid::Uuid::nil());
322 assert_eq!(id.as_str(), "00000000-0000-0000-0000-000000000000");
323 }
324
325 #[test]
326 fn parse_error_display() {
327 let err = "not-a-uuid".parse::<RequestId>().unwrap_err();
328 let s = err.to_string();
329 assert!(s.contains("invalid request ID"));
330 }
331
332 #[cfg(feature = "std")]
333 #[test]
334 fn parse_error_source() {
335 use std::error::Error as _;
336 let err = "not-a-uuid".parse::<RequestId>().unwrap_err();
337 assert!(err.source().is_some());
338 }
339
340 #[test]
341 fn try_from_string_valid() {
342 let s = "550e8400-e29b-41d4-a716-446655440000".to_owned();
343 let id = RequestId::try_from(s).unwrap();
344 assert_eq!(id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
345 }
346}