Skip to main content

api_bones/
vary.rs

1//! `Vary` response header helper.
2//!
3//! The [`Vary`] type represents the set of request header names that affect
4//! the cacheability of a response (RFC 7231 §7.1.4). A wildcard `*` value
5//! indicates that the response is uncacheable regardless of request headers.
6//!
7//! # Example
8//!
9//! ```rust
10//! use api_bones::vary::Vary;
11//!
12//! let mut vary = Vary::new();
13//! vary.add("Accept");
14//! vary.add("Accept-Encoding");
15//! assert!(vary.contains("accept"));          // case-insensitive
16//! assert_eq!(vary.to_string(), "Accept, Accept-Encoding");
17//!
18//! let wildcard = Vary::wildcard();
19//! assert!(wildcard.is_wildcard());
20//! assert_eq!(wildcard.to_string(), "*");
21//! ```
22
23#[cfg(all(not(feature = "std"), feature = "alloc"))]
24use alloc::{
25    string::{String, ToString},
26    vec::Vec,
27};
28use core::{fmt, str::FromStr};
29#[cfg(feature = "serde")]
30use serde::{Deserialize, Serialize};
31
32// ---------------------------------------------------------------------------
33// Vary
34// ---------------------------------------------------------------------------
35
36/// The `Vary` response header (RFC 7231 §7.1.4).
37///
38/// Encodes the set of request header field names that were used to select the
39/// representation. The special value `*` means any request header may affect
40/// the response.
41#[derive(Debug, Clone, PartialEq, Eq)]
42#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
43#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
44#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
45pub enum Vary {
46    /// `Vary: *` — response is uncacheable by shared caches.
47    Wildcard,
48    /// `Vary: <header-name>, ...` — a specific list of header names.
49    Headers(Vec<String>),
50}
51
52impl Vary {
53    /// Create an empty `Vary` with no header names.
54    #[must_use]
55    pub fn new() -> Self {
56        Self::Headers(Vec::new())
57    }
58
59    /// Create a wildcard `Vary: *`.
60    #[must_use]
61    pub fn wildcard() -> Self {
62        Self::Wildcard
63    }
64
65    /// Returns `true` if this is the wildcard variant.
66    #[must_use]
67    pub fn is_wildcard(&self) -> bool {
68        matches!(self, Self::Wildcard)
69    }
70
71    /// Returns the list of header names, or `None` if this is a wildcard.
72    #[must_use]
73    pub fn headers(&self) -> Option<&[String]> {
74        match self {
75            Self::Wildcard => None,
76            Self::Headers(h) => Some(h),
77        }
78    }
79
80    /// Add a header name to the `Vary` list.
81    ///
82    /// If `self` is [`Vary::Wildcard`] this is a no-op. Duplicate names
83    /// (case-insensitive) are not added a second time.
84    ///
85    /// ```
86    /// use api_bones::vary::Vary;
87    ///
88    /// let mut v = Vary::new();
89    /// v.add("Accept");
90    /// v.add("accept"); // duplicate — ignored
91    /// assert_eq!(v.to_string(), "Accept");
92    /// ```
93    pub fn add(&mut self, name: impl Into<String>) {
94        if let Self::Headers(headers) = self {
95            let name = name.into();
96            let lower = name.to_lowercase();
97            if !headers.iter().any(|h| h.to_lowercase() == lower) {
98                headers.push(name);
99            }
100        }
101    }
102
103    /// Remove a header name from the `Vary` list (case-insensitive).
104    ///
105    /// Returns `true` if the name was present and removed. Always returns
106    /// `false` for the wildcard variant.
107    ///
108    /// ```
109    /// use api_bones::vary::Vary;
110    ///
111    /// let mut v = Vary::new();
112    /// v.add("Accept");
113    /// assert!(v.remove("ACCEPT"));
114    /// assert_eq!(v.to_string(), "");
115    /// ```
116    pub fn remove(&mut self, name: &str) -> bool {
117        if let Self::Headers(headers) = self {
118            let lower = name.to_lowercase();
119            let before = headers.len();
120            headers.retain(|h| h.to_lowercase() != lower);
121            return headers.len() < before;
122        }
123        false
124    }
125
126    /// Returns `true` if the named header is in the `Vary` list
127    /// (case-insensitive).
128    ///
129    /// Always returns `false` for the wildcard variant.
130    ///
131    /// ```
132    /// use api_bones::vary::Vary;
133    ///
134    /// let mut v = Vary::new();
135    /// v.add("Accept-Encoding");
136    /// assert!(v.contains("accept-encoding"));
137    /// assert!(!v.contains("accept"));
138    /// ```
139    #[must_use]
140    pub fn contains(&self, name: &str) -> bool {
141        match self {
142            Self::Wildcard => false,
143            Self::Headers(headers) => {
144                let lower = name.to_lowercase();
145                headers.iter().any(|h| h.to_lowercase() == lower)
146            }
147        }
148    }
149
150    /// Returns the number of header names in the list, or `None` for wildcard.
151    #[must_use]
152    pub fn len(&self) -> Option<usize> {
153        match self {
154            Self::Wildcard => None,
155            Self::Headers(h) => Some(h.len()),
156        }
157    }
158
159    /// Returns `true` when the list is empty (not a wildcard, and no names).
160    #[must_use]
161    pub fn is_empty(&self) -> bool {
162        match self {
163            Self::Wildcard => false,
164            Self::Headers(h) => h.is_empty(),
165        }
166    }
167}
168
169impl Default for Vary {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175// ---------------------------------------------------------------------------
176// Display / FromStr
177// ---------------------------------------------------------------------------
178
179impl fmt::Display for Vary {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        match self {
182            Self::Wildcard => f.write_str("*"),
183            Self::Headers(headers) => {
184                for (i, h) in headers.iter().enumerate() {
185                    if i > 0 {
186                        f.write_str(", ")?;
187                    }
188                    f.write_str(h)?;
189                }
190                Ok(())
191            }
192        }
193    }
194}
195
196/// Error returned when parsing a `Vary` header fails.
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct ParseVaryError;
199
200impl fmt::Display for ParseVaryError {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        f.write_str("invalid Vary header")
203    }
204}
205
206#[cfg(feature = "std")]
207impl std::error::Error for ParseVaryError {}
208
209impl FromStr for Vary {
210    type Err = ParseVaryError;
211
212    /// Parse a `Vary` header value.
213    ///
214    /// ```
215    /// use api_bones::vary::Vary;
216    ///
217    /// let v: Vary = "*".parse().unwrap();
218    /// assert!(v.is_wildcard());
219    ///
220    /// let v: Vary = "Accept, Accept-Encoding".parse().unwrap();
221    /// assert!(v.contains("Accept-Encoding"));
222    /// ```
223    fn from_str(s: &str) -> Result<Self, Self::Err> {
224        let s = s.trim();
225        if s == "*" {
226            return Ok(Self::Wildcard);
227        }
228        let mut vary = Self::new();
229        for part in s.split(',') {
230            let part = part.trim();
231            if !part.is_empty() {
232                vary.add(part.to_string());
233            }
234        }
235        Ok(vary)
236    }
237}
238
239// ---------------------------------------------------------------------------
240// Tests
241// ---------------------------------------------------------------------------
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn new_is_empty() {
249        let v = Vary::new();
250        assert!(v.is_empty());
251        assert!(!v.is_wildcard());
252        assert_eq!(v.to_string(), "");
253    }
254
255    #[test]
256    fn wildcard() {
257        let v = Vary::wildcard();
258        assert!(v.is_wildcard());
259        assert!(!v.is_empty());
260        assert_eq!(v.to_string(), "*");
261    }
262
263    #[test]
264    fn add_and_contains() {
265        let mut v = Vary::new();
266        v.add("Accept");
267        v.add("Accept-Encoding");
268        assert!(v.contains("Accept"));
269        assert!(v.contains("accept"));
270        assert!(v.contains("ACCEPT-ENCODING"));
271        assert!(!v.contains("Content-Type"));
272        assert_eq!(v.len(), Some(2));
273    }
274
275    #[test]
276    fn add_deduplicates() {
277        let mut v = Vary::new();
278        v.add("Accept");
279        v.add("accept");
280        assert_eq!(v.len(), Some(1));
281        assert_eq!(v.to_string(), "Accept");
282    }
283
284    #[test]
285    fn remove_present() {
286        let mut v = Vary::new();
287        v.add("Accept");
288        v.add("Content-Type");
289        assert!(v.remove("accept"));
290        assert!(!v.contains("Accept"));
291        assert_eq!(v.len(), Some(1));
292    }
293
294    #[test]
295    fn remove_absent_returns_false() {
296        let mut v = Vary::new();
297        v.add("Accept");
298        assert!(!v.remove("Content-Type"));
299    }
300
301    #[test]
302    fn add_on_wildcard_is_noop() {
303        let mut v = Vary::wildcard();
304        v.add("Accept");
305        assert!(v.is_wildcard());
306    }
307
308    #[test]
309    fn remove_on_wildcard_returns_false() {
310        let mut v = Vary::wildcard();
311        assert!(!v.remove("Accept"));
312    }
313
314    #[test]
315    fn display_multiple() {
316        let mut v = Vary::new();
317        v.add("Accept");
318        v.add("Accept-Encoding");
319        assert_eq!(v.to_string(), "Accept, Accept-Encoding");
320    }
321
322    #[test]
323    fn parse_wildcard() {
324        let v: Vary = "*".parse().unwrap();
325        assert!(v.is_wildcard());
326    }
327
328    #[test]
329    fn parse_header_list() {
330        let v: Vary = "Accept, Accept-Encoding".parse().unwrap();
331        assert!(v.contains("Accept"));
332        assert!(v.contains("Accept-Encoding"));
333        assert_eq!(v.len(), Some(2));
334    }
335
336    #[test]
337    fn roundtrip() {
338        let mut v = Vary::new();
339        v.add("Accept");
340        v.add("Origin");
341        let s = v.to_string();
342        let parsed: Vary = s.parse().unwrap();
343        assert_eq!(parsed, v);
344    }
345
346    // --- Default ---
347
348    #[test]
349    fn default_is_empty_headers() {
350        let v = Vary::default();
351        assert!(v.is_empty());
352        assert!(!v.is_wildcard());
353    }
354
355    // --- headers() ---
356
357    #[test]
358    fn headers_returns_slice_for_headers_variant() {
359        let mut v = Vary::new();
360        v.add("Accept");
361        v.add("Content-Type");
362        let h = v.headers().unwrap();
363        assert_eq!(h.len(), 2);
364        assert_eq!(h[0], "Accept");
365        assert_eq!(h[1], "Content-Type");
366    }
367
368    #[test]
369    fn headers_returns_none_for_wildcard() {
370        let v = Vary::wildcard();
371        assert!(v.headers().is_none());
372    }
373
374    // --- ParseVaryError Display ---
375
376    #[test]
377    fn parse_vary_error_display() {
378        let e = ParseVaryError;
379        assert_eq!(e.to_string(), "invalid Vary header");
380    }
381
382    // --- len() for wildcard ---
383
384    #[test]
385    fn len_returns_none_for_wildcard() {
386        assert_eq!(Vary::wildcard().len(), None);
387    }
388
389    // --- contains on wildcard ---
390
391    #[test]
392    fn contains_returns_false_for_wildcard() {
393        let v = Vary::wildcard();
394        assert!(!v.contains("Accept"));
395    }
396}