rama_http_headers/common/
vary.rs1use rama_core::error::BoxError;
2use rama_core::telemetry::tracing;
3use rama_http_types::{HeaderName, HeaderValue, header};
4use rama_utils::collections::{NonEmptyVec, non_empty_vec};
5
6use crate::ClientHint;
7use crate::util::{
8 FlatCsvSeparator, TryFromValues, try_decode_flat_csv_header_values_as_non_empty_vec,
9 try_encode_non_empty_vec_as_flat_csv_header_value,
10};
11
12#[derive(Clone, Debug)]
39pub struct Vary(Directive);
40
41#[derive(Clone, Debug)]
42enum Directive {
43 Any,
44 Headers(NonEmptyVec<HeaderName>),
45}
46
47impl TryFrom<&Directive> for HeaderValue {
48 type Error = BoxError;
49
50 fn try_from(value: &Directive) -> Result<Self, Self::Error> {
51 match value {
52 Directive::Any => Ok(DIRECTIVE_HEADER_VALUE_ANY),
53 Directive::Headers(values) => {
54 try_encode_non_empty_vec_as_flat_csv_header_value(values, FlatCsvSeparator::Comma)
55 }
56 }
57 }
58}
59
60impl TryFromValues for Directive {
61 fn try_from_values<'i, I>(values: &mut I) -> Result<Self, crate::Error>
62 where
63 Self: Sized,
64 I: Iterator<Item = &'i HeaderValue>,
65 {
66 match try_decode_flat_csv_header_values_as_non_empty_vec(values, FlatCsvSeparator::Comma) {
67 Ok(values) => {
68 if values.len() == 1 && values.first() == "*" {
69 Ok(Self::Any)
70 } else {
71 Ok(Self::Headers(values))
72 }
73 }
74 Err(err) => {
75 tracing::trace!("invalid vary directive: {err}");
76 Err(crate::Error::invalid())
77 }
78 }
79 }
80}
81
82const DIRECTIVE_HEADER_VALUE_ANY: HeaderValue = HeaderValue::from_static("*");
83
84impl crate::TypedHeader for Vary {
85 fn name() -> &'static ::rama_http_types::header::HeaderName {
86 &::rama_http_types::header::VARY
87 }
88}
89
90impl crate::HeaderDecode for Vary {
91 fn decode<'i, I>(values: &mut I) -> Result<Self, crate::Error>
92 where
93 I: Iterator<Item = &'i ::rama_http_types::header::HeaderValue>,
94 {
95 Directive::try_from_values(values).map(Self)
96 }
97}
98
99impl crate::HeaderEncode for Vary {
100 fn encode<E: Extend<::rama_http_types::HeaderValue>>(&self, values: &mut E) {
101 match HeaderValue::try_from(&self.0) {
102 Ok(value) => values.extend(::std::iter::once(value)),
103 Err(err) => {
104 rama_core::telemetry::tracing::debug!(
105 "failed to encode vary directive {:?} as flat csv header: {err}",
106 self.0,
107 );
108 }
109 }
110 }
111}
112
113impl Vary {
114 #[must_use]
116 pub const fn any() -> Self {
117 Self(Directive::Any)
118 }
119
120 #[must_use]
122 #[inline(always)]
123 pub fn preflight_request_headers() -> Self {
124 Self::headers(non_empty_vec![
125 header::ORIGIN,
126 header::ACCESS_CONTROL_REQUEST_METHOD,
127 header::ACCESS_CONTROL_REQUEST_HEADERS,
128 ])
129 }
130
131 #[must_use]
133 pub fn headers(values: NonEmptyVec<HeaderName>) -> Self {
134 Self(Directive::Headers(values))
135 }
136
137 #[must_use]
143 pub fn from_client_hints<'a>(hints: impl IntoIterator<Item = &'a ClientHint>) -> Option<Self> {
144 NonEmptyVec::collect(hints.into_iter().flat_map(ClientHint::iter_header_names))
145 .map(Self::headers)
146 }
147
148 pub const fn is_any(&self) -> bool {
150 matches!(&self.0, Directive::Any)
151 }
152
153 pub fn iter_strs(&self) -> Option<impl Iterator<Item = &HeaderName>> {
156 match &self.0 {
157 Directive::Any => None,
158 Directive::Headers(non_empty_vec) => Some(non_empty_vec.iter()),
159 }
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::super::{test_decode, test_encode};
166 use super::*;
167 use rama_utils::collections::non_empty_vec;
168
169 #[test]
170 fn any_is_any() {
171 assert!(Vary::any().is_any());
172 }
173
174 #[test]
175 fn decode_header_single_open() {
176 let Vary(directive) = test_decode(&["foo, bar"]).unwrap();
177 match directive {
178 Directive::Any => panic!("unexpected any directive"),
179 Directive::Headers(non_empty_vec) => {
180 assert_eq!(2, non_empty_vec.len());
181 assert_eq!(non_empty_vec[0], "foo");
182 assert_eq!(non_empty_vec[1], "bar");
183 }
184 }
185 }
186
187 #[test]
188 fn decode_header_single_close() {
189 let Vary(directive) = test_decode(&["*"]).unwrap();
190 match directive {
191 Directive::Any => (),
192 Directive::Headers(non_empty_vec) => {
193 panic!("unexpected headers directive, headers: {non_empty_vec:?}")
194 }
195 }
196 }
197
198 #[test]
199 fn encode_headers() {
200 let vary = Vary::headers(non_empty_vec![
201 ::rama_http_types::header::USER_AGENT,
202 ::rama_http_types::header::CONTENT_ENCODING,
203 ]);
204
205 let headers = test_encode(vary);
206 assert_eq!(headers["vary"], "user-agent, content-encoding");
207 }
208
209 #[test]
210 fn from_client_hints_lists_every_spelling() {
211 let vary = Vary::from_client_hints(&[ClientHint::SaveData, ClientHint::Ect])
212 .expect("non-empty hint set");
213 let headers = test_encode(vary);
214 assert_eq!(
217 headers["vary"],
218 "sec-ch-save-data, save-data, sec-ch-ect, ect",
219 );
220 }
221
222 #[test]
223 fn from_client_hints_empty_is_none() {
224 assert!(Vary::from_client_hints(std::iter::empty::<&ClientHint>()).is_none());
225 }
226
227 #[test]
228 fn decode_with_empty_header_value() {
229 assert!(test_decode::<Vary>(&[""]).is_none());
230 }
231
232 #[test]
233 fn decode_with_no_headers() {
234 assert!(test_decode::<Vary>(&[]).is_none());
235 }
236
237 #[test]
238 fn decode_with_invalid_value_str() {
239 assert!(test_decode::<Vary>(&["foo foo, bar"]).is_none());
240 }
241}