From f182f53210ec8f7ec0c9f342b28f71f0efa1141b Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 1 Dec 2014 19:58:07 -0800 Subject: [PATCH] feat(headers): add CacheControl header --- src/header/common/cache_control.rs | 165 +++++++++++++++++++++++++ src/header/common/mod.rs | 4 + src/header/common/transfer_encoding.rs | 5 +- src/header/common/util.rs | 10 +- 4 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 src/header/common/cache_control.rs diff --git a/src/header/common/cache_control.rs b/src/header/common/cache_control.rs new file mode 100644 index 00000000..f4563d49 --- /dev/null +++ b/src/header/common/cache_control.rs @@ -0,0 +1,165 @@ +use std::fmt; +use std::str::FromStr; +use header::{Header, HeaderFormat}; +use super::util::{from_one_comma_delimited, fmt_comma_delimited}; + +/// The Cache-Control header. +#[deriving(PartialEq, Clone, Show)] +pub struct CacheControl(pub Vec); + +deref!(CacheControl -> Vec) + +impl Header for CacheControl { + fn header_name(_: Option) -> &'static str { + "Cache-Control" + } + + fn parse_header(raw: &[Vec]) -> Option { + let directives = raw.iter() + .filter_map(|line| from_one_comma_delimited(line[])) + .collect::>>() + .concat_vec(); + if directives.len() > 0 { + Some(CacheControl(directives)) + } else { + None + } + } +} + +impl HeaderFormat for CacheControl { + fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt_comma_delimited(fmt, self[]) + } +} + +/// CacheControl contains a list of these directives. +#[deriving(PartialEq, Clone)] +pub enum CacheDirective { + /// "no-cache" + NoCache, + /// "no-store" + NoStore, + /// "no-transform" + NoTransform, + /// "only-if-cached" + OnlyIfCached, + + // request directives + /// "max-age=delta" + MaxAge(uint), + /// "max-stale=delta" + MaxStale(uint), + /// "min-fresh=delta" + MinFresh(uint), + + // response directives + /// "must-revalidate" + MustRevalidate, + /// "public" + Public, + /// "private" + Private, + /// "proxy-revalidate" + ProxyRevalidate, + /// "s-maxage=delta" + SMaxAge(uint), + + /// Extension directives. Optionally include an argument. + Extension(String, Option) +} + +impl fmt::Show for CacheDirective { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::CacheDirective::*; + match *self { + NoCache => "no-cache", + NoStore => "no-store", + NoTransform => "no-transform", + OnlyIfCached => "only-if-cached", + + MaxAge(secs) => return write!(f, "max-age={}", secs), + MaxStale(secs) => return write!(f, "max-stale={}", secs), + MinFresh(secs) => return write!(f, "min-fresh={}", secs), + + MustRevalidate => "must-revalidate", + Public => "public", + Private => "private", + ProxyRevalidate => "proxy-revalidate", + SMaxAge(secs) => return write!(f, "s-maxage={}", secs), + + Extension(ref name, None) => name[], + Extension(ref name, Some(ref arg)) => return write!(f, "{}={}", name, arg), + + }.fmt(f) + } +} + +impl FromStr for CacheDirective { + fn from_str(s: &str) -> Option { + use self::CacheDirective::*; + match s { + "no-cache" => Some(NoCache), + "no-store" => Some(NoStore), + "no-transform" => Some(NoTransform), + "only-if-cached" => Some(OnlyIfCached), + "must-revalidate" => Some(MustRevalidate), + "public" => Some(Public), + "private" => Some(Private), + "proxy-revalidate" => Some(ProxyRevalidate), + "" => None, + _ => match s.find('=') { + Some(idx) if idx+1 < s.len() => match (s[..idx], s[idx+1..].trim_chars('"')) { + ("max-age" , secs) => from_str::(secs).map(MaxAge), + ("max-stale", secs) => from_str::(secs).map(MaxStale), + ("min-fresh", secs) => from_str::(secs).map(MinFresh), + ("s-maxage", secs) => from_str::(secs).map(SMaxAge), + (left, right) => Some(Extension(left.into_string(), Some(right.into_string()))) + }, + Some(_) => None, + None => Some(Extension(s.into_string(), None)) + } + } + } +} + +#[cfg(test)] +mod tests { + use header::Header; + use super::*; + + #[test] + fn test_parse_multiple_headers() { + let cache = Header::parse_header(&[b"no-cache".to_vec(), b"private".to_vec()]); + assert_eq!(cache, Some(CacheControl(vec![CacheDirective::NoCache, + CacheDirective::Private]))) + } + + #[test] + fn test_parse_argument() { + let cache = Header::parse_header(&[b"max-age=100, private".to_vec()]); + assert_eq!(cache, Some(CacheControl(vec![CacheDirective::MaxAge(100), + CacheDirective::Private]))) + } + + #[test] + fn test_parse_quote_form() { + let cache = Header::parse_header(&[b"max-age=\"200\"".to_vec()]); + assert_eq!(cache, Some(CacheControl(vec![CacheDirective::MaxAge(200)]))) + } + + #[test] + fn test_parse_extension() { + let cache = Header::parse_header(&[b"foo, bar=baz".to_vec()]); + assert_eq!(cache, Some(CacheControl(vec![CacheDirective::Extension("foo".to_string(), None), + CacheDirective::Extension("bar".to_string(), Some("baz".to_string()))]))) + } + + #[test] + fn test_parse_bad_syntax() { + let cache: Option = Header::parse_header(&[b"foo=".to_vec()]); + assert_eq!(cache, None) + } +} + +bench_header!(normal, CacheControl, { vec![b"no-cache, private".to_vec(), b"max-age=100".to_vec()] }) diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs index f262622d..4c019ff0 100644 --- a/src/header/common/mod.rs +++ b/src/header/common/mod.rs @@ -8,6 +8,7 @@ pub use self::accept::Accept; pub use self::authorization::Authorization; +pub use self::cache_control::CacheControl; pub use self::cookie::Cookies; pub use self::connection::Connection; pub use self::content_length::ContentLength; @@ -83,6 +84,9 @@ pub mod accept; /// Exposes the Authorization header. pub mod authorization; +/// Exposes the CacheControl header. +pub mod cache_control; + /// Exposes the Cookie header. pub mod cookie; diff --git a/src/header/common/transfer_encoding.rs b/src/header/common/transfer_encoding.rs index ac628e0b..d3db256e 100644 --- a/src/header/common/transfer_encoding.rs +++ b/src/header/common/transfer_encoding.rs @@ -76,14 +76,13 @@ impl Header for TransferEncoding { } fn parse_header(raw: &[Vec]) -> Option { - from_comma_delimited(raw).map(|vec| TransferEncoding(vec)) + from_comma_delimited(raw).map(TransferEncoding) } } impl HeaderFormat for TransferEncoding { fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - let TransferEncoding(ref parts) = *self; - fmt_comma_delimited(fmt, parts[]) + fmt_comma_delimited(fmt, self[]) } } diff --git a/src/header/common/util.rs b/src/header/common/util.rs index 0d4f60f9..b6bb4d3c 100644 --- a/src/header/common/util.rs +++ b/src/header/common/util.rs @@ -16,13 +16,19 @@ pub fn from_one_raw_str(raw: &[Vec]) -> Option { } } -/// Reads a comma-delimited raw string into a Vec. +/// Reads a comma-delimited raw header into a Vec. +#[inline] pub fn from_comma_delimited(raw: &[Vec]) -> Option> { if raw.len() != 1 { return None; } // we JUST checked that raw.len() == 1, so raw[0] WILL exist. - match from_utf8(unsafe { raw.as_slice().unsafe_get(0).as_slice() }) { + from_one_comma_delimited(unsafe { raw.as_slice().unsafe_get(0).as_slice() }) +} + +/// Reads a comma-delimited raw string into a Vec. +pub fn from_one_comma_delimited(raw: &[u8]) -> Option> { + match from_utf8(raw) { Some(s) => { Some(s.as_slice() .split([',', ' '].as_slice())