lib_humus/templating/
manifest.rs1use serde::{Deserialize, Serialize};
6pub use tinystr::TinyAsciiStr;
7
8use std::collections::HashMap;
9
10use crate::headers::AcceptHeaderItem;
11use crate::templating::{HumusApiFormat, MediaType};
12
13pub type HumusFormatIdentifier = TinyAsciiStr<16>;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct TemplatesManifest {
21 pub default_format: HumusFormatIdentifier,
23
24 pub format: HashMap<HumusFormatIdentifier, FormatDescription>,
26
27 #[serde(default)]
29 pub media_type_aliases: HashMap<AcceptHeaderItem, HumusFormatIdentifier>,
30
31 #[serde(default)]
33 pub format_by_user_agent_prefix: HashMap<String, HumusFormatIdentifier>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct FormatDescription {
39 pub media_type: MediaType,
41
42 pub extension: String,
46
47 #[serde(default)]
49 pub additional_templates: Vec<String>,
50}
51
52impl TemplatesManifest {
53 fn is_valid_format<ApiFormat: HumusApiFormat>(&self, format: &HumusFormatIdentifier) -> bool {
54 self.format.contains_key(format) || ApiFormat::from_name(format).is_some()
55 }
56
57 pub fn find_problems<ApiFormat: HumusApiFormat>(&self) -> Vec<TemplatesManifestProblem> {
59 let mut problems = vec![];
60
61 if !self.is_valid_format::<ApiFormat>(&self.default_format) {
63 problems.push(TemplatesManifestProblem::DefaultReferencesNonSpecifiedFormat);
64 }
65
66 let mut media_type_to_formats: HashMap<AcceptHeaderItem, Vec<HumusFormatIdentifier>> =
67 HashMap::new();
68
69 for (format, description) in self.format.iter() {
70 if ApiFormat::from_name(format).is_some() {
72 problems.push(
73 TemplatesManifestProblem::SpecifiedFormatOverlapsWithApiFormat {
74 format: *format,
75 },
76 );
77 }
78
79 if description.extension.starts_with(".") || description.extension.ends_with(".") {
81 problems.push(
82 TemplatesManifestProblem::ExtensionShouldNotStartOrEndWithDot {
83 format: *format,
84 },
85 )
86 }
87
88 let media_type_string = description.media_type.to_string();
89
90 if media_type_string != description.media_type.essence_str() {
91 media_type_to_formats
92 .entry(AcceptHeaderItem::MediaType(
93 description.media_type.essence_str().to_string(),
94 ))
95 .or_default()
96 .push(*format);
97 }
98
99 media_type_to_formats
100 .entry(AcceptHeaderItem::MediaType(media_type_string))
101 .or_default()
102 .push(*format);
103
104 media_type_to_formats
105 .entry(AcceptHeaderItem::PartialWildcard(
106 description.media_type.type_().to_string(),
107 ))
108 .or_default()
109 .push(*format);
110 }
111
112 for api_format in ApiFormat::get_all() {
113 let format = api_format.get_name();
114 let media_type_string = api_format.get_media_type().to_string();
115
116 if media_type_string != api_format.get_media_type().essence_str() {
117 media_type_to_formats
118 .entry(AcceptHeaderItem::MediaType(
119 api_format.get_media_type().essence_str().to_owned(),
120 ))
121 .or_default()
122 .push(format);
123 }
124
125 media_type_to_formats
126 .entry(AcceptHeaderItem::MediaType(media_type_string))
127 .or_default()
128 .push(format);
129
130 media_type_to_formats
131 .entry(AcceptHeaderItem::PartialWildcard(
132 api_format.get_media_type().type_().to_string(),
133 ))
134 .or_default()
135 .push(format);
136 }
137
138 for (accept_header_item, formats) in media_type_to_formats {
139 if formats.len() <= 1 || self.media_type_aliases.contains_key(&accept_header_item) {
140 continue;
141 }
142
143 problems.push(TemplatesManifestProblem::AmbigiousMediaType {
144 media_type: accept_header_item,
145 formats,
146 });
147 }
148
149 for (accept_header_item, format) in self.media_type_aliases.iter() {
150 if matches!(accept_header_item, AcceptHeaderItem::Wildcard) {
151 problems.push(TemplatesManifestProblem::BlankWildcardMediaTypeAliasNotSupported);
152 continue;
153 }
154 if !self.is_valid_format::<ApiFormat>(format) {
155 problems.push(
156 TemplatesManifestProblem::MediaTypeAliasReferencesNonSpecifiedFormat {
157 accept_header_item: accept_header_item.clone(),
158 },
159 )
160 }
161 }
162
163 for (prefix, format) in self.format_by_user_agent_prefix.iter() {
164 if !self.is_valid_format::<ApiFormat>(format) {
165 problems.push(
166 TemplatesManifestProblem::DefaultByUserAgentReferencesNonSpecifiedFormat {
167 prefix: prefix.clone(),
168 },
169 );
170 }
171 }
172
173 problems
174 }
175}
176
177#[derive(Debug, thiserror::Error)]
179pub enum TemplatesManifestProblem {
180 #[error("The File extension at format.{format}.extension may not start or end with a dot.")]
181 ExtensionShouldNotStartOrEndWithDot { format: HumusFormatIdentifier },
182
183 #[error(
184 "The format specified at format.{format} overlaps with an API format that uses the same id, please change the format id."
185 )]
186 SpecifiedFormatOverlapsWithApiFormat { format: HumusFormatIdentifier },
187
188 #[error("The default_format references a format that isn't specified as format or API format.")]
189 DefaultReferencesNonSpecifiedFormat,
190
191 #[error(
192 "The media_type_aliases.{accept_header_item:?} references a format that isn't specified as format or API format."
193 )]
194 MediaTypeAliasReferencesNonSpecifiedFormat {
195 accept_header_item: AcceptHeaderItem,
196 },
197
198 #[error(
199 "The default_format_by_user_agent_prefix.{prefix:?} references a format that isn't specified as format or API format."
200 )]
201 DefaultByUserAgentReferencesNonSpecifiedFormat { prefix: String },
202
203 #[error(
204 "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."
205 )]
206 AmbigiousMediaType {
207 media_type: AcceptHeaderItem,
208 formats: Vec<HumusFormatIdentifier>,
209 },
210
211 #[error(
212 "Using '*/*' as a key in media_type_aliases is not supported, use the default_format for this case."
213 )]
214 BlankWildcardMediaTypeAliasNotSupported,
215}