1use std::fmt;
2use std::str::FromStr;
3
4#[derive(
5 Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, PartialOrd, Ord,
6)]
7#[serde(transparent)]
8pub struct AttachmentId(String);
9
10impl AttachmentId {
11 pub fn new(id: impl Into<String>) -> Self {
12 Self(id.into())
13 }
14
15 pub fn as_str(&self) -> &str {
16 &self.0
17 }
18}
19
20impl fmt::Display for AttachmentId {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 f.write_str(&self.0)
23 }
24}
25
26impl From<String> for AttachmentId {
27 fn from(value: String) -> Self {
28 Self::new(value)
29 }
30}
31
32impl From<&str> for AttachmentId {
33 fn from(value: &str) -> Self {
34 Self::new(value)
35 }
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct InvalidMediaType {
40 value: String,
41}
42
43impl fmt::Display for InvalidMediaType {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 write!(
46 f,
47 "invalid media type `{}`: expected a syntactically valid type/subtype",
48 self.value
49 )
50 }
51}
52
53impl std::error::Error for InvalidMediaType {}
54
55#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
60pub struct MediaType(String);
61
62impl MediaType {
63 pub fn parse(value: impl AsRef<str>) -> Result<Self, InvalidMediaType> {
64 let original = value.as_ref();
65 let normalized = original.trim().to_ascii_lowercase();
66 let mut pieces = normalized.split('/');
67 let type_name = pieces.next().unwrap_or_default();
68 let subtype = pieces.next().unwrap_or_default();
69 if pieces.next().is_some() || !is_mime_token(type_name) || !is_mime_token(subtype) {
70 return Err(InvalidMediaType {
71 value: original.to_string(),
72 });
73 }
74 Ok(Self(normalized))
75 }
76
77 pub fn as_str(&self) -> &str {
78 &self.0
79 }
80
81 pub fn family(&self) -> &str {
82 self.0.split_once('/').map_or("", |(family, _)| family)
83 }
84
85 pub fn is_image(&self) -> bool {
86 self.family() == "image"
87 }
88}
89
90fn is_mime_token(value: &str) -> bool {
91 !value.is_empty()
92 && value.bytes().all(|byte| {
93 byte.is_ascii_alphanumeric()
94 || matches!(
95 byte,
96 b'!' | b'#'
97 | b'$'
98 | b'%'
99 | b'&'
100 | b'\''
101 | b'*'
102 | b'+'
103 | b'-'
104 | b'.'
105 | b'^'
106 | b'_'
107 | b'`'
108 | b'|'
109 | b'~'
110 )
111 })
112}
113
114impl fmt::Display for MediaType {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 f.write_str(&self.0)
117 }
118}
119
120impl FromStr for MediaType {
121 type Err = InvalidMediaType;
122
123 fn from_str(value: &str) -> Result<Self, Self::Err> {
124 Self::parse(value)
125 }
126}
127
128impl TryFrom<String> for MediaType {
129 type Error = InvalidMediaType;
130
131 fn try_from(value: String) -> Result<Self, Self::Error> {
132 Self::parse(value)
133 }
134}
135
136impl serde::Serialize for MediaType {
137 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138 where
139 S: serde::Serializer,
140 {
141 serializer.serialize_str(self.as_str())
142 }
143}
144
145impl<'de> serde::Deserialize<'de> for MediaType {
146 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147 where
148 D: serde::Deserializer<'de>,
149 {
150 let value = String::deserialize(deserializer)?;
151 Self::parse(value).map_err(serde::de::Error::custom)
152 }
153}
154
155#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
156#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
157pub enum AttachmentTypeMetadata {
158 Image {
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 width: Option<u32>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 height: Option<u32>,
163 },
164}
165
166impl AttachmentTypeMetadata {
167 pub fn image(width: Option<u32>, height: Option<u32>) -> Self {
168 Self::Image { width, height }
169 }
170}
171
172#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
173pub struct AttachmentCreateMeta {
174 pub media_type: MediaType,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub type_metadata: Option<AttachmentTypeMetadata>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub label: Option<String>,
179}
180
181impl AttachmentCreateMeta {
182 pub fn new(
183 media_type: MediaType,
184 type_metadata: Option<AttachmentTypeMetadata>,
185 label: Option<String>,
186 ) -> Self {
187 Self {
188 media_type,
189 type_metadata,
190 label,
191 }
192 }
193}
194
195#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
196pub struct AttachmentMeta {
197 pub id: AttachmentId,
198 pub media_type: MediaType,
199 pub byte_len: u64,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub type_metadata: Option<AttachmentTypeMetadata>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub label: Option<String>,
204}
205
206impl AttachmentMeta {
207 pub fn new(
208 id: AttachmentId,
209 media_type: MediaType,
210 byte_len: u64,
211 type_metadata: Option<AttachmentTypeMetadata>,
212 label: Option<String>,
213 ) -> Self {
214 Self {
215 id,
216 media_type,
217 byte_len,
218 type_metadata,
219 label,
220 }
221 }
222
223 pub fn as_ref(&self) -> AttachmentRef {
224 AttachmentRef {
225 id: self.id.clone(),
226 media_type: self.media_type.clone(),
227 byte_len: self.byte_len,
228 type_metadata: self.type_metadata.clone(),
229 label: self.label.clone(),
230 }
231 }
232}
233
234#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
235pub struct AttachmentRef {
236 pub id: AttachmentId,
237 pub media_type: MediaType,
238 pub byte_len: u64,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub type_metadata: Option<AttachmentTypeMetadata>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub label: Option<String>,
243}
244
245impl AttachmentRef {
246 pub fn meta(&self) -> AttachmentMeta {
247 AttachmentMeta {
248 id: self.id.clone(),
249 media_type: self.media_type.clone(),
250 byte_len: self.byte_len,
251 type_metadata: self.type_metadata.clone(),
252 label: self.label.clone(),
253 }
254 }
255
256 pub fn media_type(&self) -> &MediaType {
257 &self.media_type
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn media_type_accepts_any_valid_type_and_normalizes_case() {
267 assert_eq!(
268 MediaType::parse(" IMAGE/PNG ").unwrap().as_str(),
269 "image/png"
270 );
271 assert_eq!(
272 MediaType::parse("application/vnd.example+json")
273 .unwrap()
274 .as_str(),
275 "application/vnd.example+json"
276 );
277 assert!(MediaType::parse("application/x.foo~bar").is_ok());
278 }
279
280 #[test]
281 fn media_type_rejects_parameters_and_malformed_values() {
282 for invalid in [
283 "image",
284 "/png",
285 "image/",
286 "image/png/extra",
287 "text/plain; charset=utf-8",
288 ] {
289 assert!(MediaType::parse(invalid).is_err(), "accepted {invalid}");
290 }
291 }
292
293 #[test]
294 fn serde_cannot_bypass_media_type_validation() {
295 assert!(serde_json::from_str::<MediaType>(r#""not a mime""#).is_err());
296 }
297}