#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::string::{String, ToString};
use core::fmt;
use core::str::FromStr;
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequestIdError {
InvalidUuid(uuid::Error),
}
impl fmt::Display for RequestIdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidUuid(e) => write!(f, "invalid request ID: {e}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for RequestIdError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidUuid(e) => Some(e),
}
}
}
pub type RequestIdParseError = RequestIdError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RequestId(uuid::Uuid);
impl RequestId {
#[must_use]
pub fn new() -> Self {
Self(uuid::Uuid::new_v4())
}
#[must_use]
pub fn from_uuid(id: uuid::Uuid) -> Self {
Self(id)
}
#[must_use]
pub fn as_uuid(&self) -> uuid::Uuid {
self.0
}
#[must_use]
pub fn header_name(&self) -> &'static str {
"X-Request-Id"
}
#[must_use]
pub fn as_str(&self) -> String {
self.0.to_string()
}
}
#[cfg(feature = "std")]
impl crate::header_id::HeaderId for RequestId {
const HEADER_NAME: &'static str = "X-Request-Id";
fn as_str(&self) -> std::borrow::Cow<'_, str> {
std::borrow::Cow::Owned(self.0.to_string())
}
}
#[cfg(all(not(feature = "std"), feature = "alloc"))]
impl crate::header_id::HeaderId for RequestId {
const HEADER_NAME: &'static str = "X-Request-Id";
fn as_str(&self) -> alloc::borrow::Cow<'_, str> {
alloc::borrow::Cow::Owned(self.0.to_string())
}
}
impl Default for RequestId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for RequestId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl From<uuid::Uuid> for RequestId {
fn from(id: uuid::Uuid) -> Self {
Self(id)
}
}
impl From<RequestId> for uuid::Uuid {
fn from(r: RequestId) -> Self {
r.0
}
}
impl FromStr for RequestId {
type Err = RequestIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
uuid::Uuid::parse_str(s)
.map(Self)
.map_err(RequestIdError::InvalidUuid)
}
}
impl TryFrom<&str> for RequestId {
type Error = RequestIdError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl TryFrom<String> for RequestId {
type Error = RequestIdError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for RequestId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse::<Self>().map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "axum")]
impl<S: Send + Sync> axum::extract::FromRequestParts<S> for RequestId {
type Rejection = crate::error::ApiError;
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
let raw = parts
.headers
.get("x-request-id")
.ok_or_else(|| {
crate::error::ApiError::bad_request("missing required header: x-request-id")
})?
.to_str()
.map_err(|_| {
crate::error::ApiError::bad_request("header x-request-id contains non-UTF-8 bytes")
})?;
raw.parse::<Self>()
.map_err(|e| crate::error::ApiError::bad_request(format!("invalid X-Request-Id: {e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_generates_v4() {
let id = RequestId::new();
assert_eq!(id.as_uuid().get_version_num(), 4);
}
#[test]
fn from_uuid_roundtrip() {
let uuid = uuid::Uuid::nil();
let id = RequestId::from_uuid(uuid);
assert_eq!(id.as_uuid(), uuid);
}
#[test]
fn display_is_hyphenated_uuid() {
let id = RequestId::from_uuid(uuid::Uuid::nil());
assert_eq!(id.to_string(), "00000000-0000-0000-0000-000000000000");
}
#[test]
fn header_name() {
let id = RequestId::new();
assert_eq!(id.header_name(), "X-Request-Id");
}
#[test]
fn from_str_valid() {
let s = "550e8400-e29b-41d4-a716-446655440000";
let id: RequestId = s.parse().unwrap();
assert_eq!(id.to_string(), s);
}
#[test]
fn from_str_invalid() {
assert!("not-a-uuid".parse::<RequestId>().is_err());
}
#[test]
fn try_from_str() {
let s = "00000000-0000-0000-0000-000000000000";
let id = RequestId::try_from(s).unwrap();
assert_eq!(id.to_string(), s);
}
#[test]
fn from_into_uuid() {
let uuid = uuid::Uuid::new_v4();
let id = RequestId::from(uuid);
let back: uuid::Uuid = id.into();
assert_eq!(back, uuid);
}
#[test]
fn default_generates_new() {
let id = RequestId::default();
assert_eq!(id.as_uuid().get_version_num(), 4);
}
#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip() {
let id = RequestId::from_uuid(uuid::Uuid::nil());
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, r#""00000000-0000-0000-0000-000000000000""#);
let back: RequestId = serde_json::from_str(&json).unwrap();
assert_eq!(back, id);
}
#[cfg(feature = "serde")]
#[test]
fn serde_deserialize_invalid_rejects() {
let result: Result<RequestId, _> = serde_json::from_str(r#""not-a-uuid""#);
assert!(result.is_err());
}
#[test]
fn request_id_as_str() {
let id = RequestId::from_uuid(uuid::Uuid::nil());
assert_eq!(id.as_str(), "00000000-0000-0000-0000-000000000000");
}
#[test]
fn parse_error_display() {
let err = "not-a-uuid".parse::<RequestId>().unwrap_err();
let s = err.to_string();
assert!(s.contains("invalid request ID"));
}
#[cfg(feature = "std")]
#[test]
fn parse_error_source() {
use std::error::Error as _;
let err = "not-a-uuid".parse::<RequestId>().unwrap_err();
assert!(err.source().is_some());
}
#[test]
fn try_from_string_valid() {
let s = "550e8400-e29b-41d4-a716-446655440000".to_owned();
let id = RequestId::try_from(s).unwrap();
assert_eq!(id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
}
}