#![allow(clippy::derive_partial_eq_without_eq)]
use std::collections::HashMap;
use std::fmt;
#[cfg(not(feature = "url"))]
use http::Uri;
#[cfg(feature = "url")]
use url::Url as Uri;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Error(ErrorKind);
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ErrorKind {
InternalError,
InvalidURI,
MalformedParam,
MalformedQuery,
MissingRel,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
ErrorKind::InternalError => write!(f, "internal parser error"),
ErrorKind::InvalidURI => write!(f, "unable to parse URI component"),
ErrorKind::MalformedParam => write!(f, "malformed parameter list"),
ErrorKind::MalformedQuery => write!(f, "malformed URI query"),
ErrorKind::MissingRel => write!(f, "missing 'rel' parameter"),
}
}
}
impl std::error::Error for Error {}
impl From<&Error> for Error {
fn from(x: &Error) -> Self {
Error(x.0)
}
}
#[derive(Debug, PartialEq)]
pub struct Link {
pub uri: Uri,
pub raw_uri: String,
pub queries: HashMap<String, String>,
pub params: HashMap<String, String>,
}
type Rel = String;
pub type LinkMap = HashMap<Option<Rel>, Link>;
pub type RelLinkMap = HashMap<Rel, Link>;
pub fn parse_with_rel(link_header: &str) -> Result<RelLinkMap> {
parse_with(link_header, |x| x.ok_or(Error(ErrorKind::MissingRel)))
}
pub fn parse(link_header: &str) -> Result<LinkMap> {
parse_with(link_header, Ok)
}
fn parse_with<K, F>(link_header: &str, make_key: F) -> Result<HashMap<K, Link>>
where
K: Eq + std::hash::Hash,
F: Fn(Option<String>) -> Result<K>,
{
use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
static ref RE: Result<Regex> =
Regex::new(r#"[<>"\s]"#).or(Err(Error(ErrorKind::InternalError)));
}
let mut result = HashMap::new();
let preprocessed = RE.as_ref()?.replace_all(link_header, "");
let splited = preprocessed.split(',');
for s in splited {
let mut link_vec: Vec<_> = s.split(';').collect();
link_vec.reverse();
let raw_uri = link_vec
.pop()
.ok_or(Error(ErrorKind::InternalError))?
.to_string();
let uri: Uri = raw_uri.parse().or(Err(Error(ErrorKind::InvalidURI)))?;
let mut queries = HashMap::new();
if let Some(query) = uri
.query()
.map(|query| query.trim_start_matches('&'))
{
for q in query.split('&') {
let (key, val) = q.split_once('=').ok_or(Error(ErrorKind::MalformedQuery))?;
queries.insert(key.to_string(), val.to_string());
}
}
let mut params = HashMap::new();
for param in link_vec {
let (key, val) = param
.split_once('=')
.ok_or(Error(ErrorKind::MalformedParam))?;
params.insert(key.to_string(), val.to_string());
}
result.insert(
make_key(params.get("rel").cloned())?,
Link {
uri,
raw_uri,
queries,
params,
},
);
}
Ok(result)
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[test]
fn parse_link_header_works() {
let link_header = r#"<https://api.github.com/repositories/41986369/contributors?page=2>; rel="next", <https://api.github.com/repositories/41986369/contributors?page=14>; rel="last""#;
let mut expected = HashMap::new();
expected.insert(
Some("next".to_string()),
Link {
uri: "https://api.github.com/repositories/41986369/contributors?page=2"
.parse()
.unwrap(),
raw_uri: "https://api.github.com/repositories/41986369/contributors?page=2"
.to_string(),
queries: [("page".to_string(), "2".to_string())]
.iter()
.cloned()
.collect(),
params: [("rel".to_string(), "next".to_string())]
.iter()
.cloned()
.collect(),
},
);
expected.insert(
Some("last".to_string()),
Link {
uri: "https://api.github.com/repositories/41986369/contributors?page=14"
.parse()
.unwrap(),
raw_uri: "https://api.github.com/repositories/41986369/contributors?page=14"
.to_string(),
queries: [("page".to_string(), "14".to_string())]
.iter()
.cloned()
.collect(),
params: [("rel".to_string(), "last".to_string())]
.iter()
.cloned()
.collect(),
},
);
let parsed = parse(link_header).unwrap();
assert_eq!(expected, parsed);
#[cfg(not(feature = "url"))]
{
let mut rel_link_expected = HashMap::new();
rel_link_expected.insert(
Some("foo/bar".to_string()),
Link {
uri: "/foo/bar".parse().unwrap(),
raw_uri: "/foo/bar".to_string(),
queries: HashMap::new(),
params: [("rel".to_string(), "foo/bar".to_string())]
.iter()
.cloned()
.collect(),
},
);
let rel_link_parsed = parse(r#"</foo/bar>; rel="foo/bar""#).unwrap();
assert_eq!(rel_link_expected, rel_link_parsed);
}
}
#[test]
fn parse_with_rel_works() {
let link_header = r#"<https://api.github.com/repositories/41986369/contributors?page=2>; rel="next", <https://api.github.com/repositories/41986369/contributors?page=14>; rel="last""#;
let mut expected = HashMap::new();
expected.insert(
"next".to_string(),
Link {
uri: "https://api.github.com/repositories/41986369/contributors?page=2"
.parse()
.unwrap(),
raw_uri: "https://api.github.com/repositories/41986369/contributors?page=2"
.to_string(),
queries: [("page".to_string(), "2".to_string())]
.iter()
.cloned()
.collect(),
params: [("rel".to_string(), "next".to_string())]
.iter()
.cloned()
.collect(),
},
);
expected.insert(
"last".to_string(),
Link {
uri: "https://api.github.com/repositories/41986369/contributors?page=14"
.parse()
.unwrap(),
raw_uri: "https://api.github.com/repositories/41986369/contributors?page=14"
.to_string(),
queries: [("page".to_string(), "14".to_string())]
.iter()
.cloned()
.collect(),
params: [("rel".to_string(), "last".to_string())]
.iter()
.cloned()
.collect(),
},
);
let parsed = parse_with_rel(link_header).unwrap();
assert_eq!(expected, parsed);
#[cfg(not(feature = "url"))]
{
let mut rel_link_expected = HashMap::new();
rel_link_expected.insert(
"foo/bar".to_string(),
Link {
uri: "/foo/bar".parse().unwrap(),
raw_uri: "/foo/bar".to_string(),
queries: HashMap::new(),
params: [("rel".to_string(), "foo/bar".to_string())]
.iter()
.cloned()
.collect(),
},
);
let rel_link_parsed = parse_with_rel(r#"</foo/bar>; rel="foo/bar""#).unwrap();
assert_eq!(rel_link_expected, rel_link_parsed);
}
}
#[test]
fn parse_link_header_should_err() {
assert_eq!(parse("<>"), Err(Error(ErrorKind::InvalidURI)));
}
#[test]
fn parse_with_rel_should_err() {
assert_eq!(
parse_with_rel(r#"<http://local.host/foo/bar>; type="foo/bar""#),
Err(Error(ErrorKind::MissingRel))
);
}
#[test]
fn sentry_paginating_results() {
let link_header = r#"<https://sentry.io/api/0/projects/1/groups/?&cursor=1420837590:0:1>; rel="previous"; results="false", <https://sentry.io/api/0/projects/1/groups/?&cursor=1420837533:0:0>; rel="next"; results="true""#;
let mut expected = HashMap::new();
expected.insert(
Some("previous".to_string()),
Link {
uri: "https://sentry.io/api/0/projects/1/groups/?&cursor=1420837590:0:1"
.parse()
.unwrap(),
raw_uri: "https://sentry.io/api/0/projects/1/groups/?&cursor=1420837590:0:1"
.to_string(),
queries: [("cursor".to_string(), "1420837590:0:1".to_string())]
.iter()
.cloned()
.collect(),
params: [
("rel".to_string(), "previous".to_string()),
("results".to_string(), "false".to_string()),
]
.iter()
.cloned()
.collect(),
},
);
expected.insert(
Some("next".to_string()),
Link {
uri: "https://sentry.io/api/0/projects/1/groups/?&cursor=1420837533:0:0"
.parse()
.unwrap(),
raw_uri: "https://sentry.io/api/0/projects/1/groups/?&cursor=1420837533:0:0"
.to_string(),
queries: [("cursor".to_string(), "1420837533:0:0".to_string())]
.iter()
.cloned()
.collect(),
params: [
("rel".to_string(), "next".to_string()),
("results".to_string(), "true".to_string()),
]
.iter()
.cloned()
.collect(),
},
);
let parsed = parse(link_header).unwrap();
assert_eq!(expected, parsed);
}
#[test]
fn test_error_display() {
assert_eq!(
format!("{}", Error(ErrorKind::InternalError)),
"internal parser error"
);
assert_eq!(
format!("{}", Error(ErrorKind::InvalidURI)),
"unable to parse URI component"
);
assert_eq!(
format!("{}", Error(ErrorKind::MalformedParam)),
"malformed parameter list"
);
assert_eq!(
format!("{}", Error(ErrorKind::MalformedQuery)),
"malformed URI query"
);
assert_eq!(
format!("{}", Error(ErrorKind::MissingRel)),
"missing 'rel' parameter"
);
}
#[test]
fn test_error_from() {
let e1 = Error(ErrorKind::InternalError);
let e2 = Error::from(&e1);
assert_eq!(e1, e2);
}
}