use std::collections::HashMap;
use http::{header::ACCEPT, HeaderValue};
use url::Url;
use crate::mf2;
#[derive(Debug, Clone)]
pub struct RelMap(HashMap<String, Vec<url::Url>>);
impl From<HashMap<String, Vec<url::Url>>> for RelMap {
fn from(v: HashMap<String, Vec<url::Url>>) -> Self {
Self(v)
}
}
impl TryInto<HeaderValue> for RelMap {
type Error = crate::Error;
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let link_value = self
.iter()
.flat_map(|(name, urls)| {
urls.iter()
.map(move |url| format!("<{url}>; rel=\"{name}\""))
})
.collect::<Vec<String>>()
.join(" , ");
Ok(HeaderValue::from_str(&link_value)?)
}
}
impl std::ops::Deref for RelMap {
type Target = HashMap<String, Vec<url::Url>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub(crate) fn get_rels_for_header(header_value: &HeaderValue, rel: &str, base_url: &Url) -> Vec<url::Url> {
let header_value = String::from_utf8(header_value.as_bytes().to_vec()).unwrap_or_default();
header_value
.split(',')
.collect::<Vec<&str>>()
.into_iter()
.map(|link_rel: &str| {
let mut parameters = link_rel.split(';').collect::<Vec<&str>>();
let uri_value = parameters.remove(0).to_owned().trim().to_string();
(uri_value, parameters)
})
.filter(|(_uri, parameters)| {
parameters
.iter()
.map(|s| s.to_lowercase())
.map(|s| s.trim().to_string())
.filter(|r| r.starts_with("rel="))
.flat_map(|rv| {
let rel_value = rv.strip_prefix("rel=").unwrap_or("");
rel_value
.trim_matches('"')
.trim_matches('\'')
.split_whitespace()
.map(|s| s.to_string())
.collect::<Vec<_>>()
})
.any(|s| s == rel)
})
.filter_map(|(uri, _parameters)| {
uri.strip_prefix('<')
.map(|u| u.to_string())
.unwrap_or_default()
.strip_suffix('>')
.and_then(|u| u.parse().or_else(|_| base_url.join(u)).ok())
})
.collect::<Vec<Url>>()
}
fn get_rels_from_headers(headers: &http::HeaderMap, rel: &str, base_url: &Url) -> Vec<url::Url> {
headers
.get_all("link")
.into_iter()
.flat_map(|uri_header_value| get_rels_for_header(uri_header_value, rel, base_url))
.collect::<Vec<_>>()
}
#[tracing::instrument(skip(client))]
pub async fn for_url(
client: &impl crate::http::Client,
url: &Url,
rels: &[&str],
method: &str,
) -> Result<RelMap, crate::Error> {
let req = http::Request::builder()
.uri(url.as_str())
.header(ACCEPT, "text/html, text/plain, */*")
.method(method)
.body(Default::default())
.map_err(crate::Error::Http)?;
let resp = client.send_request(req).await?;
let headers = resp.headers().clone();
let header_rels: HashMap<String, Vec<_>> = HashMap::from_iter(
rels.iter()
.map(|rel| (rel.to_string(), get_rels_from_headers(&headers, rel, url))),
);
let mut rels_map = HashMap::from_iter(header_rels);
if method == "GET" {
let document = mf2::http::to_mf2_document(
resp.map(|body| body.as_bytes().to_vec()),
url.as_str(),
)
.map_err(crate::Error::from)?;
let body_rels: HashMap<String, Vec<_>> =
HashMap::from_iter(document.rels.by_rels().into_iter());
for (header, rels) in body_rels {
if !rels_map.contains_key(&header) {
rels_map.insert(header.clone(), Vec::default());
}
if let Some(r) = rels_map.get_mut(&header) {
r.extend(rels)
}
}
}
rels_map = HashMap::from_iter(rels_map.into_iter().filter(|(_, v)| !v.is_empty()));
if !rels_map.is_empty() {
tracing::trace!(rels = format!("{:?}", rels_map), "Found relations.");
} else {
tracing::trace!(rels = format!("{:?}", rels), "No relations found.");
}
Ok(rels_map.into())
}
#[cfg(test)]
mod test;