use glob::{Pattern, PatternError};
use indexmap::{IndexMap, indexmap};
use mime::Mime;
use serde::{Deserialize, Serialize};
use slumber_util::yaml::{
self, DeserializeYaml, Expected, LocatedError, SourceMap, SourcedYaml,
};
use std::str::FromStr;
#[derive(Debug, Serialize)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct MimeMap<V> {
patterns: IndexMap<MimePattern, V>,
}
impl<V> MimeMap<V> {
pub fn get(
&self,
mime_overrides: &MimeOverrideMap,
mime: &Mime,
) -> Option<&V> {
let mime = mime_overrides.get(mime);
self.get_inner(mime)
}
fn get_inner(&self, mime: &Mime) -> Option<&V> {
let essence_str = mime.essence_str();
self.patterns
.iter()
.find(|(pattern, _)| pattern.matches(essence_str))
.map(|(_, value)| value)
}
}
impl<V> Default for MimeMap<V> {
fn default() -> Self {
Self {
patterns: Default::default(),
}
}
}
impl<V: DeserializeYaml> DeserializeYaml for MimeMap<V> {
fn expected() -> Expected {
Expected::OneOf(&[&Expected::String, &Expected::Mapping])
}
fn deserialize(
yaml: SourcedYaml,
source_map: &SourceMap,
) -> yaml::Result<Self> {
let patterns = if yaml.data.is_mapping() {
IndexMap::<MimePattern, V>::deserialize(yaml, source_map)?
} else {
let value = V::deserialize(yaml, source_map)?;
indexmap! { MimePattern::default() => value }
};
Ok(Self { patterns })
}
}
#[derive(Debug, Default, Serialize)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(
feature = "schema",
derive(schemars::JsonSchema),
schemars(example = Self::example()),
)]
#[serde(transparent)]
pub struct MimeOverrideMap(MimeMap<MimeAdopt>);
impl MimeOverrideMap {
pub fn get<'a>(&'a self, mime: &'a Mime) -> &'a Mime {
self.0.get_inner(mime).map(|mime| &mime.0).unwrap_or(mime)
}
#[cfg(feature = "schema")]
fn example() -> Self {
Self::from_iter([("text/javascript", mime::APPLICATION_JSON)])
}
}
impl FromIterator<(&'static str, Mime)> for MimeOverrideMap {
fn from_iter<T: IntoIterator<Item = (&'static str, Mime)>>(
iter: T,
) -> Self {
let patterns = iter
.into_iter()
.map(|(key, value)| (key.parse().unwrap(), MimeAdopt(value)))
.collect();
Self(MimeMap { patterns })
}
}
impl DeserializeYaml for MimeOverrideMap {
fn expected() -> Expected {
Expected::Mapping
}
fn deserialize(
yaml: SourcedYaml,
source_map: &SourceMap,
) -> yaml::Result<Self> {
let patterns =
IndexMap::<MimePattern, MimeAdopt>::deserialize(yaml, source_map)?;
Ok(Self(MimeMap { patterns }))
}
}
#[derive(Debug, PartialEq)]
struct MimeAdopt(Mime);
impl Serialize for MimeAdopt {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.as_ref().serialize(serializer)
}
}
impl DeserializeYaml for MimeAdopt {
fn expected() -> Expected {
Expected::String
}
fn deserialize(
yaml: SourcedYaml,
_source_map: &SourceMap,
) -> yaml::Result<Self> {
let location = yaml.location;
let s = yaml.try_into_string()?;
let mime: Mime = s
.parse()
.map_err(|error| LocatedError::other(error, location))?;
Ok(Self(mime))
}
}
#[cfg(feature = "schema")]
impl schemars::JsonSchema for MimeAdopt {
fn schema_name() -> std::borrow::Cow<'static, str> {
String::schema_name()
}
fn json_schema(
generator: &mut schemars::SchemaGenerator,
) -> schemars::Schema {
String::json_schema(generator)
}
}
#[derive(
Clone,
Debug,
derive_more::Display,
derive_more::Deref,
Serialize,
Deserialize,
Eq,
Hash,
PartialEq,
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(try_from = "String", into = "String")]
struct MimePattern(Pattern);
impl Default for MimePattern {
fn default() -> Self {
Self(Pattern::from_str("*/*").unwrap())
}
}
impl From<MimePattern> for String {
fn from(value: MimePattern) -> Self {
value.to_string()
}
}
impl FromStr for MimePattern {
type Err = PatternError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let dealiased = match s {
"default" => "*/*",
"image" => "image/*",
"json" => "application/*json",
other => other,
};
Ok(Self(dealiased.parse()?))
}
}
impl TryFrom<String> for MimePattern {
type Error = PatternError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl DeserializeYaml for MimePattern {
fn expected() -> Expected {
Expected::String
}
fn deserialize(
yaml: SourcedYaml,
_source_map: &SourceMap,
) -> yaml::Result<Self> {
let location = yaml.location;
let s = yaml.try_into_string()?;
s.parse()
.map_err(|error| LocatedError::other(error, location))
}
}
#[cfg(test)]
mod tests {
use super::*;
use mime::APPLICATION_JSON;
use rstest::rstest;
use serde_yaml::Mapping;
use slumber_util::yaml::deserialize_yaml;
fn map(entries: &[(&str, &str)]) -> MimeMap<String> {
MimeMap {
patterns: entries
.iter()
.map(|(k, v)| {
(k.parse::<MimePattern>().unwrap(), (*v).to_owned())
})
.collect(),
}
}
#[rstest]
#[case::string("test", map(&[("*/*", "test")]))]
#[case::empty_map(serde_yaml::Value::Mapping(Mapping::default()), map(&[]))]
#[case::aliases(serde_yaml::Value::Mapping([
("json".into(), "json-value".into()),
("image".into(), "image-value".into()),
("default".into(), "default-value".into()),
].into_iter().collect()),
map(&[
("application/*json", "json-value"),
("image/*","image-value"),
("*/*", "default-value"),
]),
)]
fn test_deserialize(
#[case] yaml: impl Into<serde_yaml::Value>,
#[case] expected: MimeMap<String>,
) {
assert_eq!(
deserialize_yaml::<MimeMap<String>>(yaml.into()).unwrap(),
expected
);
}
#[rstest]
#[case::wildcard("text/plain", "text")]
#[case::default("image/png", "default")]
#[case::priority("audio/ogg", "default")]
#[case::json_plain("application/json", "json")]
#[case::json_extension("application/ld+json", "json")]
#[case::essence("text/csv; charset=utf-8", "csv")]
#[case::override_mime("text/override; charset=utf-8", "json")]
fn test_match_mimes(#[case] mime: Mime, #[case] expected: String) {
let map = map(&[
("text/csv", "csv"),
("text/*", "text"),
("json", "json"),
("*/*", "default"),
("audio/*", "audio"),
]);
let overrides =
MimeOverrideMap::from_iter([("text/override", APPLICATION_JSON)]);
let actual = map.get(&overrides, &mime).unwrap();
assert_eq!(actual, &expected);
}
}