1use super::generics::{FromAttributes, ParseFrom};
2use crate::util::{date::parse_date, text::bytes_to_string};
3use chrono::{DateTime, Utc};
4use compact_str::CompactString;
5use serde_json::Value;
6use std::ops::Deref;
7use std::sync::Arc;
8
9pub type SmallString = CompactString;
27
28#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
46#[serde(transparent)]
47pub struct Url(String);
48
49impl Url {
50 #[inline]
62 pub fn new(s: impl Into<String>) -> Self {
63 Self(s.into())
64 }
65
66 #[inline]
77 pub fn as_str(&self) -> &str {
78 &self.0
79 }
80
81 #[inline]
93 pub fn into_inner(self) -> String {
94 self.0
95 }
96}
97
98impl Deref for Url {
99 type Target = str;
100
101 #[inline]
102 fn deref(&self) -> &str {
103 &self.0
104 }
105}
106
107impl From<String> for Url {
108 #[inline]
109 fn from(s: String) -> Self {
110 Self(s)
111 }
112}
113
114impl From<&str> for Url {
115 #[inline]
116 fn from(s: &str) -> Self {
117 Self(s.to_string())
118 }
119}
120
121impl AsRef<str> for Url {
122 #[inline]
123 fn as_ref(&self) -> &str {
124 &self.0
125 }
126}
127
128impl std::fmt::Display for Url {
129 #[inline]
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 self.0.fmt(f)
132 }
133}
134
135impl PartialEq<str> for Url {
136 fn eq(&self, other: &str) -> bool {
137 self.0 == other
138 }
139}
140
141impl PartialEq<&str> for Url {
142 fn eq(&self, other: &&str) -> bool {
143 self.0 == *other
144 }
145}
146
147impl PartialEq<String> for Url {
148 fn eq(&self, other: &String) -> bool {
149 &self.0 == other
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Hash)]
171pub struct MimeType(Arc<str>);
172
173impl serde::Serialize for MimeType {
175 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
176 where
177 S: serde::Serializer,
178 {
179 serializer.serialize_str(&self.0)
180 }
181}
182
183impl<'de> serde::Deserialize<'de> for MimeType {
184 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185 where
186 D: serde::Deserializer<'de>,
187 {
188 let s = <String as serde::Deserialize>::deserialize(deserializer)?;
189 Ok(Self::new(s))
190 }
191}
192
193impl MimeType {
194 #[inline]
205 pub fn new(s: impl AsRef<str>) -> Self {
206 Self(Arc::from(s.as_ref()))
207 }
208
209 #[inline]
220 pub fn as_str(&self) -> &str {
221 &self.0
222 }
223
224 pub const TEXT_HTML: &'static str = "text/html";
235
236 pub const TEXT_PLAIN: &'static str = "text/plain";
238
239 pub const APPLICATION_XML: &'static str = "application/xml";
241
242 pub const APPLICATION_JSON: &'static str = "application/json";
244}
245
246impl Default for MimeType {
247 #[inline]
248 fn default() -> Self {
249 Self(Arc::from(""))
250 }
251}
252
253impl Deref for MimeType {
254 type Target = str;
255
256 #[inline]
257 fn deref(&self) -> &str {
258 &self.0
259 }
260}
261
262impl From<String> for MimeType {
263 #[inline]
264 fn from(s: String) -> Self {
265 Self(Arc::from(s.as_str()))
266 }
267}
268
269impl From<&str> for MimeType {
270 #[inline]
271 fn from(s: &str) -> Self {
272 Self(Arc::from(s))
273 }
274}
275
276impl AsRef<str> for MimeType {
277 #[inline]
278 fn as_ref(&self) -> &str {
279 &self.0
280 }
281}
282
283impl std::fmt::Display for MimeType {
284 #[inline]
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 self.0.fmt(f)
287 }
288}
289
290impl PartialEq<str> for MimeType {
291 fn eq(&self, other: &str) -> bool {
292 &*self.0 == other
293 }
294}
295
296impl PartialEq<&str> for MimeType {
297 fn eq(&self, other: &&str) -> bool {
298 &*self.0 == *other
299 }
300}
301
302impl PartialEq<String> for MimeType {
303 fn eq(&self, other: &String) -> bool {
304 &*self.0 == other
305 }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
322#[serde(transparent)]
323pub struct Email(String);
324
325impl Email {
326 #[inline]
337 pub fn new(s: impl Into<String>) -> Self {
338 Self(s.into())
339 }
340
341 #[inline]
352 pub fn as_str(&self) -> &str {
353 &self.0
354 }
355
356 #[inline]
368 pub fn into_inner(self) -> String {
369 self.0
370 }
371}
372
373impl Deref for Email {
374 type Target = str;
375
376 #[inline]
377 fn deref(&self) -> &str {
378 &self.0
379 }
380}
381
382impl From<String> for Email {
383 #[inline]
384 fn from(s: String) -> Self {
385 Self(s)
386 }
387}
388
389impl From<&str> for Email {
390 #[inline]
391 fn from(s: &str) -> Self {
392 Self(s.to_string())
393 }
394}
395
396impl AsRef<str> for Email {
397 #[inline]
398 fn as_ref(&self) -> &str {
399 &self.0
400 }
401}
402
403impl std::fmt::Display for Email {
404 #[inline]
405 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406 self.0.fmt(f)
407 }
408}
409
410impl PartialEq<str> for Email {
411 fn eq(&self, other: &str) -> bool {
412 self.0 == other
413 }
414}
415
416impl PartialEq<&str> for Email {
417 fn eq(&self, other: &&str) -> bool {
418 self.0 == *other
419 }
420}
421
422impl PartialEq<String> for Email {
423 fn eq(&self, other: &String) -> bool {
424 &self.0 == other
425 }
426}
427
428#[derive(Debug, Clone, Default)]
430pub struct Link {
431 pub href: Url,
433 pub rel: Option<SmallString>,
436 pub link_type: Option<MimeType>,
438 pub title: Option<String>,
440 pub length: Option<String>,
442 pub hreflang: Option<SmallString>,
444 pub thr_count: Option<u32>,
446 pub thr_updated: Option<DateTime<Utc>>,
448}
449
450impl Link {
451 #[inline]
453 pub fn new(href: impl Into<Url>, rel: impl AsRef<str>) -> Self {
454 Self {
455 href: href.into(),
456 rel: Some(rel.as_ref().into()),
457 link_type: None,
458 title: None,
459 length: None,
460 hreflang: None,
461 thr_count: None,
462 thr_updated: None,
463 }
464 }
465
466 #[inline]
468 pub fn alternate(href: impl Into<Url>) -> Self {
469 Self::new(href, "alternate")
470 }
471
472 #[inline]
474 pub fn self_link(href: impl Into<Url>, mime_type: impl Into<MimeType>) -> Self {
475 Self {
476 href: href.into(),
477 rel: Some("self".into()),
478 link_type: Some(mime_type.into()),
479 title: None,
480 length: None,
481 hreflang: None,
482 thr_count: None,
483 thr_updated: None,
484 }
485 }
486
487 #[inline]
489 pub fn enclosure(href: impl Into<Url>, mime_type: Option<MimeType>) -> Self {
490 Self {
491 href: href.into(),
492 rel: Some("enclosure".into()),
493 link_type: mime_type,
494 title: None,
495 length: None,
496 hreflang: None,
497 thr_count: None,
498 thr_updated: None,
499 }
500 }
501
502 #[inline]
504 pub fn related(href: impl Into<Url>) -> Self {
505 Self::new(href, "related")
506 }
507
508 #[inline]
514 pub fn banner(href: impl Into<Url>) -> Self {
515 Self::new(href, "banner")
516 }
517
518 #[inline]
520 pub fn hub(href: impl Into<Url>) -> Self {
521 Self::new(href, "hub")
522 }
523
524 #[inline]
526 #[must_use]
527 pub fn with_type(mut self, mime_type: impl Into<MimeType>) -> Self {
528 self.link_type = Some(mime_type.into());
529 self
530 }
531}
532
533#[derive(Debug, Clone, Default)]
535pub struct Person {
536 pub name: Option<SmallString>,
538 pub email: Option<Email>,
540 pub uri: Option<String>,
542 pub avatar: Option<String>,
544}
545
546impl Person {
547 #[inline]
560 pub fn from_name(name: impl AsRef<str>) -> Self {
561 Self {
562 name: Some(name.as_ref().into()),
563 email: None,
564 uri: None,
565 avatar: None,
566 }
567 }
568
569 #[must_use]
573 pub fn flat_string(&self) -> Option<SmallString> {
574 match (&self.name, &self.email) {
575 (Some(name), Some(email)) => Some(format!("{name} ({email})").into()),
576 (Some(name), None) => Some(name.clone()),
577 (None, Some(email)) => Some(email.as_str().into()),
578 (None, None) => None,
579 }
580 }
581}
582
583#[derive(Debug, Clone)]
585pub struct Tag {
586 pub term: SmallString,
588 pub scheme: Option<SmallString>,
590 pub label: Option<SmallString>,
592}
593
594impl Tag {
595 #[inline]
597 pub fn new(term: impl AsRef<str>) -> Self {
598 Self {
599 term: term.as_ref().into(),
600 scheme: None,
601 label: None,
602 }
603 }
604}
605
606#[derive(Debug, Default, Clone, PartialEq, Eq)]
608pub struct Cloud {
609 pub domain: Option<String>,
611 pub port: Option<String>,
613 pub path: Option<String>,
615 pub register_procedure: Option<String>,
617 pub protocol: Option<String>,
619}
620
621#[derive(Debug, Default, Clone, PartialEq, Eq)]
623pub struct TextInput {
624 pub title: Option<String>,
626 pub description: Option<String>,
628 pub name: Option<String>,
630 pub link: Option<String>,
632}
633
634#[derive(Debug, Clone)]
636pub struct Image {
637 pub url: Url,
639 pub title: Option<String>,
641 pub link: Option<String>,
643 pub width: Option<u32>,
645 pub height: Option<u32>,
647 pub description: Option<String>,
649}
650
651#[derive(Debug, Clone)]
653pub struct Enclosure {
654 pub url: Url,
656 pub length: Option<String>,
658 pub enclosure_type: Option<MimeType>,
660 pub title: Option<String>,
662 pub duration: Option<String>,
664}
665
666#[derive(Debug, Clone)]
668pub struct Content {
669 pub value: String,
671 pub content_type: Option<MimeType>,
673 pub language: Option<SmallString>,
675 pub base: Option<String>,
677 pub src: Option<String>,
679}
680
681impl Content {
682 #[inline]
684 pub fn html(value: impl Into<String>) -> Self {
685 Self {
686 value: value.into(),
687 content_type: Some(MimeType::new(MimeType::TEXT_HTML)),
688 language: None,
689 base: None,
690 src: None,
691 }
692 }
693
694 #[inline]
696 pub fn plain(value: impl Into<String>) -> Self {
697 Self {
698 value: value.into(),
699 content_type: Some(MimeType::new(MimeType::TEXT_PLAIN)),
700 language: None,
701 base: None,
702 src: None,
703 }
704 }
705}
706
707#[derive(Debug, Clone, Copy, PartialEq, Eq)]
709pub enum TextType {
710 Text,
712 Html,
714 Xhtml,
716}
717
718#[derive(Debug, Clone)]
720pub struct TextConstruct {
721 pub value: String,
723 pub content_type: TextType,
725 pub language: Option<SmallString>,
727 pub base: Option<String>,
729}
730
731impl TextConstruct {
732 #[inline]
734 pub fn text(value: impl Into<String>) -> Self {
735 Self {
736 value: value.into(),
737 content_type: TextType::Text,
738 language: None,
739 base: None,
740 }
741 }
742
743 #[inline]
745 pub fn html(value: impl Into<String>) -> Self {
746 Self {
747 value: value.into(),
748 content_type: TextType::Html,
749 language: None,
750 base: None,
751 }
752 }
753
754 #[inline]
756 #[must_use]
757 pub fn with_language(mut self, language: impl AsRef<str>) -> Self {
758 self.language = Some(language.as_ref().into());
759 self
760 }
761}
762
763#[derive(Debug, Clone)]
765pub struct Generator {
766 pub name: String,
768 pub href: Option<String>,
770 pub version: Option<SmallString>,
772}
773
774#[derive(Debug, Clone)]
776pub struct Source {
777 pub title: Option<String>,
779 pub href: Option<String>,
781 pub link: Option<String>,
783 pub author: Option<String>,
785 pub id: Option<String>,
787 pub links: Vec<Link>,
789 pub updated: Option<DateTime<Utc>>,
791 pub updated_str: Option<String>,
793 pub rights: Option<String>,
795 pub guidislink: Option<bool>,
801}
802
803#[derive(Debug, Clone)]
805pub struct MediaThumbnail {
806 pub url: Url,
813 pub width: Option<String>,
815 pub height: Option<String>,
817 pub time: Option<String>,
821}
822
823#[derive(Debug, Clone)]
825pub struct MediaContent {
826 pub url: Url,
833 pub content_type: Option<MimeType>,
835 pub medium: Option<String>,
837 pub filesize: Option<String>,
839 pub width: Option<String>,
841 pub height: Option<String>,
843 pub duration: Option<String>,
845 pub bitrate: Option<String>,
847 pub lang: Option<String>,
849 pub channels: Option<String>,
851 pub codec: Option<String>,
853 pub expression: Option<String>,
855 pub isdefault: Option<String>,
857 pub samplingrate: Option<String>,
859 pub framerate: Option<String>,
861}
862
863#[derive(Debug, Clone, Default, PartialEq, Eq)]
868pub struct MediaRating {
869 pub scheme: Option<String>,
871 pub content: String,
873}
874
875impl FromAttributes for Link {
876 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
877 where
878 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
879 {
880 let mut href = None;
881 let mut rel = None;
882 let mut link_type = None;
883 let mut title = None;
884 let mut hreflang = None;
885 let mut length = None;
886 let mut thr_count = None;
887 let mut thr_updated = None;
888
889 for attr in attrs {
890 if attr.value.len() > max_attr_length {
891 continue;
892 }
893 match attr.key.as_ref() {
894 b"href" => href = Some(bytes_to_string(&attr.value)),
895 b"rel" => rel = Some(bytes_to_string(&attr.value)),
896 b"type" => link_type = Some(bytes_to_string(&attr.value)),
897 b"title" => title = Some(bytes_to_string(&attr.value)),
898 b"hreflang" => hreflang = Some(bytes_to_string(&attr.value)),
899 b"length" => length = Some(bytes_to_string(&attr.value)),
900 b"thr:count" => {
901 thr_count = bytes_to_string(&attr.value).trim().parse::<u32>().ok();
902 }
903 b"thr:updated" => {
904 thr_updated = parse_date(bytes_to_string(&attr.value).trim());
905 }
906 _ => {}
907 }
908 }
909
910 let rel_str: Option<SmallString> = rel
911 .map(std::convert::Into::into)
912 .or_else(|| Some("alternate".into()));
913 let resolved_type = link_type
914 .map(MimeType::new)
915 .or_else(|| match rel_str.as_deref() {
916 Some("self") => Some(MimeType::new("application/atom+xml")),
917 _ => Some(MimeType::new("text/html")),
918 });
919
920 href.map(|href| Self {
921 href: Url::new(href),
922 rel: rel_str,
923 link_type: resolved_type,
924 title,
925 length,
926 hreflang: hreflang.map(std::convert::Into::into),
927 thr_count,
928 thr_updated,
929 })
930 }
931}
932
933impl FromAttributes for Tag {
934 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
935 where
936 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
937 {
938 let mut term = None;
939 let mut scheme = None;
940 let mut label = None;
941
942 for attr in attrs {
943 if attr.value.len() > max_attr_length {
944 continue;
945 }
946
947 match attr.key.as_ref() {
948 b"term" => term = Some(bytes_to_string(&attr.value)),
949 b"scheme" | b"domain" => scheme = Some(bytes_to_string(&attr.value)),
950 b"label" => label = Some(bytes_to_string(&attr.value)),
951 _ => {}
952 }
953 }
954
955 term.map(|term| Self {
956 term: term.into(),
957 scheme: scheme.map(std::convert::Into::into),
958 label: label.map(std::convert::Into::into),
959 })
960 }
961}
962
963impl FromAttributes for Enclosure {
964 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
965 where
966 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
967 {
968 let mut url = None;
969 let mut length = None;
970 let mut enclosure_type = None;
971
972 for attr in attrs {
973 if attr.value.len() > max_attr_length {
974 continue;
975 }
976
977 match attr.key.as_ref() {
978 b"url" => url = Some(bytes_to_string(&attr.value)),
979 b"length" => length = Some(bytes_to_string(&attr.value)),
980 b"type" => enclosure_type = Some(bytes_to_string(&attr.value)),
981 _ => {}
982 }
983 }
984
985 url.map(|url| Self {
986 url: Url::new(url),
987 length,
988 enclosure_type: enclosure_type.map(MimeType::new),
989 title: None,
990 duration: None,
991 })
992 }
993}
994
995impl FromAttributes for MediaThumbnail {
996 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
997 where
998 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
999 {
1000 let mut url = None;
1001 let mut width = None;
1002 let mut height = None;
1003
1004 let mut time = None;
1005
1006 for attr in attrs {
1007 if attr.value.len() > max_attr_length {
1008 continue;
1009 }
1010
1011 match attr.key.as_ref() {
1012 b"url" => url = Some(bytes_to_string(&attr.value)),
1013 b"width" => width = Some(bytes_to_string(&attr.value)),
1014 b"height" => height = Some(bytes_to_string(&attr.value)),
1015 b"time" => time = Some(bytes_to_string(&attr.value)),
1016 _ => {}
1017 }
1018 }
1019
1020 url.map(|url| Self {
1021 url: Url::new(url),
1022 width,
1023 height,
1024 time,
1025 })
1026 }
1027}
1028
1029impl FromAttributes for MediaContent {
1030 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
1031 where
1032 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
1033 {
1034 let mut url = None;
1035 let mut content_type = None;
1036 let mut medium = None;
1037 let mut filesize = None;
1038 let mut width = None;
1039 let mut height = None;
1040 let mut duration = None;
1041 let mut bitrate = None;
1042 let mut lang = None;
1043 let mut channels = None;
1044 let mut codec = None;
1045 let mut expression = None;
1046 let mut isdefault = None;
1047 let mut samplingrate = None;
1048 let mut framerate = None;
1049
1050 for attr in attrs {
1051 if attr.value.len() > max_attr_length {
1052 continue;
1053 }
1054
1055 match attr.key.as_ref() {
1056 b"url" => url = Some(bytes_to_string(&attr.value)),
1057 b"type" => content_type = Some(bytes_to_string(&attr.value)),
1058 b"medium" => medium = Some(bytes_to_string(&attr.value)),
1059 b"fileSize" => filesize = Some(bytes_to_string(&attr.value)),
1060 b"width" => width = Some(bytes_to_string(&attr.value)),
1061 b"height" => height = Some(bytes_to_string(&attr.value)),
1062 b"duration" => duration = Some(bytes_to_string(&attr.value)),
1063 b"bitrate" => bitrate = Some(bytes_to_string(&attr.value)),
1064 b"lang" => lang = Some(bytes_to_string(&attr.value)),
1065 b"channels" => channels = Some(bytes_to_string(&attr.value)),
1066 b"codec" => codec = Some(bytes_to_string(&attr.value)),
1067 b"expression" => expression = Some(bytes_to_string(&attr.value)),
1068 b"isDefault" => isdefault = Some(bytes_to_string(&attr.value)),
1069 b"samplingrate" => samplingrate = Some(bytes_to_string(&attr.value)),
1070 b"framerate" => framerate = Some(bytes_to_string(&attr.value)),
1071 _ => {}
1072 }
1073 }
1074
1075 url.map(|url| Self {
1076 url: Url::new(url),
1077 content_type: content_type.map(MimeType::new),
1078 medium,
1079 filesize,
1080 width,
1081 height,
1082 duration,
1083 bitrate,
1084 lang,
1085 channels,
1086 codec,
1087 expression,
1088 isdefault,
1089 samplingrate,
1090 framerate,
1091 })
1092 }
1093}
1094
1095impl ParseFrom<&Value> for Person {
1098 fn parse_from(json: &Value) -> Option<Self> {
1102 json.as_object().map(|obj| Self {
1103 name: obj
1104 .get("name")
1105 .and_then(Value::as_str)
1106 .map(std::convert::Into::into),
1107 email: None, uri: obj.get("url").and_then(Value::as_str).map(String::from),
1109 avatar: obj.get("avatar").and_then(Value::as_str).map(String::from),
1110 })
1111 }
1112}
1113
1114impl ParseFrom<&Value> for Enclosure {
1115 fn parse_from(json: &Value) -> Option<Self> {
1119 let obj = json.as_object()?;
1120 let url = obj.get("url").and_then(Value::as_str)?;
1121 Some(Self {
1122 url: Url::new(url),
1123 length: obj
1124 .get("size_in_bytes")
1125 .and_then(Value::as_u64)
1126 .map(|v| v.to_string()),
1127 enclosure_type: obj
1128 .get("mime_type")
1129 .and_then(Value::as_str)
1130 .map(MimeType::new),
1131 title: obj.get("title").and_then(Value::as_str).map(String::from),
1132 duration: obj
1133 .get("duration_in_seconds")
1134 .and_then(Value::as_u64)
1135 .map(|v| v.to_string()),
1136 })
1137 }
1138}
1139
1140#[derive(Debug, Clone, Default)]
1142pub struct MediaCredit {
1143 pub role: Option<String>,
1145 pub scheme: Option<String>,
1147 pub content: String,
1149}
1150
1151#[derive(Debug, Clone, Default)]
1153pub struct MediaCopyright {
1154 pub url: Option<String>,
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160 use super::*;
1161 use serde_json::json;
1162
1163 #[test]
1164 fn test_link_default() {
1165 let link = Link::default();
1166 assert!(link.href.is_empty());
1167 assert!(link.rel.is_none());
1168 }
1169
1170 #[test]
1171 fn test_link_builders() {
1172 let link = Link::alternate("https://example.com");
1173 assert_eq!(link.href, "https://example.com");
1174 assert_eq!(link.rel.as_deref(), Some("alternate"));
1175
1176 let link = Link::self_link("https://example.com/feed", "application/feed+json");
1177 assert_eq!(link.rel.as_deref(), Some("self"));
1178 assert_eq!(link.link_type.as_deref(), Some("application/feed+json"));
1179
1180 let link = Link::enclosure("https://example.com/audio.mp3", Some("audio/mpeg".into()));
1181 assert_eq!(link.rel.as_deref(), Some("enclosure"));
1182 assert_eq!(link.link_type.as_deref(), Some("audio/mpeg"));
1183
1184 let link = Link::related("https://other.com");
1185 assert_eq!(link.rel.as_deref(), Some("related"));
1186 }
1187
1188 #[test]
1189 fn test_tag_builder() {
1190 let tag = Tag::new("rust");
1191 assert_eq!(tag.term, "rust");
1192 assert!(tag.scheme.is_none());
1193 }
1194
1195 #[test]
1196 fn test_text_construct_builders() {
1197 let text = TextConstruct::text("Hello");
1198 assert_eq!(text.value, "Hello");
1199 assert_eq!(text.content_type, TextType::Text);
1200
1201 let html = TextConstruct::html("<p>Hello</p>");
1202 assert_eq!(html.content_type, TextType::Html);
1203
1204 let with_lang = TextConstruct::text("Hello").with_language("en");
1205 assert_eq!(with_lang.language.as_deref(), Some("en"));
1206 }
1207
1208 #[test]
1209 fn test_content_builders() {
1210 let html = Content::html("<p>Content</p>");
1211 assert_eq!(html.content_type.as_deref(), Some("text/html"));
1212
1213 let plain = Content::plain("Content");
1214 assert_eq!(plain.content_type.as_deref(), Some("text/plain"));
1215 }
1216
1217 #[test]
1218 fn test_person_default() {
1219 let person = Person::default();
1220 assert!(person.name.is_none());
1221 assert!(person.email.is_none());
1222 assert!(person.uri.is_none());
1223 }
1224
1225 #[test]
1226 fn test_person_parse_from_json() {
1227 let json = json!({"name": "John Doe", "url": "https://example.com"});
1228 let person = Person::parse_from(&json).unwrap();
1229 assert_eq!(person.name.as_deref(), Some("John Doe"));
1230 assert_eq!(person.uri.as_deref(), Some("https://example.com"));
1231 assert!(person.email.is_none());
1232 }
1233
1234 #[test]
1235 fn test_person_parse_from_empty_json() {
1236 let json = json!({});
1237 let person = Person::parse_from(&json).unwrap();
1238 assert!(person.name.is_none());
1239 }
1240
1241 #[test]
1242 fn test_enclosure_parse_from_json() {
1243 let json = json!({
1244 "url": "https://example.com/file.mp3",
1245 "mime_type": "audio/mpeg",
1246 "size_in_bytes": 12345
1247 });
1248 let enclosure = Enclosure::parse_from(&json).unwrap();
1249 assert_eq!(enclosure.url, "https://example.com/file.mp3");
1250 assert_eq!(enclosure.enclosure_type.as_deref(), Some("audio/mpeg"));
1251 assert_eq!(enclosure.length.as_deref(), Some("12345"));
1252 }
1253
1254 #[test]
1255 fn test_enclosure_parse_from_json_missing_url() {
1256 let json = json!({"mime_type": "audio/mpeg"});
1257 assert!(Enclosure::parse_from(&json).is_none());
1258 }
1259
1260 #[test]
1261 fn test_text_type_equality() {
1262 assert_eq!(TextType::Text, TextType::Text);
1263 assert_ne!(TextType::Text, TextType::Html);
1264 }
1265
1266 #[test]
1269 fn test_url_new() {
1270 let url = Url::new("https://example.com");
1271 assert_eq!(url.as_str(), "https://example.com");
1272 }
1273
1274 #[test]
1275 fn test_url_from_string() {
1276 let url: Url = String::from("https://example.com").into();
1277 assert_eq!(url.as_str(), "https://example.com");
1278 }
1279
1280 #[test]
1281 fn test_url_from_str() {
1282 let url: Url = "https://example.com".into();
1283 assert_eq!(url.as_str(), "https://example.com");
1284 }
1285
1286 #[test]
1287 fn test_url_deref() {
1288 let url = Url::new("https://example.com");
1289 assert_eq!(url.len(), 19);
1291 assert!(url.starts_with("https://"));
1292 }
1293
1294 #[test]
1295 fn test_url_into_inner() {
1296 let url = Url::new("https://example.com");
1297 let inner = url.into_inner();
1298 assert_eq!(inner, "https://example.com");
1299 }
1300
1301 #[test]
1302 fn test_url_default() {
1303 let url = Url::default();
1304 assert_eq!(url.as_str(), "");
1305 }
1306
1307 #[test]
1308 fn test_url_clone() {
1309 let url1 = Url::new("https://example.com");
1310 let url2 = url1.clone();
1311 assert_eq!(url1, url2);
1312 }
1313
1314 #[test]
1315 fn test_mime_type_new() {
1316 let mime = MimeType::new("text/html");
1317 assert_eq!(mime.as_str(), "text/html");
1318 }
1319
1320 #[test]
1321 fn test_mime_type_from_string() {
1322 let mime: MimeType = String::from("application/json").into();
1323 assert_eq!(mime.as_str(), "application/json");
1324 }
1325
1326 #[test]
1327 fn test_mime_type_from_str() {
1328 let mime: MimeType = "text/plain".into();
1329 assert_eq!(mime.as_str(), "text/plain");
1330 }
1331
1332 #[test]
1333 fn test_mime_type_deref() {
1334 let mime = MimeType::new("text/html");
1335 assert_eq!(mime.len(), 9);
1336 assert!(mime.starts_with("text/"));
1337 }
1338
1339 #[test]
1340 fn test_mime_type_default() {
1341 let mime = MimeType::default();
1342 assert_eq!(mime.as_str(), "");
1343 }
1344
1345 #[test]
1346 fn test_mime_type_clone() {
1347 let mime1 = MimeType::new("application/xml");
1348 let mime2 = mime1.clone();
1349 assert_eq!(mime1, mime2);
1350 }
1352
1353 #[test]
1354 fn test_mime_type_constants() {
1355 assert_eq!(MimeType::TEXT_HTML, "text/html");
1356 assert_eq!(MimeType::TEXT_PLAIN, "text/plain");
1357 assert_eq!(MimeType::APPLICATION_XML, "application/xml");
1358 assert_eq!(MimeType::APPLICATION_JSON, "application/json");
1359 }
1360
1361 #[test]
1362 fn test_email_new() {
1363 let email = Email::new("user@example.com");
1364 assert_eq!(email.as_str(), "user@example.com");
1365 }
1366
1367 #[test]
1368 fn test_email_from_string() {
1369 let email: Email = String::from("user@example.com").into();
1370 assert_eq!(email.as_str(), "user@example.com");
1371 }
1372
1373 #[test]
1374 fn test_email_from_str() {
1375 let email: Email = "user@example.com".into();
1376 assert_eq!(email.as_str(), "user@example.com");
1377 }
1378
1379 #[test]
1380 fn test_email_deref() {
1381 let email = Email::new("user@example.com");
1382 assert_eq!(email.len(), 16);
1383 assert!(email.contains('@'));
1384 }
1385
1386 #[test]
1387 fn test_email_into_inner() {
1388 let email = Email::new("user@example.com");
1389 let inner = email.into_inner();
1390 assert_eq!(inner, "user@example.com");
1391 }
1392
1393 #[test]
1394 fn test_email_default() {
1395 let email = Email::default();
1396 assert_eq!(email.as_str(), "");
1397 }
1398
1399 #[test]
1400 fn test_email_clone() {
1401 let email1 = Email::new("user@example.com");
1402 let email2 = email1.clone();
1403 assert_eq!(email1, email2);
1404 }
1405}