1use super::generics::{FromAttributes, ParseFrom};
2use crate::util::text::bytes_to_string;
3use serde_json::Value;
4
5#[derive(Debug, Clone, Default)]
7pub struct Link {
8 pub href: String,
10 pub rel: Option<String>,
12 pub link_type: Option<String>,
14 pub title: Option<String>,
16 pub length: Option<u64>,
18 pub hreflang: Option<String>,
20}
21
22impl Link {
23 #[inline]
25 pub fn new(href: impl Into<String>, rel: impl Into<String>) -> Self {
26 Self {
27 href: href.into(),
28 rel: Some(rel.into()),
29 link_type: None,
30 title: None,
31 length: None,
32 hreflang: None,
33 }
34 }
35
36 #[inline]
38 pub fn alternate(href: impl Into<String>) -> Self {
39 Self::new(href, "alternate")
40 }
41
42 #[inline]
44 pub fn self_link(href: impl Into<String>, mime_type: impl Into<String>) -> Self {
45 Self {
46 href: href.into(),
47 rel: Some("self".to_string()),
48 link_type: Some(mime_type.into()),
49 title: None,
50 length: None,
51 hreflang: None,
52 }
53 }
54
55 #[inline]
57 pub fn enclosure(href: impl Into<String>, mime_type: Option<String>) -> Self {
58 Self {
59 href: href.into(),
60 rel: Some("enclosure".to_string()),
61 link_type: mime_type,
62 title: None,
63 length: None,
64 hreflang: None,
65 }
66 }
67
68 #[inline]
70 pub fn related(href: impl Into<String>) -> Self {
71 Self::new(href, "related")
72 }
73
74 #[inline]
76 #[must_use]
77 pub fn with_type(mut self, mime_type: impl Into<String>) -> Self {
78 self.link_type = Some(mime_type.into());
79 self
80 }
81}
82
83#[derive(Debug, Clone, Default)]
85pub struct Person {
86 pub name: Option<String>,
88 pub email: Option<String>,
90 pub uri: Option<String>,
92}
93
94impl Person {
95 #[inline]
108 pub fn from_name(name: impl Into<String>) -> Self {
109 Self {
110 name: Some(name.into()),
111 email: None,
112 uri: None,
113 }
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct Tag {
120 pub term: String,
122 pub scheme: Option<String>,
124 pub label: Option<String>,
126}
127
128impl Tag {
129 #[inline]
131 pub fn new(term: impl Into<String>) -> Self {
132 Self {
133 term: term.into(),
134 scheme: None,
135 label: None,
136 }
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct Image {
143 pub url: String,
145 pub title: Option<String>,
147 pub link: Option<String>,
149 pub width: Option<u32>,
151 pub height: Option<u32>,
153 pub description: Option<String>,
155}
156
157#[derive(Debug, Clone)]
159pub struct Enclosure {
160 pub url: String,
162 pub length: Option<u64>,
164 pub enclosure_type: Option<String>,
166}
167
168#[derive(Debug, Clone)]
170pub struct Content {
171 pub value: String,
173 pub content_type: Option<String>,
175 pub language: Option<String>,
177 pub base: Option<String>,
179}
180
181impl Content {
182 #[inline]
184 pub fn html(value: impl Into<String>) -> Self {
185 Self {
186 value: value.into(),
187 content_type: Some("text/html".to_string()),
188 language: None,
189 base: None,
190 }
191 }
192
193 #[inline]
195 pub fn plain(value: impl Into<String>) -> Self {
196 Self {
197 value: value.into(),
198 content_type: Some("text/plain".to_string()),
199 language: None,
200 base: None,
201 }
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum TextType {
208 Text,
210 Html,
212 Xhtml,
214}
215
216#[derive(Debug, Clone)]
218pub struct TextConstruct {
219 pub value: String,
221 pub content_type: TextType,
223 pub language: Option<String>,
225 pub base: Option<String>,
227}
228
229impl TextConstruct {
230 #[inline]
232 pub fn text(value: impl Into<String>) -> Self {
233 Self {
234 value: value.into(),
235 content_type: TextType::Text,
236 language: None,
237 base: None,
238 }
239 }
240
241 #[inline]
243 pub fn html(value: impl Into<String>) -> Self {
244 Self {
245 value: value.into(),
246 content_type: TextType::Html,
247 language: None,
248 base: None,
249 }
250 }
251
252 #[inline]
254 #[must_use]
255 pub fn with_language(mut self, language: impl Into<String>) -> Self {
256 self.language = Some(language.into());
257 self
258 }
259}
260
261#[derive(Debug, Clone)]
263pub struct Generator {
264 pub value: String,
266 pub uri: Option<String>,
268 pub version: Option<String>,
270}
271
272#[derive(Debug, Clone)]
274pub struct Source {
275 pub title: Option<String>,
277 pub link: Option<String>,
279 pub id: Option<String>,
281}
282
283#[derive(Debug, Clone)]
285pub struct MediaThumbnail {
286 pub url: String,
288 pub width: Option<u32>,
290 pub height: Option<u32>,
292}
293
294#[derive(Debug, Clone)]
296pub struct MediaContent {
297 pub url: String,
299 pub content_type: Option<String>,
301 pub filesize: Option<u64>,
303 pub width: Option<u32>,
305 pub height: Option<u32>,
307 pub duration: Option<u64>,
309}
310
311impl FromAttributes for Link {
312 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
313 where
314 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
315 {
316 let mut href = None;
317 let mut rel = None;
318 let mut link_type = None;
319 let mut title = None;
320 let mut hreflang = None;
321 let mut length = None;
322
323 for attr in attrs {
324 if attr.value.len() > max_attr_length {
325 continue;
326 }
327 match attr.key.as_ref() {
328 b"href" => href = Some(bytes_to_string(&attr.value)),
329 b"rel" => rel = Some(bytes_to_string(&attr.value)),
330 b"type" => link_type = Some(bytes_to_string(&attr.value)),
331 b"title" => title = Some(bytes_to_string(&attr.value)),
332 b"hreflang" => hreflang = Some(bytes_to_string(&attr.value)),
333 b"length" => length = bytes_to_string(&attr.value).parse().ok(),
334 _ => {}
335 }
336 }
337
338 href.map(|href| Self {
339 href,
340 rel: rel.or_else(|| Some("alternate".to_string())),
341 link_type,
342 title,
343 length,
344 hreflang,
345 })
346 }
347}
348
349impl FromAttributes for Tag {
350 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
351 where
352 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
353 {
354 let mut term = None;
355 let mut scheme = None;
356 let mut label = None;
357
358 for attr in attrs {
359 if attr.value.len() > max_attr_length {
360 continue;
361 }
362
363 match attr.key.as_ref() {
364 b"term" => term = Some(bytes_to_string(&attr.value)),
365 b"scheme" | b"domain" => scheme = Some(bytes_to_string(&attr.value)),
366 b"label" => label = Some(bytes_to_string(&attr.value)),
367 _ => {}
368 }
369 }
370
371 term.map(|term| Self {
372 term,
373 scheme,
374 label,
375 })
376 }
377}
378
379impl FromAttributes for Enclosure {
380 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
381 where
382 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
383 {
384 let mut url = None;
385 let mut length = None;
386 let mut enclosure_type = None;
387
388 for attr in attrs {
389 if attr.value.len() > max_attr_length {
390 continue;
391 }
392
393 match attr.key.as_ref() {
394 b"url" => url = Some(bytes_to_string(&attr.value)),
395 b"length" => length = bytes_to_string(&attr.value).parse().ok(),
396 b"type" => enclosure_type = Some(bytes_to_string(&attr.value)),
397 _ => {}
398 }
399 }
400
401 url.map(|url| Self {
402 url,
403 length,
404 enclosure_type,
405 })
406 }
407}
408
409impl FromAttributes for MediaThumbnail {
410 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
411 where
412 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
413 {
414 let mut url = None;
415 let mut width = None;
416 let mut height = None;
417
418 for attr in attrs {
419 if attr.value.len() > max_attr_length {
420 continue;
421 }
422
423 match attr.key.as_ref() {
424 b"url" => url = Some(bytes_to_string(&attr.value)),
425 b"width" => width = bytes_to_string(&attr.value).parse().ok(),
426 b"height" => height = bytes_to_string(&attr.value).parse().ok(),
427 _ => {}
428 }
429 }
430
431 url.map(|url| Self { url, width, height })
432 }
433}
434
435impl FromAttributes for MediaContent {
436 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
437 where
438 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
439 {
440 let mut url = None;
441 let mut content_type = None;
442 let mut filesize = None;
443 let mut width = None;
444 let mut height = None;
445 let mut duration = None;
446
447 for attr in attrs {
448 if attr.value.len() > max_attr_length {
449 continue;
450 }
451
452 match attr.key.as_ref() {
453 b"url" => url = Some(bytes_to_string(&attr.value)),
454 b"type" => content_type = Some(bytes_to_string(&attr.value)),
455 b"fileSize" => filesize = bytes_to_string(&attr.value).parse().ok(),
456 b"width" => width = bytes_to_string(&attr.value).parse().ok(),
457 b"height" => height = bytes_to_string(&attr.value).parse().ok(),
458 b"duration" => duration = bytes_to_string(&attr.value).parse().ok(),
459 _ => {}
460 }
461 }
462
463 url.map(|url| Self {
464 url,
465 content_type,
466 filesize,
467 width,
468 height,
469 duration,
470 })
471 }
472}
473
474impl ParseFrom<&Value> for Person {
477 fn parse_from(json: &Value) -> Option<Self> {
481 json.as_object().map(|obj| Self {
482 name: obj.get("name").and_then(Value::as_str).map(String::from),
483 email: None, uri: obj.get("url").and_then(Value::as_str).map(String::from),
485 })
486 }
487}
488
489impl ParseFrom<&Value> for Enclosure {
490 fn parse_from(json: &Value) -> Option<Self> {
494 let obj = json.as_object()?;
495 let url = obj.get("url").and_then(Value::as_str)?;
496 Some(Self {
497 url: url.to_string(),
498 length: obj.get("size_in_bytes").and_then(Value::as_u64),
499 enclosure_type: obj
500 .get("mime_type")
501 .and_then(Value::as_str)
502 .map(String::from),
503 })
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use serde_json::json;
511
512 #[test]
513 fn test_link_default() {
514 let link = Link::default();
515 assert!(link.href.is_empty());
516 assert!(link.rel.is_none());
517 }
518
519 #[test]
520 fn test_link_builders() {
521 let link = Link::alternate("https://example.com");
522 assert_eq!(link.href, "https://example.com");
523 assert_eq!(link.rel.as_deref(), Some("alternate"));
524
525 let link = Link::self_link("https://example.com/feed", "application/feed+json");
526 assert_eq!(link.rel.as_deref(), Some("self"));
527 assert_eq!(link.link_type.as_deref(), Some("application/feed+json"));
528
529 let link = Link::enclosure(
530 "https://example.com/audio.mp3",
531 Some("audio/mpeg".to_string()),
532 );
533 assert_eq!(link.rel.as_deref(), Some("enclosure"));
534 assert_eq!(link.link_type.as_deref(), Some("audio/mpeg"));
535
536 let link = Link::related("https://other.com");
537 assert_eq!(link.rel.as_deref(), Some("related"));
538 }
539
540 #[test]
541 fn test_tag_builder() {
542 let tag = Tag::new("rust");
543 assert_eq!(tag.term, "rust");
544 assert!(tag.scheme.is_none());
545 }
546
547 #[test]
548 fn test_text_construct_builders() {
549 let text = TextConstruct::text("Hello");
550 assert_eq!(text.value, "Hello");
551 assert_eq!(text.content_type, TextType::Text);
552
553 let html = TextConstruct::html("<p>Hello</p>");
554 assert_eq!(html.content_type, TextType::Html);
555
556 let with_lang = TextConstruct::text("Hello").with_language("en");
557 assert_eq!(with_lang.language.as_deref(), Some("en"));
558 }
559
560 #[test]
561 fn test_content_builders() {
562 let html = Content::html("<p>Content</p>");
563 assert_eq!(html.content_type.as_deref(), Some("text/html"));
564
565 let plain = Content::plain("Content");
566 assert_eq!(plain.content_type.as_deref(), Some("text/plain"));
567 }
568
569 #[test]
570 fn test_person_default() {
571 let person = Person::default();
572 assert!(person.name.is_none());
573 assert!(person.email.is_none());
574 assert!(person.uri.is_none());
575 }
576
577 #[test]
578 fn test_person_parse_from_json() {
579 let json = json!({"name": "John Doe", "url": "https://example.com"});
580 let person = Person::parse_from(&json).unwrap();
581 assert_eq!(person.name.as_deref(), Some("John Doe"));
582 assert_eq!(person.uri.as_deref(), Some("https://example.com"));
583 assert!(person.email.is_none());
584 }
585
586 #[test]
587 fn test_person_parse_from_empty_json() {
588 let json = json!({});
589 let person = Person::parse_from(&json).unwrap();
590 assert!(person.name.is_none());
591 }
592
593 #[test]
594 fn test_enclosure_parse_from_json() {
595 let json = json!({
596 "url": "https://example.com/file.mp3",
597 "mime_type": "audio/mpeg",
598 "size_in_bytes": 12345
599 });
600 let enclosure = Enclosure::parse_from(&json).unwrap();
601 assert_eq!(enclosure.url, "https://example.com/file.mp3");
602 assert_eq!(enclosure.enclosure_type.as_deref(), Some("audio/mpeg"));
603 assert_eq!(enclosure.length, Some(12345));
604 }
605
606 #[test]
607 fn test_enclosure_parse_from_json_missing_url() {
608 let json = json!({"mime_type": "audio/mpeg"});
609 assert!(Enclosure::parse_from(&json).is_none());
610 }
611
612 #[test]
613 fn test_text_type_equality() {
614 assert_eq!(TextType::Text, TextType::Text);
615 assert_ne!(TextType::Text, TextType::Html);
616 }
617}