use std::{
fmt::{self, Display},
str::FromStr,
};
use http::header::{HeaderMap, InvalidHeaderValue};
#[derive(Debug, PartialEq)]
pub struct Etagged<T> {
pub etag: Option<EntityTag>,
pub value: Option<T>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntityTag {
pub weak: bool,
tag: String,
}
impl EntityTag {
pub fn extract_from_response<B>(response: &http::Response<B>) -> Option<EntityTag> {
response
.headers()
.get("ETag")
.and_then(|it| it.to_str().ok())
.and_then(|it| EntityTag::from_str(it).ok())
}
pub fn insert_if_none_match_header(
headers: &mut HeaderMap,
etag: EntityTag,
) -> Result<(), crate::Error> {
headers.insert(
"If-None-Match",
etag.to_string()
.parse()
.map_err(|err: InvalidHeaderValue| crate::Error::InvalidHeaderValue {
source: err,
backtrace: snafu::Backtrace::capture(),
})?,
);
Ok(())
}
pub fn new(weak: bool, tag: String) -> EntityTag {
assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
EntityTag { weak, tag }
}
pub fn weak(tag: String) -> EntityTag {
EntityTag::new(true, tag)
}
pub fn strong(tag: String) -> EntityTag {
EntityTag::new(false, tag)
}
pub fn tag(&self) -> &str {
self.tag.as_ref()
}
pub fn set_tag(&mut self, tag: String) {
assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
self.tag = tag
}
pub fn strong_eq(&self, other: &EntityTag) -> bool {
!self.weak && !other.weak && self.tag == other.tag
}
pub fn weak_eq(&self, other: &EntityTag) -> bool {
self.tag == other.tag
}
pub fn strong_ne(&self, other: &EntityTag) -> bool {
!self.strong_eq(other)
}
pub fn weak_ne(&self, other: &EntityTag) -> bool {
!self.weak_eq(other)
}
}
impl Display for EntityTag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.weak {
write!(f, "W/\"{}\"", self.tag)
} else {
write!(f, "\"{}\"", self.tag)
}
}
}
fn check_slice_validity(slice: &str) -> bool {
slice
.bytes()
.all(|c| c == b'\x21' || (b'\x23'..=b'\x7e').contains(&c) | (c >= b'\x80'))
}
impl FromStr for EntityTag {
type Err = String;
fn from_str(s: &str) -> Result<EntityTag, Self::Err> {
let length: usize = s.len();
let slice = s;
if !slice.ends_with('"') || slice.len() < 2 {
return Err("Does not end with double quote character".to_string());
}
if slice.len() >= 2 && slice.starts_with('"') && check_slice_validity(&slice[1..length - 1])
{
return Ok(EntityTag {
weak: false,
tag: slice[1..length - 1].to_owned(),
});
} else if slice.len() >= 4
&& slice.starts_with("W/\"")
&& check_slice_validity(&slice[3..length - 1])
{
return Ok(EntityTag {
weak: true,
tag: slice[3..length - 1].to_owned(),
});
}
Err("Could not parse EntityTag".to_string())
}
}
#[cfg(test)]
mod tests {
use super::EntityTag;
#[test]
fn test_etag_parse_success() {
assert_eq!(
"\"foobar\"".parse::<EntityTag>().unwrap(),
EntityTag::strong("foobar".to_owned())
);
assert_eq!(
"\"\"".parse::<EntityTag>().unwrap(),
EntityTag::strong("".to_owned())
);
assert_eq!(
"W/\"weaktag\"".parse::<EntityTag>().unwrap(),
EntityTag::weak("weaktag".to_owned())
);
assert_eq!(
"W/\"\x65\x62\"".parse::<EntityTag>().unwrap(),
EntityTag::weak("\x65\x62".to_owned())
);
assert_eq!(
"W/\"\"".parse::<EntityTag>().unwrap(),
EntityTag::weak("".to_owned())
);
}
#[test]
fn test_etag_parse_failures() {
assert!("no-dquotes".parse::<EntityTag>().is_err());
assert!("w/\"the-first-w-is-case-sensitive\""
.parse::<EntityTag>()
.is_err());
assert!("".parse::<EntityTag>().is_err());
assert!("\"unmatched-dquotes1".parse::<EntityTag>().is_err());
assert!("unmatched-dquotes2\"".parse::<EntityTag>().is_err());
assert!("matched-\"dquotes\"".parse::<EntityTag>().is_err());
}
#[test]
fn test_etag_fmt() {
assert_eq!(
format!("{}", EntityTag::strong("foobar".to_owned())),
"\"foobar\""
);
assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\"");
assert_eq!(
format!("{}", EntityTag::weak("weak-etag".to_owned())),
"W/\"weak-etag\""
);
assert_eq!(
format!("{}", EntityTag::weak("\u{0065}".to_owned())),
"W/\"\x65\""
);
assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\"");
}
#[test]
fn test_cmp() {
let mut etag1 = EntityTag::weak("1".to_owned());
let mut etag2 = EntityTag::weak("1".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
etag1 = EntityTag::weak("1".to_owned());
etag2 = EntityTag::weak("2".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(!etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(etag1.weak_ne(&etag2));
etag1 = EntityTag::weak("1".to_owned());
etag2 = EntityTag::strong("1".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
etag1 = EntityTag::strong("1".to_owned());
etag2 = EntityTag::strong("1".to_owned());
assert!(etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(!etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
}
}