1macro_rules! client_hint {
2 (
3 #[doc = $ch_doc:literal]
4 pub enum ClientHint {
5 $(
6 #[doc = $doc:literal]
7 $name:ident($($str:literal),*),
8 )+
9 }
10 ) => {
11 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12 pub enum ClientHint {
13 $(
14 #[doc = $doc]
15 $name,
16 )+
17 }
18
19 impl ClientHint {
20 #[doc = "Checks if the client hint is low entropy, meaning that it will be send by default."]
21 pub fn is_low_entropy(&self) -> bool {
22 matches!(self, Self::SaveData | Self::Ua | Self::Mobile | Self::Platform)
23 }
24
25 #[inline]
26 #[doc = "Attempts to convert a `HeaderName` to a `ClientHint`."]
27 pub fn match_header_name(name: &::rama_http_types::HeaderName) -> Option<Self> {
28 name.try_into().ok()
29 }
30
31 #[doc = "Return an iterator of all header names for this client hint."]
32 pub fn iter_header_names(&self) -> impl Iterator<Item = ::rama_http_types::HeaderName> {
33 match self {
34 $(
35 Self::$name => vec![$(::rama_http_types::HeaderName::from_static($str),)+].into_iter(),
36 )+
37 }
38 }
39
40 #[doc = "Returns the preferred string representation of the client hint."]
41 pub fn as_str(&self) -> &'static str {
42 match self {
43 $(
44 Self::$name => {
45 const VARIANTS: &'static [&'static str] = &[$($str,)+];
46 VARIANTS[0]
47 },
48 )+
49 }
50 }
51 }
52
53 rama_utils::macros::error::static_str_error! {
54 pub struct ClientHintParsingError;
56 }
57
58 impl TryFrom<&str> for ClientHint {
59 type Error = ClientHintParsingError;
60
61 fn try_from(name: &str) -> Result<Self, Self::Error> {
62 rama_utils::macros::match_ignore_ascii_case_str! {
63 match (name) {
64 $(
65 $($str)|+ => Ok(Self::$name),
66 )+
67 _ => Err(ClientHintParsingError),
68 }
69 }
70 }
71 }
72
73 impl TryFrom<String> for ClientHint {
74 type Error = ClientHintParsingError;
75
76 fn try_from(name: String) -> Result<Self, Self::Error> {
77 Self::try_from(name.as_str())
78 }
79 }
80
81 impl TryFrom<::rama_http_types::HeaderName> for ClientHint {
82 type Error = ClientHintParsingError;
83
84 fn try_from(name: ::rama_http_types::HeaderName) -> Result<Self, Self::Error> {
85 Self::try_from(name.as_str())
86 }
87 }
88
89 impl TryFrom<&::rama_http_types::HeaderName> for ClientHint {
90 type Error = ClientHintParsingError;
91
92 fn try_from(name: &::rama_http_types::HeaderName) -> Result<Self, Self::Error> {
93 Self::try_from(name.as_str())
94 }
95 }
96
97 impl std::str::FromStr for ClientHint {
98 type Err = ClientHintParsingError;
99
100 #[inline]
101 fn from_str(s: &str) -> Result<Self, Self::Err> {
102 Self::try_from(s)
103 }
104 }
105
106 impl std::fmt::Display for ClientHint {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(f, "{}", self.as_str())
109 }
110 }
111
112 impl serde::Serialize for ClientHint {
113 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
114 where
115 S: serde::Serializer,
116 {
117 serializer.serialize_str(self.as_str())
118 }
119 }
120
121 impl<'de> serde::Deserialize<'de> for ClientHint {
122 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
123 where
124 D: serde::Deserializer<'de>,
125 {
126 use serde::de::Error;
127 let s = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
128 Self::try_from(s.as_ref()).map_err(D::Error::custom)
129 }
130 }
131
132 #[doc = "Returns an iterator over all client hints."]
133 pub fn all_client_hints() -> impl Iterator<Item = ClientHint> {
134 [
135 $(
136 ClientHint::$name,
137 )+
138 ].into_iter()
139 }
140
141 #[doc = "Returns an iterator over all client hint header name strings."]
142 pub fn all_client_hint_header_name_strings() -> impl Iterator<Item = &'static str> {
143 [
144 $(
145 $($str,)+
146 )+
147 ].into_iter()
148 }
149
150 #[doc = "Returns an iterator over all client hint header names."]
151 pub fn all_client_hint_header_names() -> impl Iterator<Item = ::rama_http_types::HeaderName> {
152 all_client_hint_header_name_strings().map(::rama_http_types::HeaderName::from_static)
153 }
154 };
155}
156
157client_hint! {
162 #[doc = "Client Hints are a set of HTTP Headers and a JavaScript API that allow web browsers to send detailed information about the client device and browser to web servers. They are designed to be a successor to User-Agent, and provide a standardized way for web servers to optimize content for the client without relying on unreliable user-agent string-based detection or browser fingerprinting techniques."]
163 pub enum ClientHint {
164 Ua("sec-ch-ua"),
166 FullVersion("sec-ch-ua-full-version"),
168 FullVersionList("sec-ch-ua-full-version-list"),
170 Platform("sec-ch-ua-platform"),
172 PlatformVersion("sec-ch-ua-platform-version"),
174 Arch("sec-ch-ua-arch"),
176 Bitness("sec-ch-ua-bitness"),
178 Wow64("sec-ch-ua-wow64"),
180 Model("sec-ch-ua-model"),
182 Mobile("sec-ch-ua-mobile"),
184 FormFactor("sec-ch-ua-form-factors"),
186 Lang("sec-ch-lang", "lang"),
188 SaveData("sec-ch-save-data", "save-data"),
190 Width("sec-ch-width"),
192 ViewportWidth("sec-ch-viewport-width", "viewport-width"),
194 ViewportHeight("sec-ch-viewport-height"),
196 Dpr("sec-ch-dpr", "dpr"),
198 DeviceMemory("sec-ch-device-memory", "device-memory"),
200 Rtt("sec-ch-rtt", "rtt"),
202 Downlink("sec-ch-downlink", "downlink"),
204 Ect("sec-ch-ect", "ect"),
206 PrefersColorScheme("sec-ch-prefers-color-scheme"),
208 PrefersReducedMotion("sec-ch-prefers-reduced-motion"),
210 PrefersReducedTransparency("sec-ch-prefers-reduced-transparency"),
212 PrefersContrast("sec-ch-prefers-contrast"),
214 ForcedColors("sec-ch-forced-colors"),
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_client_hint_ua_from_str() {
225 let hint = ClientHint::try_from("Sec-CH-UA").unwrap();
226 assert_eq!(hint, ClientHint::Ua);
227 }
228
229 #[test]
230 fn test_client_hint_ua_from_str_lowercase() {
231 let hint = ClientHint::try_from("sec-ch-ua").unwrap();
232 assert_eq!(hint, ClientHint::Ua);
233 }
234
235 #[test]
236 fn test_client_hint_ua_from_str_uppercase() {
237 let hint = ClientHint::try_from("SEC-CH-UA").unwrap();
238 assert_eq!(hint, ClientHint::Ua);
239 }
240
241 #[test]
242 fn test_client_hint_ua_from_str_mixedcase() {
243 let hint = ClientHint::try_from("Sec-CH-UA").unwrap();
244 assert_eq!(hint, ClientHint::Ua);
245 }
246
247 #[test]
248 fn test_client_hint_low_entropy() {
249 let hints = [
250 "Sec-CH-UA",
251 "Sec-CH-UA-Mobile",
252 "Sec-CH-UA-Platform",
253 "Save-Data",
254 "Sec-CH-Save-Data",
255 ];
256
257 for hint in hints {
258 let hint = ClientHint::try_from(hint).expect(hint);
259 assert!(hint.is_low_entropy());
260 }
261 }
262
263 #[test]
264 fn test_client_hint_high_entropy() {
265 let hints = [
266 "Sec-CH-UA-Full-Version",
267 "Sec-CH-UA-Full-Version-List",
268 "Sec-CH-UA-Platform-Version",
269 "Sec-CH-UA-Arch",
270 "Sec-CH-UA-Bitness",
271 "Sec-CH-UA-WoW64",
272 "Sec-CH-UA-Model",
273 "Sec-CH-UA-Form-Factors",
274 "Sec-CH-Width",
275 "Sec-CH-Viewport-Width",
276 "Sec-CH-Viewport-Height",
277 "Sec-CH-DPR",
278 "Sec-CH-Device-Memory",
279 "Sec-CH-RTT",
280 "Sec-CH-Downlink",
281 "Sec-CH-ECT",
282 "Sec-CH-Prefers-Color-Scheme",
283 "Sec-CH-Prefers-Reduced-Motion",
284 "Sec-CH-Prefers-Reduced-Transparency",
285 "Sec-CH-Prefers-Contrast",
286 "Sec-CH-Forced-Colors",
287 ];
288
289 for hint in hints {
290 let hint = ClientHint::try_from(hint).expect(hint);
291 assert!(!hint.is_low_entropy());
292 }
293 }
294
295 #[test]
296 fn test_all_client_hint_header_name_strings_contains_some_hints() {
297 let strings = all_client_hint_header_name_strings().collect::<Vec<_>>();
298 assert!(strings.contains(&"sec-ch-ua"), "{:?}", strings);
299 }
300
301 #[test]
302 fn test_all_client_hint_header_names() {
303 let names = all_client_hint_header_names().collect::<Vec<_>>();
304 let strings = all_client_hint_header_name_strings().collect::<Vec<_>>();
305 assert_eq!(names.len(), strings.len());
306 for (name, string) in names.iter().zip(strings.iter()) {
307 assert_eq!(name.as_str(), *string);
308 }
309 }
310}