use serde::{Deserialize, Serialize};
pub use tinystr::TinyAsciiStr;
use std::collections::HashMap;
use crate::headers::AcceptHeaderItem;
use crate::templating::{HumusApiFormat, MediaType};
pub type HumusFormatIdentifier = TinyAsciiStr<16>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplatesManifest {
pub default_format: HumusFormatIdentifier,
pub format: HashMap<HumusFormatIdentifier, FormatDescription>,
#[serde(default)]
pub media_type_aliases: HashMap<AcceptHeaderItem, HumusFormatIdentifier>,
#[serde(default)]
pub format_by_user_agent_prefix: HashMap<String, HumusFormatIdentifier>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormatDescription {
pub media_type: MediaType,
pub extension: String,
#[serde(default)]
pub additional_templates: Vec<String>,
}
impl TemplatesManifest {
fn is_valid_format<ApiFormat: HumusApiFormat>(&self, format: &HumusFormatIdentifier) -> bool {
self.format.contains_key(format) || ApiFormat::from_name(format).is_some()
}
pub fn find_problems<ApiFormat: HumusApiFormat>(&self) -> Vec<TemplatesManifestProblem> {
let mut problems = vec![];
if !self.is_valid_format::<ApiFormat>(&self.default_format) {
problems.push(TemplatesManifestProblem::DefaultReferencesNonSpecifiedFormat);
}
let mut media_type_to_formats: HashMap<AcceptHeaderItem, Vec<HumusFormatIdentifier>> =
HashMap::new();
for (format, description) in self.format.iter() {
if ApiFormat::from_name(format).is_some() {
problems.push(
TemplatesManifestProblem::SpecifiedFormatOverlapsWithApiFormat {
format: *format,
},
);
}
if description.extension.starts_with(".") || description.extension.ends_with(".") {
problems.push(
TemplatesManifestProblem::ExtensionShouldNotStartOrEndWithDot {
format: *format,
},
)
}
let media_type_string = description.media_type.to_string();
if media_type_string != description.media_type.essence_str() {
media_type_to_formats
.entry(AcceptHeaderItem::MediaType(
description.media_type.essence_str().to_string(),
))
.or_default()
.push(*format);
}
media_type_to_formats
.entry(AcceptHeaderItem::MediaType(media_type_string))
.or_default()
.push(*format);
media_type_to_formats
.entry(AcceptHeaderItem::PartialWildcard(
description.media_type.type_().to_string(),
))
.or_default()
.push(*format);
}
for api_format in ApiFormat::get_all() {
let format = api_format.get_name();
let media_type_string = api_format.get_media_type().to_string();
if media_type_string != api_format.get_media_type().essence_str() {
media_type_to_formats
.entry(AcceptHeaderItem::MediaType(
api_format.get_media_type().essence_str().to_owned(),
))
.or_default()
.push(format);
}
media_type_to_formats
.entry(AcceptHeaderItem::MediaType(media_type_string))
.or_default()
.push(format);
media_type_to_formats
.entry(AcceptHeaderItem::PartialWildcard(
api_format.get_media_type().type_().to_string(),
))
.or_default()
.push(format);
}
for (accept_header_item, formats) in media_type_to_formats {
if formats.len() <= 1 || self.media_type_aliases.contains_key(&accept_header_item) {
continue;
}
problems.push(TemplatesManifestProblem::AmbigiousMediaType {
media_type: accept_header_item,
formats,
});
}
for (accept_header_item, format) in self.media_type_aliases.iter() {
if matches!(accept_header_item, AcceptHeaderItem::Wildcard) {
problems.push(TemplatesManifestProblem::BlankWildcardMediaTypeAliasNotSupported);
continue;
}
if !self.is_valid_format::<ApiFormat>(format) {
problems.push(
TemplatesManifestProblem::MediaTypeAliasReferencesNonSpecifiedFormat {
accept_header_item: accept_header_item.clone(),
},
)
}
}
for (prefix, format) in self.format_by_user_agent_prefix.iter() {
if !self.is_valid_format::<ApiFormat>(format) {
problems.push(
TemplatesManifestProblem::DefaultByUserAgentReferencesNonSpecifiedFormat {
prefix: prefix.clone(),
},
);
}
}
problems
}
}
#[derive(Debug, thiserror::Error)]
pub enum TemplatesManifestProblem {
#[error("The File extension at format.{format}.extension may not start or end with a dot.")]
ExtensionShouldNotStartOrEndWithDot { format: HumusFormatIdentifier },
#[error(
"The format specified at format.{format} overlaps with an API format that uses the same id, please change the format id."
)]
SpecifiedFormatOverlapsWithApiFormat { format: HumusFormatIdentifier },
#[error("The default_format references a format that isn't specified as format or API format.")]
DefaultReferencesNonSpecifiedFormat,
#[error(
"The media_type_aliases.{accept_header_item:?} references a format that isn't specified as format or API format."
)]
MediaTypeAliasReferencesNonSpecifiedFormat {
accept_header_item: AcceptHeaderItem,
},
#[error(
"The default_format_by_user_agent_prefix.{prefix:?} references a format that isn't specified as format or API format."
)]
DefaultByUserAgentReferencesNonSpecifiedFormat { prefix: String },
#[error(
"The media_type {media_type:?} is given for multiple formats {formats:?}, which makes it unclear which format should be the default for this media type. Please specify a default media_type using a media type alias."
)]
AmbigiousMediaType {
media_type: AcceptHeaderItem,
formats: Vec<HumusFormatIdentifier>,
},
#[error(
"Using '*/*' as a key in media_type_aliases is not supported, use the default_format for this case."
)]
BlankWildcardMediaTypeAliasNotSupported,
}