use regex::Regex;
use url::Url;
use crate::shared::MediaType;
#[derive(Debug, Clone, Default)]
pub struct SupportedUrls {
entries: Vec<(String, Vec<Regex>)>,
}
impl SupportedUrls {
#[must_use]
pub fn none() -> Self {
Self::default()
}
#[must_use]
pub fn all() -> Self {
Self::default().with("*", [any_url()])
}
#[must_use]
pub fn with(
mut self,
media_type_pattern: &str,
patterns: impl IntoIterator<Item = Regex>,
) -> Self {
self.insert(media_type_pattern, patterns);
self
}
pub fn insert(&mut self, media_type_pattern: &str, patterns: impl IntoIterator<Item = Regex>) {
let key = media_type_pattern.trim().to_ascii_lowercase();
if let Some((_, existing)) = self.entries.iter_mut().find(|(k, _)| *k == key) {
existing.extend(patterns);
} else {
self.entries.push((key, patterns.into_iter().collect()));
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.iter().all(|(_, patterns)| patterns.is_empty())
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &[Regex])> + '_ {
self.entries
.iter()
.map(|(key, patterns)| (key.as_str(), patterns.as_slice()))
}
#[must_use]
pub fn supports(&self, media_type: &MediaType, url: &Url) -> bool {
let url = url.as_str().to_ascii_lowercase();
let media_type = media_type.as_str().trim().to_ascii_lowercase();
let is_top_level_only = !media_type.contains('/');
self.entries
.iter()
.filter(|(key, _)| {
let prefix = if key == "*" || key == "*/*" {
String::new()
} else {
key.replacen('*', "", 1)
};
if prefix.is_empty() {
return true;
}
if is_top_level_only {
return format!("{media_type}/") == prefix;
}
media_type.starts_with(&prefix)
})
.flat_map(|(_, patterns)| patterns.iter())
.any(|pattern| pattern.is_match(&url))
}
}
#[must_use]
pub fn any_url() -> Regex {
#[allow(clippy::expect_used, reason = "constant pattern always compiles")]
Regex::new(".*").expect("constant regex")
}