Skip to content

Commit 8caa5eb

Browse files
committed
Add conditional::IfMatch
1 parent f93c100 commit 8caa5eb

File tree

4 files changed

+343
-31
lines changed

4 files changed

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

src/conditional/match_directive.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use crate::conditional::ETag;
2+
use crate::headers::HeaderValue;
3+
4+
/// An HTTP `If-{None-,}Match` directive.
5+
#[derive(Debug, Clone, PartialEq, Eq)]
6+
pub enum MatchDirective {
7+
/// An ETag.
8+
ETag(ETag),
9+
/// Match any resource.
10+
Wildcard,
11+
}
12+
13+
impl MatchDirective {
14+
/// Create an instance from a string slice.
15+
//
16+
// This is a private method rather than a trait because we assume the
17+
// input string is a single-value only. This is upheld by the calling
18+
// function, but we cannot guarantee this to be true in the general
19+
// sense.
20+
pub(crate) fn from_str(s: &str) -> crate::Result<Option<Self>> {
21+
let s = s.trim();
22+
23+
// We're dealing with an empty string.
24+
if s.is_empty() {
25+
return Ok(None);
26+
}
27+
28+
match s {
29+
"*" => Ok(Some(MatchDirective::Wildcard)),
30+
s => ETag::from_str(s).map(|etag| Some(MatchDirective::ETag(etag))),
31+
}
32+
}
33+
}
34+
35+
impl From<ETag> for MatchDirective {
36+
fn from(etag: ETag) -> Self {
37+
Self::ETag(etag)
38+
}
39+
}
40+
41+
impl PartialEq<ETag> for MatchDirective {
42+
fn eq(&self, other: &ETag) -> bool {
43+
match self {
44+
Self::ETag(etag) => etag.eq(other),
45+
Self::Wildcard => false,
46+
}
47+
}
48+
}
49+
50+
impl<'a> PartialEq<ETag> for &'a MatchDirective {
51+
fn eq(&self, other: &ETag) -> bool {
52+
match self {
53+
MatchDirective::ETag(etag) => etag.eq(other),
54+
MatchDirective::Wildcard => false,
55+
}
56+
}
57+
}
58+
59+
impl From<MatchDirective> for HeaderValue {
60+
fn from(directive: MatchDirective) -> Self {
61+
match directive {
62+
MatchDirective::ETag(etag) => etag.value(),
63+
MatchDirective::Wildcard => unsafe {
64+
HeaderValue::from_bytes_unchecked("*".to_string().into_bytes())
65+
},
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)