Skip to content

Commit daf3065

Browse files
authored
Merge pull request #224 from http-rs/if-match
Add `conditional::{IfMatch, IfNoneMatch}`
2 parents f93c100 + b941518 commit daf3065

File tree

8 files changed

+611
-34
lines changed

8 files changed

+611
-34
lines changed

src/conditional/etag.rs

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,41 @@ impl ETag {
6464

6565
// If a header is returned we can assume at least one exists.
6666
let s = headers.iter().last().unwrap().as_str();
67+
Self::from_str(s).map(Some)
68+
}
69+
70+
/// Sets the `ETag` header.
71+
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
72+
headers.as_mut().insert(ETAG, self.value());
73+
}
74+
75+
/// Get the `HeaderName`.
76+
pub fn name(&self) -> HeaderName {
77+
ETAG
78+
}
79+
80+
/// Get the `HeaderValue`.
81+
pub fn value(&self) -> HeaderValue {
82+
let s = match self {
83+
Self::Strong(s) => format!(r#""{}""#, s),
84+
Self::Weak(s) => format!(r#"W/"{}""#, s),
85+
};
86+
// SAFETY: the internal string is validated to be ASCII.
87+
unsafe { HeaderValue::from_bytes_unchecked(s.into()) }
88+
}
89+
90+
/// Returns `true` if the ETag is a `Strong` value.
91+
pub fn is_strong(&self) -> bool {
92+
matches!(self, Self::Strong(_))
93+
}
94+
95+
/// Returns `true` if the ETag is a `Weak` value.
96+
pub fn is_weak(&self) -> bool {
97+
matches!(self, Self::Weak(_))
98+
}
6799

100+
/// Create an Etag from a string.
101+
pub(crate) fn from_str(s: &str) -> crate::Result<Self> {
68102
let mut weak = false;
69103
let s = match s.strip_prefix("W/") {
70104
Some(s) => {
@@ -95,37 +129,7 @@ impl ETag {
95129
}
96130

97131
let etag = if weak { Self::Weak(s) } else { Self::Strong(s) };
98-
Ok(Some(etag))
99-
}
100-
101-
/// Sets the `ETag` header.
102-
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
103-
headers.as_mut().insert(ETAG, self.value());
104-
}
105-
106-
/// Get the `HeaderName`.
107-
pub fn name(&self) -> HeaderName {
108-
ETAG
109-
}
110-
111-
/// Get the `HeaderValue`.
112-
pub fn value(&self) -> HeaderValue {
113-
let s = match self {
114-
Self::Strong(s) => format!(r#""{}""#, s),
115-
Self::Weak(s) => format!(r#"W/"{}""#, s),
116-
};
117-
// SAFETY: the internal string is validated to be ASCII.
118-
unsafe { HeaderValue::from_bytes_unchecked(s.into()) }
119-
}
120-
121-
/// Returns `true` if the ETag is a `Strong` value.
122-
pub fn is_strong(&self) -> bool {
123-
matches!(self, Self::Strong(_))
124-
}
125-
126-
/// Returns `true` if the ETag is a `Weak` value.
127-
pub fn is_weak(&self) -> bool {
128-
matches!(self, Self::Weak(_))
132+
Ok(etag)
129133
}
130134
}
131135

src/conditional/if_match.rs

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
//! Apply the HTTP method if the ETag matches.
2+
3+
use crate::conditional::MatchDirective;
4+
use crate::headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, IF_MATCH};
5+
6+
use std::fmt::{self, Debug, Write};
7+
use std::iter::Iterator;
8+
use std::option;
9+
use std::slice;
10+
11+
/// Apply the HTTP method if the ETag matches.
12+
///
13+
/// # Specifications
14+
///
15+
/// - [RFC 7232, section 3.1: If-Match](https://tools.ietf.org/html/rfc7232#section-3.1)
16+
///
17+
/// # Examples
18+
///
19+
/// ```
20+
/// # fn main() -> http_types::Result<()> {
21+
/// #
22+
/// use http_types::Response;
23+
/// use http_types::conditional::{IfMatch, ETag};
24+
///
25+
/// let mut entries = IfMatch::new();
26+
/// entries.push(ETag::new("0xcafebeef".to_string()));
27+
/// entries.push(ETag::new("0xbeefcafe".to_string()));
28+
///
29+
/// let mut res = Response::new(200);
30+
/// entries.apply(&mut res);
31+
///
32+
/// let entries = IfMatch::from_headers(res)?.unwrap();
33+
/// let mut entries = entries.iter();
34+
/// assert_eq!(entries.next().unwrap(), ETag::new("0xcafebeef".to_string()));
35+
/// assert_eq!(entries.next().unwrap(), ETag::new("0xbeefcafe".to_string()));
36+
/// #
37+
/// # Ok(()) }
38+
/// ```
39+
pub struct IfMatch {
40+
entries: Vec<MatchDirective>,
41+
}
42+
43+
impl IfMatch {
44+
/// Create a new instance of `IfMatch`.
45+
pub fn new() -> Self {
46+
Self { entries: vec![] }
47+
}
48+
49+
/// Create a new instance from headers.
50+
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
51+
let mut entries = vec![];
52+
let headers = match headers.as_ref().get(IF_MATCH) {
53+
Some(headers) => headers,
54+
None => return Ok(None),
55+
};
56+
57+
for value in headers {
58+
for part in value.as_str().trim().split(',') {
59+
// Try and parse a directive from a str. If the directive is
60+
// unkown we skip it.
61+
if let Some(entry) = MatchDirective::from_str(part)? {
62+
entries.push(entry);
63+
}
64+
}
65+
}
66+
67+
Ok(Some(Self { entries }))
68+
}
69+
70+
/// Sets the `If-Match` header.
71+
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
72+
headers.as_mut().insert(IF_MATCH, self.value());
73+
}
74+
75+
/// Get the `HeaderName`.
76+
pub fn name(&self) -> HeaderName {
77+
IF_MATCH
78+
}
79+
80+
/// Get the `HeaderValue`.
81+
pub fn value(&self) -> HeaderValue {
82+
let mut output = String::new();
83+
for (n, directive) in self.entries.iter().enumerate() {
84+
let directive: HeaderValue = directive.clone().into();
85+
match n {
86+
0 => write!(output, "{}", directive).unwrap(),
87+
_ => write!(output, ", {}", directive).unwrap(),
88+
};
89+
}
90+
91+
// SAFETY: the internal string is validated to be ASCII.
92+
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
93+
}
94+
95+
/// Push a directive into the list of entries.
96+
pub fn push(&mut self, directive: impl Into<MatchDirective>) {
97+
self.entries.push(directive.into());
98+
}
99+
100+
/// An iterator visiting all server entries.
101+
pub fn iter(&self) -> Iter<'_> {
102+
Iter {
103+
inner: self.entries.iter(),
104+
}
105+
}
106+
107+
/// An iterator visiting all server entries.
108+
pub fn iter_mut(&mut self) -> IterMut<'_> {
109+
IterMut {
110+
inner: self.entries.iter_mut(),
111+
}
112+
}
113+
}
114+
115+
impl IntoIterator for IfMatch {
116+
type Item = MatchDirective;
117+
type IntoIter = IntoIter;
118+
119+
#[inline]
120+
fn into_iter(self) -> Self::IntoIter {
121+
IntoIter {
122+
inner: self.entries.into_iter(),
123+
}
124+
}
125+
}
126+
127+
impl<'a> IntoIterator for &'a IfMatch {
128+
type Item = &'a MatchDirective;
129+
type IntoIter = Iter<'a>;
130+
131+
#[inline]
132+
fn into_iter(self) -> Self::IntoIter {
133+
self.iter()
134+
}
135+
}
136+
137+
impl<'a> IntoIterator for &'a mut IfMatch {
138+
type Item = &'a mut MatchDirective;
139+
type IntoIter = IterMut<'a>;
140+
141+
#[inline]
142+
fn into_iter(self) -> Self::IntoIter {
143+
self.iter_mut()
144+
}
145+
}
146+
147+
/// A borrowing iterator over entries in `IfMatch`.
148+
#[derive(Debug)]
149+
pub struct IntoIter {
150+
inner: std::vec::IntoIter<MatchDirective>,
151+
}
152+
153+
impl Iterator for IntoIter {
154+
type Item = MatchDirective;
155+
156+
fn next(&mut self) -> Option<Self::Item> {
157+
self.inner.next()
158+
}
159+
160+
#[inline]
161+
fn size_hint(&self) -> (usize, Option<usize>) {
162+
self.inner.size_hint()
163+
}
164+
}
165+
166+
/// A lending iterator over entries in `IfMatch`.
167+
#[derive(Debug)]
168+
pub struct Iter<'a> {
169+
inner: slice::Iter<'a, MatchDirective>,
170+
}
171+
172+
impl<'a> Iterator for Iter<'a> {
173+
type Item = &'a MatchDirective;
174+
175+
fn next(&mut self) -> Option<Self::Item> {
176+
self.inner.next()
177+
}
178+
179+
#[inline]
180+
fn size_hint(&self) -> (usize, Option<usize>) {
181+
self.inner.size_hint()
182+
}
183+
}
184+
185+
/// A mutable iterator over entries in `IfMatch`.
186+
#[derive(Debug)]
187+
pub struct IterMut<'a> {
188+
inner: slice::IterMut<'a, MatchDirective>,
189+
}
190+
191+
impl<'a> Iterator for IterMut<'a> {
192+
type Item = &'a mut MatchDirective;
193+
194+
fn next(&mut self) -> Option<Self::Item> {
195+
self.inner.next()
196+
}
197+
198+
#[inline]
199+
fn size_hint(&self) -> (usize, Option<usize>) {
200+
self.inner.size_hint()
201+
}
202+
}
203+
204+
impl ToHeaderValues for IfMatch {
205+
type Iter = option::IntoIter<HeaderValue>;
206+
fn to_header_values(&self) -> crate::Result<Self::Iter> {
207+
// A HeaderValue will always convert into itself.
208+
Ok(self.value().to_header_values().unwrap())
209+
}
210+
}
211+
212+
impl Debug for IfMatch {
213+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214+
let mut list = f.debug_list();
215+
for directive in &self.entries {
216+
list.entry(directive);
217+
}
218+
list.finish()
219+
}
220+
}
221+
222+
#[cfg(test)]
223+
mod test {
224+
use crate::conditional::{ETag, IfMatch};
225+
use crate::Response;
226+
227+
#[test]
228+
fn smoke() -> crate::Result<()> {
229+
let mut entries = IfMatch::new();
230+
entries.push(ETag::new("0xcafebeef".to_string()));
231+
entries.push(ETag::new("0xbeefcafe".to_string()));
232+
233+
let mut res = Response::new(200);
234+
entries.apply(&mut res);
235+
236+
let entries = IfMatch::from_headers(res)?.unwrap();
237+
let mut entries = entries.iter();
238+
assert_eq!(entries.next().unwrap(), ETag::new("0xcafebeef".to_string()));
239+
assert_eq!(entries.next().unwrap(), ETag::new("0xbeefcafe".to_string()));
240+
Ok(())
241+
}
242+
}

src/conditional/if_modified_since.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use std::fmt::Debug;
55
use std::option;
66
use std::time::SystemTime;
77

8-
/// HTTP `IfModifiedSince` header
8+
/// Apply the HTTP method if the entity has been modified after the given
9+
/// date.
910
///
1011
/// # Specifications
1112
///

0 commit comments

Comments
 (0)