Skip to main content

http_smtp_rele/
request_id.rs

1//! Application-owned request identifier.
2//!
3//! Implements RFC 036/086: `request_id` is an opaque, application-owned identifier.
4//! The canonical external format is `req_` followed by a ULID.
5//!
6//! Clients must treat `request_id` as an opaque string.
7//! They must not parse the timestamp component or depend on internal format details.
8
9use std::{fmt, str::FromStr};
10
11use serde::{Deserialize, Serialize};
12use ulid::Ulid;
13
14/// An opaque, application-owned submission request identifier.
15///
16/// External format: `req_` + ULID (26 uppercase base-32 characters).
17/// Example: `req_01HX7Q9V6R6W9V8Y5E3E6E7M9A`
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(transparent)]
20pub struct RequestId(String);
21
22impl RequestId {
23    /// Generate a new, unique `RequestId`.
24    pub fn new() -> Self {
25        RequestId(format!("req_{}", Ulid::new()))
26    }
27
28    /// Return the canonical string representation.
29    pub fn as_str(&self) -> &str {
30        &self.0
31    }
32}
33
34impl Default for RequestId {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl fmt::Display for RequestId {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        f.write_str(&self.0)
43    }
44}
45
46#[derive(Debug, PartialEq, Eq)]
47pub struct RequestIdParseError;
48
49impl fmt::Display for RequestIdParseError {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.write_str("invalid request_id: must be req_<ULID>")
52    }
53}
54
55impl FromStr for RequestId {
56    type Err = RequestIdParseError;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        let ulid_part = s.strip_prefix("req_").ok_or(RequestIdParseError)?;
60        Ulid::from_string(ulid_part).map_err(|_| RequestIdParseError)?;
61        Ok(RequestId(s.to_string()))
62    }
63}
64
65// Implement AsRef<str> for use as HashMap key etc.
66impl AsRef<str> for RequestId {
67    fn as_ref(&self) -> &str {
68        &self.0
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn new_id_has_req_prefix() {
78        let id = RequestId::new();
79        assert!(id.as_str().starts_with("req_"), "got: {id}");
80    }
81
82    #[test]
83    fn display_roundtrips_via_fromstr() {
84        let id = RequestId::new();
85        let s = id.to_string();
86        let parsed: RequestId = s.parse().unwrap();
87        assert_eq!(id, parsed);
88    }
89
90    #[test]
91    fn plain_uuid_rejected() {
92        let result = "550e8400-e29b-41d4-a716-446655440000".parse::<RequestId>();
93        assert!(result.is_err());
94    }
95
96    #[test]
97    fn wrong_prefix_rejected() {
98        let result = "id_01HX7Q9V6R6W9V8Y5E3E6E7M9A".parse::<RequestId>();
99        assert!(result.is_err());
100    }
101
102    #[test]
103    fn two_ids_are_unique() {
104        assert_ne!(RequestId::new(), RequestId::new());
105    }
106}