1use std::time::Duration;
2
3use rama_http_types::{HeaderName, HeaderValue};
4use rama_utils::collections::NonEmptySmallVec;
5
6use crate::util::{self, IterExt};
7use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader};
8
9macro_rules! client_hint {
10 (
11 #[doc = $ch_doc:literal]
12 pub enum ClientHint {
13 $(
14 #[doc = $doc:literal]
15 $name:ident($($str:literal),*),
16 )+
17 }
18 ) => {
19 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20 pub enum ClientHint {
21 $(
22 #[doc = $doc]
23 $name,
24 )+
25 }
26
27 impl ClientHint {
28 #[doc = "Checks if the client hint is low entropy, meaning that it will be send by default."]
29 #[must_use] pub fn is_low_entropy(&self) -> bool {
30 matches!(self, Self::SaveData | Self::Ua | Self::Mobile | Self::Platform)
31 }
32
33 #[inline]
34 #[doc = "Attempts to convert a `HeaderName` to a `ClientHint`."]
35 pub fn match_header_name(name: &::rama_http_types::HeaderName) -> Option<Self> {
36 name.try_into().ok()
37 }
38
39 #[doc = "All header-name spellings this client hint answers to, as borrowed `'static` strs: the preferred (`Sec-CH-` prefixed) form first, followed by any legacy bare alias (e.g. `[\"sec-ch-ect\", \"ect\"]`). Allocation-free — the slice is statically promoted."]
40 #[must_use] pub fn header_name_strs(&self) -> &'static [&'static str] {
41 match self {
42 $(
43 Self::$name => {
44 const NAMES: &[&str] = &[$($str,)+];
45 NAMES
46 },
47 )+
48 }
49 }
50
51 #[doc = "Return an iterator of all header names for this client hint. Allocation-free."]
52 pub fn iter_header_names(&self) -> impl Iterator<Item = ::rama_http_types::HeaderName> {
53 self.header_name_strs()
54 .iter()
55 .copied()
56 .map(::rama_http_types::HeaderName::from_static)
57 }
58
59 #[doc = "Returns the preferred string representation of the client hint."]
60 #[must_use] pub fn as_str(&self) -> &'static str {
61 self.header_name_strs()[0]
62 }
63 }
64
65 rama_utils::macros::error::static_str_error! {
66 pub struct ClientHintParsingError;
68 }
69
70 impl TryFrom<&str> for ClientHint {
71 type Error = ClientHintParsingError;
72
73 fn try_from(name: &str) -> Result<Self, Self::Error> {
74 rama_utils::macros::match_ignore_ascii_case_str! {
75 match (name) {
76 $(
77 $($str)|+ => Ok(Self::$name),
78 )+
79 _ => Err(ClientHintParsingError),
80 }
81 }
82 }
83 }
84
85 impl TryFrom<String> for ClientHint {
86 type Error = ClientHintParsingError;
87
88 fn try_from(name: String) -> Result<Self, Self::Error> {
89 Self::try_from(name.as_str())
90 }
91 }
92
93 impl TryFrom<::rama_http_types::HeaderName> for ClientHint {
94 type Error = ClientHintParsingError;
95
96 fn try_from(name: ::rama_http_types::HeaderName) -> Result<Self, Self::Error> {
97 Self::try_from(name.as_str())
98 }
99 }
100
101 impl TryFrom<&::rama_http_types::HeaderName> for ClientHint {
102 type Error = ClientHintParsingError;
103
104 fn try_from(name: &::rama_http_types::HeaderName) -> Result<Self, Self::Error> {
105 Self::try_from(name.as_str())
106 }
107 }
108
109 impl std::str::FromStr for ClientHint {
110 type Err = ClientHintParsingError;
111
112 #[inline]
113 fn from_str(s: &str) -> Result<Self, Self::Err> {
114 Self::try_from(s)
115 }
116 }
117
118 impl std::fmt::Display for ClientHint {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 write!(f, "{}", self.as_str())
121 }
122 }
123
124 impl serde::Serialize for ClientHint {
125 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
126 where
127 S: serde::Serializer,
128 {
129 serializer.serialize_str(self.as_str())
130 }
131 }
132
133 impl<'de> serde::Deserialize<'de> for ClientHint {
134 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135 where
136 D: serde::Deserializer<'de>,
137 {
138 use serde::de::Error;
139 let s = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
140 Self::try_from(s.as_ref()).map_err(D::Error::custom)
141 }
142 }
143
144 #[doc = "Returns an iterator over all client hints."]
145 pub fn all_client_hints() -> impl Iterator<Item = ClientHint> {
146 [
147 $(
148 ClientHint::$name,
149 )+
150 ].into_iter()
151 }
152
153 #[doc = "Returns an iterator over all client hint header name strings."]
154 pub fn all_client_hint_header_name_strings() -> impl Iterator<Item = &'static str> {
155 [
156 $(
157 $($str,)+
158 )+
159 ].into_iter()
160 }
161
162 #[doc = "Returns an iterator over all client hint header names."]
163 pub fn all_client_hint_header_names() -> impl Iterator<Item = ::rama_http_types::HeaderName> {
164 all_client_hint_header_name_strings().map(::rama_http_types::HeaderName::from_static)
165 }
166 };
167}
168
169client_hint! {
174 #[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."]
175 pub enum ClientHint {
176 Ua("sec-ch-ua"),
178 FullVersion("sec-ch-ua-full-version"),
180 FullVersionList("sec-ch-ua-full-version-list"),
182 Platform("sec-ch-ua-platform"),
184 PlatformVersion("sec-ch-ua-platform-version"),
186 Arch("sec-ch-ua-arch"),
188 Bitness("sec-ch-ua-bitness"),
190 Wow64("sec-ch-ua-wow64"),
192 Model("sec-ch-ua-model"),
194 Mobile("sec-ch-ua-mobile"),
196 FormFactor("sec-ch-ua-form-factors"),
198 Lang("sec-ch-lang", "lang"),
200 SaveData("sec-ch-save-data", "save-data"),
202 Width("sec-ch-width"),
204 ViewportWidth("sec-ch-viewport-width", "viewport-width"),
206 ViewportHeight("sec-ch-viewport-height"),
208 Dpr("sec-ch-dpr", "dpr"),
210 DeviceMemory("sec-ch-device-memory", "device-memory"),
212 Rtt("sec-ch-rtt", "rtt"),
214 Downlink("sec-ch-downlink", "downlink"),
216 Ect("sec-ch-ect", "ect"),
218 PrefersColorScheme("sec-ch-prefers-color-scheme"),
220 PrefersReducedMotion("sec-ch-prefers-reduced-motion"),
222 PrefersReducedTransparency("sec-ch-prefers-reduced-transparency"),
224 PrefersContrast("sec-ch-prefers-contrast"),
226 ForcedColors("sec-ch-forced-colors"),
228 }
229}
230
231fn encode_client_hint_names(hints: &NonEmptySmallVec<16, ClientHint>) -> Option<HeaderValue> {
250 let mut buf = Vec::new();
251 for name in hints
252 .iter()
253 .flat_map(|h| h.header_name_strs().iter().copied())
254 {
255 if !buf.is_empty() {
256 buf.extend_from_slice(b", ");
257 }
258 buf.extend_from_slice(name.as_bytes());
259 }
260 HeaderValue::from_bytes(&buf).ok()
261}
262
263macro_rules! client_hint_advert_header {
264 (
265 #[header(name = $name:ident)]
266 $(#[$m:meta])*
267 $type:ident
268 ) => {
269 $(#[$m])*
270 #[derive(Clone, Debug, PartialEq, Eq)]
271 pub struct $type(pub NonEmptySmallVec<16, ClientHint>);
272
273 impl $type {
274 #[doc = concat!("Create a `", stringify!($type), "` advertising a single [`ClientHint`].")]
275 #[must_use]
276 pub fn new(hint: ClientHint) -> Self {
277 Self(NonEmptySmallVec::new(hint))
278 }
279 }
280
281 impl TypedHeader for $type {
282 fn name() -> &'static HeaderName {
283 &rama_http_types::header::$name
284 }
285 }
286
287 impl HeaderDecode for $type {
288 fn decode<'i, I>(values: &mut I) -> Result<Self, Error>
289 where
290 I: Iterator<Item = &'i HeaderValue>,
291 {
292 util::try_decode_flat_csv_header_values_as_non_empty_smallvec(
293 values,
294 util::FlatCsvSeparator::Comma,
295 )
296 .map(Self)
297 .map_err(|err| {
298 rama_core::telemetry::tracing::debug!(
299 concat!("failed to decode ", stringify!($name), ": {}"),
300 err,
301 );
302 Error::invalid()
303 })
304 }
305 }
306
307 impl HeaderEncode for $type {
308 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
309 match encode_client_hint_names(&self.0) {
310 Some(value) => values.extend(std::iter::once(value)),
311 None => rama_core::telemetry::tracing::debug!(
312 concat!("failed to encode ", stringify!($name), " client-hint names")
313 ),
314 }
315 }
316 }
317 };
318}
319
320client_hint_advert_header! {
321 #[header(name = ACCEPT_CH)]
322 AcceptCh
347}
348
349client_hint_advert_header! {
350 #[header(name = CRITICAL_CH)]
351 CriticalCh
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
406pub struct SaveData(bool);
407
408impl SaveData {
409 pub const HINT: ClientHint = ClientHint::SaveData;
411
412 #[must_use]
414 pub const fn new(enabled: bool) -> Self {
415 Self(enabled)
416 }
417
418 #[must_use]
420 pub const fn on() -> Self {
421 Self(true)
422 }
423
424 #[must_use]
426 pub const fn off() -> Self {
427 Self(false)
428 }
429
430 #[must_use]
432 pub const fn is_on(self) -> bool {
433 self.0
434 }
435}
436
437impl From<bool> for SaveData {
438 fn from(enabled: bool) -> Self {
439 Self(enabled)
440 }
441}
442
443impl From<SaveData> for bool {
444 fn from(value: SaveData) -> Self {
445 value.0
446 }
447}
448
449impl TypedHeader for SaveData {
450 fn name() -> &'static HeaderName {
451 &rama_http_types::header::SEC_CH_SAVE_DATA
452 }
453}
454
455impl HeaderDecode for SaveData {
456 fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
457 let value = values
458 .just_one()
459 .and_then(|value| value.to_str().ok())
460 .ok_or_else(Error::invalid)?;
461 if value.eq_ignore_ascii_case("on") {
462 Ok(Self(true))
463 } else if value.eq_ignore_ascii_case("off") {
464 Ok(Self(false))
465 } else {
466 Err(Error::invalid())
467 }
468 }
469}
470
471impl HeaderEncode for SaveData {
472 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
473 let value = if self.0 {
474 HeaderValue::from_static("on")
475 } else {
476 HeaderValue::from_static("off")
477 };
478 values.extend(std::iter::once(value));
479 }
480}
481
482#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
499pub enum Ect {
500 Slow2g,
502 Type2g,
504 Type3g,
506 Type4g,
508}
509
510impl Ect {
511 pub const HINT: ClientHint = ClientHint::Ect;
513
514 #[must_use]
516 pub const fn as_str(self) -> &'static str {
517 match self {
518 Self::Slow2g => "slow-2g",
519 Self::Type2g => "2g",
520 Self::Type3g => "3g",
521 Self::Type4g => "4g",
522 }
523 }
524}
525
526impl std::fmt::Display for Ect {
527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528 f.write_str(self.as_str())
529 }
530}
531
532impl TypedHeader for Ect {
533 fn name() -> &'static HeaderName {
534 &rama_http_types::header::SEC_CH_ECT
535 }
536}
537
538impl HeaderDecode for Ect {
539 fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
540 let value = values
541 .just_one()
542 .and_then(|value| value.to_str().ok())
543 .ok_or_else(Error::invalid)?;
544 rama_utils::macros::match_ignore_ascii_case_str! {
545 match (value) {
546 "slow-2g" => Ok(Self::Slow2g),
547 "2g" => Ok(Self::Type2g),
548 "3g" => Ok(Self::Type3g),
549 "4g" => Ok(Self::Type4g),
550 _ => Err(Error::invalid()),
551 }
552 }
553 }
554}
555
556impl HeaderEncode for Ect {
557 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
558 values.extend(std::iter::once(HeaderValue::from_static(self.as_str())));
559 }
560}
561
562#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
581pub struct Rtt(u64);
582
583impl Rtt {
584 pub const HINT: ClientHint = ClientHint::Rtt;
586
587 #[must_use]
589 pub const fn from_millis(millis: u64) -> Self {
590 Self(millis)
591 }
592
593 #[must_use]
595 pub const fn as_millis(self) -> u64 {
596 self.0
597 }
598
599 #[must_use]
601 pub const fn as_duration(self) -> Duration {
602 Duration::from_millis(self.0)
603 }
604}
605
606impl From<Rtt> for Duration {
607 fn from(rtt: Rtt) -> Self {
608 rtt.as_duration()
609 }
610}
611
612impl TypedHeader for Rtt {
613 fn name() -> &'static HeaderName {
614 &rama_http_types::header::SEC_CH_RTT
615 }
616}
617
618impl HeaderDecode for Rtt {
619 fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
620 values
621 .just_one()
622 .and_then(|value| value.to_str().ok())
623 .and_then(|s| s.parse::<u64>().ok())
624 .map(Self)
625 .ok_or_else(Error::invalid)
626 }
627}
628
629impl HeaderEncode for Rtt {
630 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
631 values.extend(std::iter::once(self.0.into()));
632 }
633}
634
635#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
652pub struct Downlink(f64);
653
654impl Downlink {
655 pub const HINT: ClientHint = ClientHint::Downlink;
657
658 #[must_use]
660 pub const fn new(mbps: f64) -> Self {
661 Self(mbps)
662 }
663
664 #[must_use]
666 pub const fn as_mbps(self) -> f64 {
667 self.0
668 }
669}
670
671impl From<Downlink> for f64 {
672 fn from(downlink: Downlink) -> Self {
673 downlink.0
674 }
675}
676
677impl TypedHeader for Downlink {
678 fn name() -> &'static HeaderName {
679 &rama_http_types::header::SEC_CH_DOWNLINK
680 }
681}
682
683impl HeaderDecode for Downlink {
684 fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
685 values
686 .just_one()
687 .and_then(|value| value.to_str().ok())
688 .and_then(|s| s.parse::<f64>().ok())
689 .filter(|mbps| mbps.is_finite() && *mbps >= 0.0)
690 .map(Self)
691 .ok_or_else(Error::invalid)
692 }
693}
694
695impl HeaderEncode for Downlink {
696 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
697 values.extend(std::iter::once(util::fmt(self.0)));
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704
705 #[test]
706 fn test_client_hint_ua_from_str() {
707 let hint = ClientHint::try_from("Sec-CH-UA").unwrap();
708 assert_eq!(hint, ClientHint::Ua);
709 }
710
711 #[test]
712 fn test_client_hint_ua_from_str_lowercase() {
713 let hint = ClientHint::try_from("sec-ch-ua").unwrap();
714 assert_eq!(hint, ClientHint::Ua);
715 }
716
717 #[test]
718 fn test_client_hint_ua_from_str_uppercase() {
719 let hint = ClientHint::try_from("SEC-CH-UA").unwrap();
720 assert_eq!(hint, ClientHint::Ua);
721 }
722
723 #[test]
724 fn test_client_hint_ua_from_str_mixedcase() {
725 let hint = ClientHint::try_from("Sec-CH-UA").unwrap();
726 assert_eq!(hint, ClientHint::Ua);
727 }
728
729 #[test]
730 fn test_client_hint_low_entropy() {
731 let hints = [
732 "Sec-CH-UA",
733 "Sec-CH-UA-Mobile",
734 "Sec-CH-UA-Platform",
735 "Save-Data",
736 "Sec-CH-Save-Data",
737 ];
738
739 for hint in hints {
740 let hint = ClientHint::try_from(hint).expect(hint);
741 assert!(hint.is_low_entropy());
742 }
743 }
744
745 #[test]
746 fn test_client_hint_high_entropy() {
747 let hints = [
748 "Sec-CH-UA-Full-Version",
749 "Sec-CH-UA-Full-Version-List",
750 "Sec-CH-UA-Platform-Version",
751 "Sec-CH-UA-Arch",
752 "Sec-CH-UA-Bitness",
753 "Sec-CH-UA-WoW64",
754 "Sec-CH-UA-Model",
755 "Sec-CH-UA-Form-Factors",
756 "Sec-CH-Width",
757 "Sec-CH-Viewport-Width",
758 "Sec-CH-Viewport-Height",
759 "Sec-CH-DPR",
760 "Sec-CH-Device-Memory",
761 "Sec-CH-RTT",
762 "Sec-CH-Downlink",
763 "Sec-CH-ECT",
764 "Sec-CH-Prefers-Color-Scheme",
765 "Sec-CH-Prefers-Reduced-Motion",
766 "Sec-CH-Prefers-Reduced-Transparency",
767 "Sec-CH-Prefers-Contrast",
768 "Sec-CH-Forced-Colors",
769 ];
770
771 for hint in hints {
772 let hint = ClientHint::try_from(hint).expect(hint);
773 assert!(!hint.is_low_entropy());
774 }
775 }
776
777 #[test]
778 fn test_all_client_hint_header_name_strings_contains_some_hints() {
779 let strings = all_client_hint_header_name_strings().collect::<Vec<_>>();
780 assert!(strings.contains(&"sec-ch-ua"), "{strings:?}");
781 }
782
783 #[test]
784 fn test_all_client_hint_header_names() {
785 let names = all_client_hint_header_names().collect::<Vec<_>>();
786 let strings = all_client_hint_header_name_strings().collect::<Vec<_>>();
787 assert_eq!(names.len(), strings.len());
788 for (name, string) in names.iter().zip(strings.iter()) {
789 assert_eq!(name.as_str(), *string);
790 }
791 }
792
793 fn decode<T: HeaderDecode>(values: &[&str]) -> Option<T> {
794 use crate::HeaderMapExt;
795 let mut map = rama_http_types::HeaderMap::new();
796 for value in values {
797 map.append(T::name(), value.parse().unwrap());
798 }
799 map.typed_get()
800 }
801
802 fn encode<T: HeaderEncode>(header: T) -> String {
803 use crate::HeaderMapExt;
804 let mut map = rama_http_types::HeaderMap::new();
805 map.typed_insert(header);
806 map.get(T::name())
807 .expect("header set")
808 .to_str()
809 .unwrap()
810 .to_owned()
811 }
812
813 #[test]
814 fn test_save_data_decode() {
815 assert_eq!(decode::<SaveData>(&["on"]), Some(SaveData::on()));
816 assert_eq!(decode::<SaveData>(&["ON"]), Some(SaveData::on()));
817 assert_eq!(decode::<SaveData>(&["off"]), Some(SaveData::off()));
818 assert!(decode::<SaveData>(&["1"]).is_none());
819 assert!(decode::<SaveData>(&[""]).is_none());
820 assert!(decode::<SaveData>(&["on", "off"]).is_none());
821 }
822
823 #[test]
824 fn test_save_data_round_trip() {
825 assert_eq!(encode(SaveData::on()), "on");
826 assert_eq!(encode(SaveData::off()), "off");
827 assert_eq!(
828 decode::<SaveData>(&[encode(SaveData::on()).as_str()]),
829 Some(SaveData::on())
830 );
831 }
832
833 #[test]
834 fn test_ect_decode() {
835 assert_eq!(decode::<Ect>(&["slow-2g"]), Some(Ect::Slow2g));
836 assert_eq!(decode::<Ect>(&["2g"]), Some(Ect::Type2g));
837 assert_eq!(decode::<Ect>(&["3g"]), Some(Ect::Type3g));
838 assert_eq!(decode::<Ect>(&["4g"]), Some(Ect::Type4g));
839 assert_eq!(decode::<Ect>(&["SLOW-2G"]), Some(Ect::Slow2g));
840 assert!(decode::<Ect>(&["5g"]).is_none());
841 assert!(decode::<Ect>(&[""]).is_none());
842 }
843
844 #[test]
845 fn test_ect_round_trip() {
846 for ect in [Ect::Slow2g, Ect::Type2g, Ect::Type3g, Ect::Type4g] {
847 assert_eq!(decode::<Ect>(&[encode(ect).as_str()]), Some(ect));
848 }
849 }
850
851 #[test]
852 fn test_rtt_decode() {
853 assert_eq!(
854 decode::<Rtt>(&["100"]).map(Duration::from),
855 Some(Duration::from_millis(100)),
856 );
857 assert_eq!(decode::<Rtt>(&["0"]), Some(Rtt::from_millis(0)));
858 assert!(decode::<Rtt>(&["-25"]).is_none());
859 assert!(decode::<Rtt>(&["1.5"]).is_none());
860 assert!(decode::<Rtt>(&["fast"]).is_none());
861 }
862
863 #[test]
864 fn test_rtt_round_trip() {
865 assert_eq!(encode(Rtt::from_millis(125)), "125");
866 assert_eq!(decode::<Rtt>(&["125"]), Some(Rtt::from_millis(125)));
867 }
868
869 #[test]
870 fn test_downlink_decode() {
871 assert_eq!(decode::<Downlink>(&["1.5"]), Some(Downlink::new(1.5)));
872 assert_eq!(decode::<Downlink>(&["100"]), Some(Downlink::new(100.0)));
873 assert_eq!(decode::<Downlink>(&["0"]), Some(Downlink::new(0.0)));
874 assert!(decode::<Downlink>(&["-1"]).is_none());
875 assert!(decode::<Downlink>(&["inf"]).is_none());
876 assert!(decode::<Downlink>(&["fast"]).is_none());
877 }
878
879 #[test]
880 fn test_downlink_round_trip() {
881 assert_eq!(encode(Downlink::new(1.5)), "1.5");
882 assert_eq!(encode(Downlink::new(100.0)), "100");
883 assert_eq!(decode::<Downlink>(&["1.5"]), Some(Downlink::new(1.5)));
884 }
885
886 #[test]
887 fn test_accept_ch_decode() {
888 use rama_utils::collections::non_empty_smallvec;
889
890 assert_eq!(
891 decode::<AcceptCh>(&["sec-ch-ua, sec-ch-ua-platform, sec-ch-ua-mobile"]),
892 Some(AcceptCh(non_empty_smallvec![
893 ClientHint::Ua,
894 ClientHint::Platform,
895 ClientHint::Mobile; 16
896 ])),
897 );
898 assert_eq!(
900 decode::<AcceptCh>(&["DPR, save-data"]),
901 Some(AcceptCh(non_empty_smallvec![
902 ClientHint::Dpr,
903 ClientHint::SaveData; 16
904 ])),
905 );
906 assert_eq!(
908 decode::<AcceptCh>(&["sec-ch-ua", "sec-ch-ua-platform"]),
909 Some(AcceptCh(non_empty_smallvec![
910 ClientHint::Ua,
911 ClientHint::Platform; 16
912 ])),
913 );
914 assert!(decode::<AcceptCh>(&["sec-ch-ua, not-a-hint"]).is_none());
916 assert!(decode::<AcceptCh>(&[""]).is_none());
917 assert!(decode::<AcceptCh>(&[]).is_none());
918 }
919
920 #[test]
921 fn test_accept_ch_round_trip() {
922 use rama_utils::collections::non_empty_smallvec;
923
924 let header = AcceptCh(non_empty_smallvec![
925 ClientHint::Ua,
926 ClientHint::Platform,
927 ClientHint::Mobile; 16
928 ]);
929 assert_eq!(
930 encode(header.clone()),
931 "sec-ch-ua, sec-ch-ua-platform, sec-ch-ua-mobile",
932 );
933 assert_eq!(
934 decode::<AcceptCh>(&[encode(header.clone()).as_str()]),
935 Some(header)
936 );
937 }
938
939 #[test]
940 fn test_critical_ch_round_trip() {
941 use rama_utils::collections::non_empty_smallvec;
942
943 let header = CriticalCh(non_empty_smallvec![ClientHint::Ua, ClientHint::Platform; 16]);
944 assert_eq!(encode(header.clone()), "sec-ch-ua, sec-ch-ua-platform");
945 assert_eq!(
946 decode::<CriticalCh>(&[encode(header.clone()).as_str()]),
947 Some(header),
948 );
949 }
950
951 #[test]
952 fn test_header_name_strs() {
953 assert_eq!(ClientHint::Ect.header_name_strs(), &["sec-ch-ect", "ect"]);
955 assert_eq!(
956 ClientHint::SaveData.header_name_strs(),
957 &["sec-ch-save-data", "save-data"]
958 );
959 assert_eq!(ClientHint::Ua.header_name_strs(), &["sec-ch-ua"]);
961 for hint in all_client_hints() {
963 assert_eq!(hint.as_str(), hint.header_name_strs()[0]);
964 let names: Vec<_> = hint
966 .iter_header_names()
967 .map(|n| n.as_str().to_owned())
968 .collect();
969 assert_eq!(names, hint.header_name_strs());
970 }
971 }
972
973 #[test]
974 fn test_accept_ch_emits_all_aliases() {
975 use rama_utils::collections::non_empty_smallvec;
976
977 let header = AcceptCh(non_empty_smallvec![
981 ClientHint::SaveData,
982 ClientHint::Ect,
983 ClientHint::Rtt,
984 ClientHint::Downlink; 16
985 ]);
986 assert_eq!(
987 encode(header),
988 "sec-ch-save-data, save-data, sec-ch-ect, ect, \
989 sec-ch-rtt, rtt, sec-ch-downlink, downlink",
990 );
991 assert_eq!(
993 decode::<AcceptCh>(&["save-data, ect, rtt, downlink"]),
994 Some(AcceptCh(non_empty_smallvec![
995 ClientHint::SaveData,
996 ClientHint::Ect,
997 ClientHint::Rtt,
998 ClientHint::Downlink; 16
999 ])),
1000 );
1001 }
1002
1003 #[test]
1004 fn test_critical_ch_emits_all_aliases() {
1005 let header = CriticalCh::new(ClientHint::SaveData);
1006 assert_eq!(encode(header), "sec-ch-save-data, save-data");
1007 }
1008
1009 #[test]
1010 fn test_typed_value_parsers_match_their_client_hint() {
1011 assert_eq!(SaveData::HINT, ClientHint::SaveData);
1012 assert_eq!(Ect::HINT, ClientHint::Ect);
1013 assert_eq!(Rtt::HINT, ClientHint::Rtt);
1014 assert_eq!(Downlink::HINT, ClientHint::Downlink);
1015
1016 assert_eq!(SaveData::name().as_str(), SaveData::HINT.as_str());
1018 assert_eq!(Ect::name().as_str(), Ect::HINT.as_str());
1019 assert_eq!(Rtt::name().as_str(), Rtt::HINT.as_str());
1020 assert_eq!(Downlink::name().as_str(), Downlink::HINT.as_str());
1021 }
1022}