Skip to content

Commit c065bfd

Browse files
authored
Merge pull request #211 from http-rs/etag
Typed ETags
2 parents 7b0a1b3 + 55bb472 commit c065bfd

File tree

4 files changed

+197
-1
lines changed

4 files changed

+197
-1
lines changed

src/cache/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
//! # Further Reading
88
//!
99
//! - [MDN: HTTP Caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching)
10-
//! - [MDN: HTTP Conditional Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests)
1110
1211
mod cache_control;
1312

src/conditional/etag.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
use crate::headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, ETAG};
2+
use crate::{Error, StatusCode};
3+
4+
use std::fmt::Debug;
5+
use std::option;
6+
7+
/// HTTP Entity Tags.
8+
///
9+
/// ETags provide an ID for a particular resource, enabling clients and servers
10+
/// to reason about caches and make conditional requests.
11+
///
12+
/// # Specifications
13+
///
14+
/// - [RFC 7232 HTTP/1.1: Conditional Requests](https://tools.ietf.org/html/rfc7232#section-2.3)
15+
///
16+
/// # Examples
17+
///
18+
/// ```
19+
/// # fn main() -> http_types::Result<()> {
20+
/// #
21+
/// use http_types::Response;
22+
/// use http_types::conditional::ETag;
23+
///
24+
/// let etag = ETag::new("0xcafebeef".to_string());
25+
///
26+
/// let mut res = Response::new(200);
27+
/// etag.apply(&mut res);
28+
///
29+
/// let etag = ETag::from_headers(res)?.unwrap();
30+
/// assert_eq!(etag, ETag::Strong(String::from("0xcafebeef")));
31+
/// #
32+
/// # Ok(()) }
33+
/// ```
34+
#[derive(Debug, Clone, Eq, PartialEq)]
35+
pub enum ETag {
36+
/// An ETag using strong validation.
37+
Strong(String),
38+
/// An ETag using weak validation.
39+
Weak(String),
40+
}
41+
42+
impl ETag {
43+
/// Create a new ETag that uses strong validation.
44+
pub fn new(s: String) -> Self {
45+
debug_assert!(!s.contains('\\'), "ETags ought to avoid backslash chars");
46+
Self::Strong(s)
47+
}
48+
49+
/// Create a new ETag that uses weak validation.
50+
pub fn new_weak(s: String) -> Self {
51+
debug_assert!(!s.contains('\\'), "ETags ought to avoid backslash chars");
52+
Self::Weak(s)
53+
}
54+
55+
/// Create a new instance from headers.
56+
///
57+
/// Only a single ETag per resource is assumed to exist. If multiple ETag
58+
/// headers are found the last one is used.
59+
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
60+
let headers = match headers.as_ref().get(ETAG) {
61+
Some(headers) => headers,
62+
None => return Ok(None),
63+
};
64+
65+
// If a header is returned we can assume at least one exists.
66+
let s = headers.iter().last().unwrap().as_str();
67+
68+
let mut weak = false;
69+
let s = match s.strip_prefix("W/") {
70+
Some(s) => {
71+
weak = true;
72+
s
73+
}
74+
None => s,
75+
};
76+
77+
let s = match s.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
78+
Some(s) => s.to_owned(),
79+
None => {
80+
return Err(Error::from_str(
81+
StatusCode::BadRequest,
82+
"Invalid ETag header",
83+
))
84+
}
85+
};
86+
87+
let etag = if weak { Self::Weak(s) } else { Self::Strong(s) };
88+
Ok(Some(etag))
89+
}
90+
91+
/// Sets the `ETag` header.
92+
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
93+
headers.as_mut().insert(ETAG, self.value());
94+
}
95+
96+
/// Get the `HeaderName`.
97+
pub fn name(&self) -> HeaderName {
98+
ETAG
99+
}
100+
101+
/// Get the `HeaderValue`.
102+
pub fn value(&self) -> HeaderValue {
103+
let s = match self {
104+
Self::Strong(s) => format!(r#""{}""#, s),
105+
Self::Weak(s) => format!(r#"W/"{}""#, s),
106+
};
107+
// SAFETY: the internal string is validated to be ASCII.
108+
unsafe { HeaderValue::from_bytes_unchecked(s.into()) }
109+
}
110+
111+
/// Returns `true` if the ETag is a `Strong` value.
112+
pub fn is_strong(&self) -> bool {
113+
matches!(self, Self::Strong(_))
114+
}
115+
116+
/// Returns `true` if the ETag is a `Weak` value.
117+
pub fn is_weak(&self) -> bool {
118+
matches!(self, Self::Weak(_))
119+
}
120+
}
121+
122+
impl ToHeaderValues for ETag {
123+
type Iter = option::IntoIter<HeaderValue>;
124+
fn to_header_values(&self) -> crate::Result<Self::Iter> {
125+
// A HeaderValue will always convert into itself.
126+
Ok(self.value().to_header_values().unwrap())
127+
}
128+
}
129+
130+
#[cfg(test)]
131+
mod test {
132+
use super::*;
133+
use crate::headers::Headers;
134+
135+
#[test]
136+
fn smoke() -> crate::Result<()> {
137+
let etag = ETag::new("0xcafebeef".to_string());
138+
139+
let mut headers = Headers::new();
140+
etag.apply(&mut headers);
141+
142+
let etag = ETag::from_headers(headers)?.unwrap();
143+
assert_eq!(etag, ETag::Strong(String::from("0xcafebeef")));
144+
Ok(())
145+
}
146+
147+
#[test]
148+
fn smoke_weak() -> crate::Result<()> {
149+
let etag = ETag::new_weak("0xcafebeef".to_string());
150+
151+
let mut headers = Headers::new();
152+
etag.apply(&mut headers);
153+
154+
let etag = ETag::from_headers(headers)?.unwrap();
155+
assert_eq!(etag, ETag::Weak(String::from("0xcafebeef")));
156+
Ok(())
157+
}
158+
159+
#[test]
160+
fn bad_request_on_parse_error() -> crate::Result<()> {
161+
let mut headers = Headers::new();
162+
headers.insert(ETAG, "<nori ate the tag. yum.>");
163+
let err = ETag::from_headers(headers).unwrap_err();
164+
assert_eq!(err.status(), 400);
165+
Ok(())
166+
}
167+
168+
#[test]
169+
fn validate_quotes() -> crate::Result<()> {
170+
assert_entry_err(r#""hello"#, "Invalid ETag header");
171+
assert_entry_err(r#"hello""#, "Invalid ETag header");
172+
assert_entry_err(r#"/O"valid content""#, "Invalid ETag header");
173+
assert_entry_err(r#"/Wvalid content""#, "Invalid ETag header");
174+
Ok(())
175+
}
176+
177+
fn assert_entry_err(s: &str, msg: &str) {
178+
let mut headers = Headers::new();
179+
headers.insert(ETAG, s);
180+
let err = ETag::from_headers(headers).unwrap_err();
181+
assert_eq!(format!("{}", err), msg);
182+
}
183+
}

src/conditional/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//! HTTP conditional headers.
2+
//!
3+
//! Web page performance can be significantly improved by caching resources.
4+
//! This submodule includes headers and types to communicate how and when to
5+
//! cache resources.
6+
//!
7+
//! # Further Reading
8+
//!
9+
//! - [MDN: HTTP Conditional Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests)
10+
11+
mod etag;
12+
13+
pub use etag::ETag;

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ pub mod url {
118118
mod utils;
119119

120120
pub mod cache;
121+
pub mod conditional;
121122
pub mod headers;
122123
pub mod mime;
123124

0 commit comments

Comments
 (0)