1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
use crate::blueprint::constructor::{Constructor, RegisteredConstructor};
use crate::blueprint::Blueprint;
use crate::f;
use crate::http::HeaderValue;
use std::fmt::Formatter;
use std::hash::{Hash, Hasher};
use type_safe_id::{StaticType, TypeSafeId};
use uuid::Uuid;

/// A unique identifier generated for each incoming request.
///
/// It is a [TypeId](https://github.com/jetify-com/typeid).
/// The prefix is always `sri`, followed by an underscore and UUIDv7 in base32 encoding.
/// For example:
///
/// ```text
///  sri_2x4y6z8a0b1c2d3e4f5g6h7j8k
///  └─┘ └────────────────────────┘
/// prefix    uuid suffix (base32)
/// ```
///
/// # Example
///
/// ```rust
/// use http::{HeaderName, HeaderValue};
/// use pavex::response::Response;
/// use pavex::telemetry::ServerRequestId;
///
/// pub async fn request_handler(request_id: ServerRequestId) -> Response {
///     // Add the request id as a header on the outgoing response
///     Response::ok()
///         .insert_header(
///             HeaderName::from_static("X-Request-Id"),
///             request_id.header_value()
///         )
/// }
/// ```
#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
pub struct ServerRequestId(TypeSafeId<SriTag>);

impl Hash for ServerRequestId {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.0.uuid().hash(state)
    }
}

#[derive(Default, Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct SriTag;

impl StaticType for SriTag {
    const TYPE: &'static str = "sri";
}

impl ServerRequestId {
    /// The default constructor for [`ServerRequestId`].
    ///
    /// It generates a new request id using a [UUID v7](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-04#name-uuid-version-7),
    /// with a random number and the current (UNIX) timestamp.
    pub fn generate() -> Self {
        Self(TypeSafeId::from_uuid(Uuid::now_v7()))
    }

    /// Access the underlying UUID.
    pub fn inner(&self) -> Uuid {
        self.0.uuid()
    }

    /// Return a [hyphenated representation](https://docs.rs/uuid/1.7.0/uuid/struct.Uuid.html#formatting)
    /// of [`ServerRequestId`] to be used as a [`HeaderValue`].
    pub fn header_value(&self) -> HeaderValue {
        // It is always a valid header value.
        self.0.to_string().try_into().unwrap()
    }
}

impl From<Uuid> for ServerRequestId {
    fn from(value: Uuid) -> Self {
        Self(TypeSafeId::from_uuid(value))
    }
}

impl std::fmt::Display for ServerRequestId {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", &self.0)
    }
}

/// Registration helpers.
impl ServerRequestId {
    /// Register the [default constructor](Self::generate)
    /// for [`ServerRequestId`] with a [`Blueprint`].
    pub fn register(bp: &mut Blueprint) -> RegisteredConstructor {
        Self::default_constructor().register(bp)
    }

    /// The [default constructor](Self::generate) for [`ServerRequestId`].
    pub fn default_constructor() -> Constructor {
        Constructor::request_scoped(f!(super::ServerRequestId::generate))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn server_request_id() {
        let request_id = ServerRequestId::generate();
        let header_value = request_id.header_value();
        let header_value = header_value.to_str().unwrap();
        assert!(header_value.starts_with("sri_"));
        // Prefix + `_` + UUID in base32 encoding
        assert_eq!(header_value.len(), 3 + 1 + 26)
    }
}