Skip to main content

rama_http_headers/exotic/
x_clacks_overhead.rs

1//! module for [`XClacksOverhead`]
2
3use std::fmt;
4use std::str::FromStr;
5
6use rama_utils::time::now_unix_ms;
7
8use crate::util::HeaderValueString;
9
10#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
11/// X-Clacks-Overhead header implementation
12///
13/// A non-standardised HTTP header based upon the fictional work of the late, great,
14/// Sir Terry Pratchett. The header commemorates influential figures in computing
15/// and technology by cycling through a predefined list of names.
16///
17/// Use the [`XClacksOverhead::new`] constructor when using it with a response header
18/// adder layer or something similar. This way you get each response a different header.
19///
20/// # Credits
21///
22/// Original implementation inspired by work from Xe.
23/// See: <https://xclacksoverhead.org/home/about>
24///
25/// # Examples
26///
27/// ```
28/// use rama_http_headers::exotic::XClacksOverhead;
29///
30/// // Time-based rotation through commemorated names
31/// let header = XClacksOverhead::new();
32///
33/// // Parse from a string
34/// let header: XClacksOverhead = "GNU Terry Pratchett".parse().unwrap();
35///
36/// // Compile-time constant
37/// let header = XClacksOverhead::from_static("GNU Dennis Ritchie");
38/// ```
39pub struct XClacksOverhead(HeaderValueString);
40
41derive_header! {
42    XClacksOverhead(_),
43    name: X_CLACKS_OVERHEAD
44}
45
46macro_rules! name_list {
47    ($($name:literal),+ $(,)?) => {
48        const NAMES: &[&str] = &[
49            $(
50                concat!("GNU ", $name),
51            )+
52        ];
53    };
54}
55
56name_list![
57    "Karen Sparck Jones",
58    "Grant Imahara",
59    "Douglas Adams",
60    "Ian Murdock",
61    "Sir Terry Pratchett",
62    "Kevin Mitnick",
63    "Radia Perlman",
64    "Sophie Wilson",
65    "Grace Hopper",
66    "Terry Davis",
67    "Paul Allen",
68    "Edsger Dijkstra",
69    "Joe Armstrong",
70    "David Bowie",
71    "Barbara Liskov",
72    "Kris Nova",
73    "Alan Turing",
74    "Sir Clive Sinclair",
75    "Ada Lovelace",
76    "John Conway",
77    "Satoru Iwata",
78    "Dennis Ritchie",
79    "Ruth Bader Ginsburg",
80    "Matt Trout",
81    "Bram Moolenaar",
82    "Aaron Swartz",
83    "Steven Hawking",
84];
85
86impl XClacksOverhead {
87    /// Construct a new `XClacksOverhead` header with a name selected based on current epoch time.
88    ///
89    /// The name changes once per day (UTC).
90    #[must_use]
91    pub fn new() -> Self {
92        Self::new_with_index(now_unix_ms().wrapping_abs() as usize)
93    }
94
95    #[inline(always)]
96    fn new_with_index(n: usize) -> Self {
97        let index = n % NAMES.len();
98        Self(HeaderValueString::from_static(NAMES[index]))
99    }
100
101    /// Construct an `XClacksOverhead` from a static string.
102    ///
103    /// # Panic
104    ///
105    /// Panics if the static string is not a legal header value.
106    #[must_use]
107    pub const fn from_static(s: &'static str) -> Self {
108        Self(HeaderValueString::from_static(s))
109    }
110
111    pub fn as_str(&self) -> &str {
112        self.0.as_str()
113    }
114}
115
116rama_utils::macros::error::static_str_error! {
117    #[doc = "invalid X-Clacks-Overhead header value"]
118    pub struct InvalidXClacksOverhead;
119}
120
121impl FromStr for XClacksOverhead {
122    type Err = InvalidXClacksOverhead;
123
124    fn from_str(src: &str) -> Result<Self, Self::Err> {
125        HeaderValueString::from_str(src)
126            .map(XClacksOverhead)
127            .map_err(|_e| InvalidXClacksOverhead)
128    }
129}
130
131impl Default for XClacksOverhead {
132    #[inline(always)]
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138impl fmt::Display for XClacksOverhead {
139    #[inline(always)]
140    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141        fmt::Display::fmt(&self.0, f)
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::HeaderEncode;
149
150    use ahash::{HashSet, HashSetExt as _};
151
152    fn test_value(value: &XClacksOverhead) -> String {
153        _ = value.encode_to_value();
154
155        let s = value.to_string();
156        _ = XClacksOverhead::from_str(&s).unwrap();
157
158        s
159    }
160
161    #[test]
162    fn test_new() {
163        let value = XClacksOverhead::new();
164        _ = test_value(&value);
165    }
166
167    #[test]
168    fn test_default() {
169        let value = XClacksOverhead::default();
170        _ = test_value(&value);
171    }
172
173    #[test]
174    fn test_random_values() {
175        let mut unique_values = HashSet::new();
176        for index in 0..NAMES.len() * 2 {
177            let value = XClacksOverhead::new_with_index(index);
178            let s = test_value(&value);
179            unique_values.insert(s);
180        }
181        assert_eq!(NAMES.len(), unique_values.len());
182    }
183}