Skip to main content

notedthat_core/
conditional.rs

1//! Raw HTTP conditional request headers as received from the client.
2//!
3//! Per SPECIFICATIONS.md D9, `NotedThat` forwards these values verbatim to the S3 backend
4//! without validation. Each field holds the raw header value as a string; the S3 adapter
5//! performs any parsing (e.g., HTTP-date conversion for `If-*-Since`).
6
7/// Raw HTTP conditional request headers as received from the client.
8///
9/// This struct is a pure carrier per SPECIFICATIONS.md D9 — `NotedThat` forwards
10/// these values verbatim to the S3 backend without validation. Each field holds
11/// the raw header value as a string; the S3 adapter performs any parsing
12/// (e.g., HTTP-date conversion for `If-*-Since`).
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct ConditionalHeaders {
15    /// Value of the `If-Match` header, if present.
16    pub if_match: Option<String>,
17    /// Value of the `If-None-Match` header, if present.
18    pub if_none_match: Option<String>,
19    /// Value of the `If-Modified-Since` header, if present (raw HTTP-date string).
20    pub if_modified_since: Option<String>,
21    /// Value of the `If-Unmodified-Since` header, if present (raw HTTP-date string).
22    pub if_unmodified_since: Option<String>,
23}
24
25impl ConditionalHeaders {
26    /// Returns true if all four conditional header fields are None.
27    pub fn is_empty(&self) -> bool {
28        self.if_match.is_none()
29            && self.if_none_match.is_none()
30            && self.if_modified_since.is_none()
31            && self.if_unmodified_since.is_none()
32    }
33
34    /// Extracts the four conditional headers from an HTTP `HeaderMap`.
35    ///
36    /// Non-UTF-8 header values are silently dropped (treated as missing).
37    pub fn from_header_map(headers: &http::HeaderMap) -> Self {
38        Self {
39            if_match: headers
40                .get(http::header::IF_MATCH)
41                .and_then(|v| v.to_str().ok())
42                .map(String::from),
43            if_none_match: headers
44                .get(http::header::IF_NONE_MATCH)
45                .and_then(|v| v.to_str().ok())
46                .map(String::from),
47            if_modified_since: headers
48                .get(http::header::IF_MODIFIED_SINCE)
49                .and_then(|v| v.to_str().ok())
50                .map(String::from),
51            if_unmodified_since: headers
52                .get(http::header::IF_UNMODIFIED_SINCE)
53                .and_then(|v| v.to_str().ok())
54                .map(String::from),
55        }
56    }
57}
58
59// Compile-time trait bound check: ConditionalHeaders must be Send + Sync + Clone + Debug
60fn _assert_bounds()
61where
62    ConditionalHeaders: Send + Sync + Clone + std::fmt::Debug,
63{
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn test_default_all_none() {
72        let headers = ConditionalHeaders::default();
73        assert_eq!(headers.if_match, None);
74        assert_eq!(headers.if_none_match, None);
75        assert_eq!(headers.if_modified_since, None);
76        assert_eq!(headers.if_unmodified_since, None);
77    }
78
79    #[test]
80    fn test_default_is_empty() {
81        let headers = ConditionalHeaders::default();
82        assert!(headers.is_empty());
83    }
84
85    #[test]
86    fn test_is_empty_false_with_if_match() {
87        let headers = ConditionalHeaders {
88            if_match: Some("\"abc123\"".to_string()),
89            if_none_match: None,
90            if_modified_since: None,
91            if_unmodified_since: None,
92        };
93        assert!(!headers.is_empty());
94    }
95
96    #[test]
97    fn test_is_empty_false_with_if_none_match() {
98        let headers = ConditionalHeaders {
99            if_match: None,
100            if_none_match: Some("\"xyz789\"".to_string()),
101            if_modified_since: None,
102            if_unmodified_since: None,
103        };
104        assert!(!headers.is_empty());
105    }
106
107    #[test]
108    fn test_from_header_map_extracts_if_match() {
109        let mut map = http::HeaderMap::new();
110        map.insert(
111            http::header::IF_MATCH,
112            http::HeaderValue::from_static("\"etag-value\""),
113        );
114        let headers = ConditionalHeaders::from_header_map(&map);
115        assert_eq!(headers.if_match, Some("\"etag-value\"".to_string()));
116        assert_eq!(headers.if_none_match, None);
117        assert_eq!(headers.if_modified_since, None);
118        assert_eq!(headers.if_unmodified_since, None);
119    }
120
121    #[test]
122    fn test_from_header_map_missing_headers() {
123        let map = http::HeaderMap::new();
124        let headers = ConditionalHeaders::from_header_map(&map);
125        assert_eq!(headers, ConditionalHeaders::default());
126    }
127
128    #[test]
129    fn test_from_header_map_empty_map_is_empty() {
130        let map = http::HeaderMap::new();
131        let headers = ConditionalHeaders::from_header_map(&map);
132        assert!(headers.is_empty());
133    }
134
135    #[test]
136    fn test_from_header_map_all_four_headers() {
137        let mut map = http::HeaderMap::new();
138        map.insert(
139            http::header::IF_MATCH,
140            http::HeaderValue::from_static("\"match-etag\""),
141        );
142        map.insert(
143            http::header::IF_NONE_MATCH,
144            http::HeaderValue::from_static("\"none-match-etag\""),
145        );
146        map.insert(
147            http::header::IF_MODIFIED_SINCE,
148            http::HeaderValue::from_static("Wed, 21 Oct 2015 07:28:00 GMT"),
149        );
150        map.insert(
151            http::header::IF_UNMODIFIED_SINCE,
152            http::HeaderValue::from_static("Wed, 21 Oct 2015 07:28:00 GMT"),
153        );
154
155        let headers = ConditionalHeaders::from_header_map(&map);
156        assert_eq!(headers.if_match, Some("\"match-etag\"".to_string()));
157        assert_eq!(
158            headers.if_none_match,
159            Some("\"none-match-etag\"".to_string())
160        );
161        assert_eq!(
162            headers.if_modified_since,
163            Some("Wed, 21 Oct 2015 07:28:00 GMT".to_string())
164        );
165        assert_eq!(
166            headers.if_unmodified_since,
167            Some("Wed, 21 Oct 2015 07:28:00 GMT".to_string())
168        );
169        assert!(!headers.is_empty());
170    }
171
172    #[test]
173    fn test_trait_bounds_send_sync_clone_debug() {
174        // This test verifies at compile time that ConditionalHeaders implements
175        // Send, Sync, Clone, and Debug. If it doesn't, this won't compile.
176        fn assert_send_sync_clone_debug<T: Send + Sync + Clone + std::fmt::Debug>() {}
177        assert_send_sync_clone_debug::<ConditionalHeaders>();
178    }
179}