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 #[must_use]
521 pub fn with_type(mut self, mime_type: impl Into<MimeType>) -> Self {
522 self.link_type = Some(mime_type.into());
523 self
524 }
525}
526
527#[derive(Debug, Clone, Default)]
529pub struct Person {
530 pub name: Option<SmallString>,
532 pub email: Option<Email>,
534 pub uri: Option<String>,
536 pub avatar: Option<String>,
538}
539
540impl Person {
541 #[inline]
554 pub fn from_name(name: impl AsRef<str>) -> Self {
555 Self {
556 name: Some(name.as_ref().into()),
557 email: None,
558 uri: None,
559 avatar: None,
560 }
561 }
562
563 #[must_use]
567 pub fn flat_string(&self) -> Option<SmallString> {
568 match (&self.name, &self.email) {
569 (Some(name), Some(email)) => Some(format!("{name} ({email})").into()),
570 (Some(name), None) => Some(name.clone()),
571 (None, Some(email)) => Some(email.as_str().into()),
572 (None, None) => None,
573 }
574 }
575}
576
577#[derive(Debug, Clone)]
579pub struct Tag {
580 pub term: SmallString,
582 pub scheme: Option<SmallString>,
584 pub label: Option<SmallString>,
586}
587
588impl Tag {
589 #[inline]
591 pub fn new(term: impl AsRef<str>) -> Self {
592 Self {
593 term: term.as_ref().into(),
594 scheme: None,
595 label: None,
596 }
597 }
598}
599
600#[derive(Debug, Default, Clone, PartialEq, Eq)]
602pub struct Cloud {
603 pub domain: Option<String>,
605 pub port: Option<String>,
607 pub path: Option<String>,
609 pub register_procedure: Option<String>,
611 pub protocol: Option<String>,
613}
614
615#[derive(Debug, Default, Clone, PartialEq, Eq)]
617pub struct TextInput {
618 pub title: Option<String>,
620 pub description: Option<String>,
622 pub name: Option<String>,
624 pub link: Option<String>,
626}
627
628#[derive(Debug, Clone)]
630pub struct Image {
631 pub url: Url,
633 pub title: Option<String>,
635 pub link: Option<String>,
637 pub width: Option<u32>,
639 pub height: Option<u32>,
641 pub description: Option<String>,
643}
644
645#[derive(Debug, Clone)]
647pub struct Enclosure {
648 pub url: Url,
650 pub length: Option<String>,
652 pub enclosure_type: Option<MimeType>,
654 pub title: Option<String>,
656 pub duration: Option<String>,
658}
659
660#[derive(Debug, Clone)]
662pub struct Content {
663 pub value: String,
665 pub content_type: Option<MimeType>,
667 pub language: Option<SmallString>,
669 pub base: Option<String>,
671 pub src: Option<String>,
673}
674
675impl Content {
676 #[inline]
678 pub fn html(value: impl Into<String>) -> Self {
679 Self {
680 value: value.into(),
681 content_type: Some(MimeType::new(MimeType::TEXT_HTML)),
682 language: None,
683 base: None,
684 src: None,
685 }
686 }
687
688 #[inline]
690 pub fn plain(value: impl Into<String>) -> Self {
691 Self {
692 value: value.into(),
693 content_type: Some(MimeType::new(MimeType::TEXT_PLAIN)),
694 language: None,
695 base: None,
696 src: None,
697 }
698 }
699}
700
701#[derive(Debug, Clone, Copy, PartialEq, Eq)]
703pub enum TextType {
704 Text,
706 Html,
708 Xhtml,
710}
711
712#[derive(Debug, Clone)]
714pub struct TextConstruct {
715 pub value: String,
717 pub content_type: TextType,
719 pub language: Option<SmallString>,
721 pub base: Option<String>,
723}
724
725impl TextConstruct {
726 #[inline]
728 pub fn text(value: impl Into<String>) -> Self {
729 Self {
730 value: value.into(),
731 content_type: TextType::Text,
732 language: None,
733 base: None,
734 }
735 }
736
737 #[inline]
739 pub fn html(value: impl Into<String>) -> Self {
740 Self {
741 value: value.into(),
742 content_type: TextType::Html,
743 language: None,
744 base: None,
745 }
746 }
747
748 #[inline]
750 #[must_use]
751 pub fn with_language(mut self, language: impl AsRef<str>) -> Self {
752 self.language = Some(language.as_ref().into());
753 self
754 }
755}
756
757#[derive(Debug, Clone)]
759pub struct Generator {
760 pub name: String,
762 pub href: Option<String>,
764 pub version: Option<SmallString>,
766}
767
768#[derive(Debug, Clone)]
770pub struct Source {
771 pub title: Option<String>,
773 pub href: Option<String>,
775 pub link: Option<String>,
777 pub author: Option<String>,
779 pub id: Option<String>,
781 pub links: Vec<Link>,
783 pub updated: Option<DateTime<Utc>>,
785 pub updated_str: Option<String>,
787 pub rights: Option<String>,
789 pub guidislink: Option<bool>,
795}
796
797#[derive(Debug, Clone)]
799pub struct MediaThumbnail {
800 pub url: Url,
807 pub width: Option<String>,
809 pub height: Option<String>,
811 pub time: Option<String>,
815}
816
817#[derive(Debug, Clone)]
819pub struct MediaContent {
820 pub url: Url,
827 pub content_type: Option<MimeType>,
829 pub medium: Option<String>,
831 pub filesize: Option<String>,
833 pub width: Option<String>,
835 pub height: Option<String>,
837 pub duration: Option<String>,
839 pub bitrate: Option<String>,
841 pub lang: Option<String>,
843 pub channels: Option<String>,
845 pub codec: Option<String>,
847 pub expression: Option<String>,
849 pub isdefault: Option<String>,
851 pub samplingrate: Option<String>,
853 pub framerate: Option<String>,
855}
856
857#[derive(Debug, Clone, Default, PartialEq, Eq)]
862pub struct MediaRating {
863 pub scheme: Option<String>,
865 pub content: String,
867}
868
869impl FromAttributes for Link {
870 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
871 where
872 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
873 {
874 let mut href = None;
875 let mut rel = None;
876 let mut link_type = None;
877 let mut title = None;
878 let mut hreflang = None;
879 let mut length = None;
880 let mut thr_count = None;
881 let mut thr_updated = None;
882
883 for attr in attrs {
884 if attr.value.len() > max_attr_length {
885 continue;
886 }
887 match attr.key.as_ref() {
888 b"href" => href = Some(bytes_to_string(&attr.value)),
889 b"rel" => rel = Some(bytes_to_string(&attr.value)),
890 b"type" => link_type = Some(bytes_to_string(&attr.value)),
891 b"title" => title = Some(bytes_to_string(&attr.value)),
892 b"hreflang" => hreflang = Some(bytes_to_string(&attr.value)),
893 b"length" => length = Some(bytes_to_string(&attr.value)),
894 b"thr:count" => {
895 thr_count = bytes_to_string(&attr.value).trim().parse::<u32>().ok();
896 }
897 b"thr:updated" => {
898 thr_updated = parse_date(bytes_to_string(&attr.value).trim());
899 }
900 _ => {}
901 }
902 }
903
904 let rel_str: Option<SmallString> = rel
905 .map(std::convert::Into::into)
906 .or_else(|| Some("alternate".into()));
907 let resolved_type = link_type
908 .map(MimeType::new)
909 .or_else(|| match rel_str.as_deref() {
910 Some("self") => Some(MimeType::new("application/atom+xml")),
911 _ => Some(MimeType::new("text/html")),
912 });
913
914 href.map(|href| Self {
915 href: Url::new(href),
916 rel: rel_str,
917 link_type: resolved_type,
918 title,
919 length,
920 hreflang: hreflang.map(std::convert::Into::into),
921 thr_count,
922 thr_updated,
923 })
924 }
925}
926
927impl FromAttributes for Tag {
928 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
929 where
930 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
931 {
932 let mut term = None;
933 let mut scheme = None;
934 let mut label = None;
935
936 for attr in attrs {
937 if attr.value.len() > max_attr_length {
938 continue;
939 }
940
941 match attr.key.as_ref() {
942 b"term" => term = Some(bytes_to_string(&attr.value)),
943 b"scheme" | b"domain" => scheme = Some(bytes_to_string(&attr.value)),
944 b"label" => label = Some(bytes_to_string(&attr.value)),
945 _ => {}
946 }
947 }
948
949 term.map(|term| Self {
950 term: term.into(),
951 scheme: scheme.map(std::convert::Into::into),
952 label: label.map(std::convert::Into::into),
953 })
954 }
955}
956
957impl FromAttributes for Enclosure {
958 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
959 where
960 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
961 {
962 let mut url = None;
963 let mut length = None;
964 let mut enclosure_type = None;
965
966 for attr in attrs {
967 if attr.value.len() > max_attr_length {
968 continue;
969 }
970
971 match attr.key.as_ref() {
972 b"url" => url = Some(bytes_to_string(&attr.value)),
973 b"length" => length = Some(bytes_to_string(&attr.value)),
974 b"type" => enclosure_type = Some(bytes_to_string(&attr.value)),
975 _ => {}
976 }
977 }
978
979 url.map(|url| Self {
980 url: Url::new(url),
981 length,
982 enclosure_type: enclosure_type.map(MimeType::new),
983 title: None,
984 duration: None,
985 })
986 }
987}
988
989impl FromAttributes for MediaThumbnail {
990 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
991 where
992 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
993 {
994 let mut url = None;
995 let mut width = None;
996 let mut height = None;
997
998 let mut time = None;
999
1000 for attr in attrs {
1001 if attr.value.len() > max_attr_length {
1002 continue;
1003 }
1004
1005 match attr.key.as_ref() {
1006 b"url" => url = Some(bytes_to_string(&attr.value)),
1007 b"width" => width = Some(bytes_to_string(&attr.value)),
1008 b"height" => height = Some(bytes_to_string(&attr.value)),
1009 b"time" => time = Some(bytes_to_string(&attr.value)),
1010 _ => {}
1011 }
1012 }
1013
1014 url.map(|url| Self {
1015 url: Url::new(url),
1016 width,
1017 height,
1018 time,
1019 })
1020 }
1021}
1022
1023impl FromAttributes for MediaContent {
1024 fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
1025 where
1026 I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
1027 {
1028 let mut url = None;
1029 let mut content_type = None;
1030 let mut medium = None;
1031 let mut filesize = None;
1032 let mut width = None;
1033 let mut height = None;
1034 let mut duration = None;
1035 let mut bitrate = None;
1036 let mut lang = None;
1037 let mut channels = None;
1038 let mut codec = None;
1039 let mut expression = None;
1040 let mut isdefault = None;
1041 let mut samplingrate = None;
1042 let mut framerate = None;
1043
1044 for attr in attrs {
1045 if attr.value.len() > max_attr_length {
1046 continue;
1047 }
1048
1049 match attr.key.as_ref() {
1050 b"url" => url = Some(bytes_to_string(&attr.value)),
1051 b"type" => content_type = Some(bytes_to_string(&attr.value)),
1052 b"medium" => medium = Some(bytes_to_string(&attr.value)),
1053 b"fileSize" => filesize = Some(bytes_to_string(&attr.value)),
1054 b"width" => width = Some(bytes_to_string(&attr.value)),
1055 b"height" => height = Some(bytes_to_string(&attr.value)),
1056 b"duration" => duration = Some(bytes_to_string(&attr.value)),
1057 b"bitrate" => bitrate = Some(bytes_to_string(&attr.value)),
1058 b"lang" => lang = Some(bytes_to_string(&attr.value)),
1059 b"channels" => channels = Some(bytes_to_string(&attr.value)),
1060 b"codec" => codec = Some(bytes_to_string(&attr.value)),
1061 b"expression" => expression = Some(bytes_to_string(&attr.value)),
1062 b"isDefault" => isdefault = Some(bytes_to_string(&attr.value)),
1063 b"samplingrate" => samplingrate = Some(bytes_to_string(&attr.value)),
1064 b"framerate" => framerate = Some(bytes_to_string(&attr.value)),
1065 _ => {}
1066 }
1067 }
1068
1069 url.map(|url| Self {
1070 url: Url::new(url),
1071 content_type: content_type.map(MimeType::new),
1072 medium,
1073 filesize,
1074 width,
1075 height,
1076 duration,
1077 bitrate,
1078 lang,
1079 channels,
1080 codec,
1081 expression,
1082 isdefault,
1083 samplingrate,
1084 framerate,
1085 })
1086 }
1087}
1088
1089impl ParseFrom<&Value> for Person {
1092 fn parse_from(json: &Value) -> Option<Self> {
1096 json.as_object().map(|obj| Self {
1097 name: obj
1098 .get("name")
1099 .and_then(Value::as_str)
1100 .map(std::convert::Into::into),
1101 email: None, uri: obj.get("url").and_then(Value::as_str).map(String::from),
1103 avatar: obj.get("avatar").and_then(Value::as_str).map(String::from),
1104 })
1105 }
1106}
1107
1108impl ParseFrom<&Value> for Enclosure {
1109 fn parse_from(json: &Value) -> Option<Self> {
1113 let obj = json.as_object()?;
1114 let url = obj.get("url").and_then(Value::as_str)?;
1115 Some(Self {
1116 url: Url::new(url),
1117 length: obj
1118 .get("size_in_bytes")
1119 .and_then(Value::as_u64)
1120 .map(|v| v.to_string()),
1121 enclosure_type: obj
1122 .get("mime_type")
1123 .and_then(Value::as_str)
1124 .map(MimeType::new),
1125 title: obj.get("title").and_then(Value::as_str).map(String::from),
1126 duration: obj
1127 .get("duration_in_seconds")
1128 .and_then(Value::as_u64)
1129 .map(|v| v.to_string()),
1130 })
1131 }
1132}
1133
1134#[derive(Debug, Clone, Default)]
1136pub struct MediaCredit {
1137 pub role: Option<String>,
1139 pub scheme: Option<String>,
1141 pub content: String,
1143}
1144
1145#[derive(Debug, Clone, Default)]
1147pub struct MediaCopyright {
1148 pub url: Option<String>,
1150}
1151
1152#[cfg(test)]
1153mod tests {
1154 use super::*;
1155 use serde_json::json;
1156
1157 #[test]
1158 fn test_link_default() {
1159 let link = Link::default();
1160 assert!(link.href.is_empty());
1161 assert!(link.rel.is_none());
1162 }
1163
1164 #[test]
1165 fn test_link_builders() {
1166 let link = Link::alternate("https://example.com");
1167 assert_eq!(link.href, "https://example.com");
1168 assert_eq!(link.rel.as_deref(), Some("alternate"));
1169
1170 let link = Link::self_link("https://example.com/feed", "application/feed+json");
1171 assert_eq!(link.rel.as_deref(), Some("self"));
1172 assert_eq!(link.link_type.as_deref(), Some("application/feed+json"));
1173
1174 let link = Link::enclosure("https://example.com/audio.mp3", Some("audio/mpeg".into()));
1175 assert_eq!(link.rel.as_deref(), Some("enclosure"));
1176 assert_eq!(link.link_type.as_deref(), Some("audio/mpeg"));
1177
1178 let link = Link::related("https://other.com");
1179 assert_eq!(link.rel.as_deref(), Some("related"));
1180 }
1181
1182 #[test]
1183 fn test_tag_builder() {
1184 let tag = Tag::new("rust");
1185 assert_eq!(tag.term, "rust");
1186 assert!(tag.scheme.is_none());
1187 }
1188
1189 #[test]
1190 fn test_text_construct_builders() {
1191 let text = TextConstruct::text("Hello");
1192 assert_eq!(text.value, "Hello");
1193 assert_eq!(text.content_type, TextType::Text);
1194
1195 let html = TextConstruct::html("<p>Hello</p>");
1196 assert_eq!(html.content_type, TextType::Html);
1197
1198 let with_lang = TextConstruct::text("Hello").with_language("en");
1199 assert_eq!(with_lang.language.as_deref(), Some("en"));
1200 }
1201
1202 #[test]
1203 fn test_content_builders() {
1204 let html = Content::html("<p>Content</p>");
1205 assert_eq!(html.content_type.as_deref(), Some("text/html"));
1206
1207 let plain = Content::plain("Content");
1208 assert_eq!(plain.content_type.as_deref(), Some("text/plain"));
1209 }
1210
1211 #[test]
1212 fn test_person_default() {
1213 let person = Person::default();
1214 assert!(person.name.is_none());
1215 assert!(person.email.is_none());
1216 assert!(person.uri.is_none());
1217 }
1218
1219 #[test]
1220 fn test_person_parse_from_json() {
1221 let json = json!({"name": "John Doe", "url": "https://example.com"});
1222 let person = Person::parse_from(&json).unwrap();
1223 assert_eq!(person.name.as_deref(), Some("John Doe"));
1224 assert_eq!(person.uri.as_deref(), Some("https://example.com"));
1225 assert!(person.email.is_none());
1226 }
1227
1228 #[test]
1229 fn test_person_parse_from_empty_json() {
1230 let json = json!({});
1231 let person = Person::parse_from(&json).unwrap();
1232 assert!(person.name.is_none());
1233 }
1234
1235 #[test]
1236 fn test_enclosure_parse_from_json() {
1237 let json = json!({
1238 "url": "https://example.com/file.mp3",
1239 "mime_type": "audio/mpeg",
1240 "size_in_bytes": 12345
1241 });
1242 let enclosure = Enclosure::parse_from(&json).unwrap();
1243 assert_eq!(enclosure.url, "https://example.com/file.mp3");
1244 assert_eq!(enclosure.enclosure_type.as_deref(), Some("audio/mpeg"));
1245 assert_eq!(enclosure.length.as_deref(), Some("12345"));
1246 }
1247
1248 #[test]
1249 fn test_enclosure_parse_from_json_missing_url() {
1250 let json = json!({"mime_type": "audio/mpeg"});
1251 assert!(Enclosure::parse_from(&json).is_none());
1252 }
1253
1254 #[test]
1255 fn test_text_type_equality() {
1256 assert_eq!(TextType::Text, TextType::Text);
1257 assert_ne!(TextType::Text, TextType::Html);
1258 }
1259
1260 #[test]
1263 fn test_url_new() {
1264 let url = Url::new("https://example.com");
1265 assert_eq!(url.as_str(), "https://example.com");
1266 }
1267
1268 #[test]
1269 fn test_url_from_string() {
1270 let url: Url = String::from("https://example.com").into();
1271 assert_eq!(url.as_str(), "https://example.com");
1272 }
1273
1274 #[test]
1275 fn test_url_from_str() {
1276 let url: Url = "https://example.com".into();
1277 assert_eq!(url.as_str(), "https://example.com");
1278 }
1279
1280 #[test]
1281 fn test_url_deref() {
1282 let url = Url::new("https://example.com");
1283 assert_eq!(url.len(), 19);
1285 assert!(url.starts_with("https://"));
1286 }
1287
1288 #[test]
1289 fn test_url_into_inner() {
1290 let url = Url::new("https://example.com");
1291 let inner = url.into_inner();
1292 assert_eq!(inner, "https://example.com");
1293 }
1294
1295 #[test]
1296 fn test_url_default() {
1297 let url = Url::default();
1298 assert_eq!(url.as_str(), "");
1299 }
1300
1301 #[test]
1302 fn test_url_clone() {
1303 let url1 = Url::new("https://example.com");
1304 let url2 = url1.clone();
1305 assert_eq!(url1, url2);
1306 }
1307
1308 #[test]
1309 fn test_mime_type_new() {
1310 let mime = MimeType::new("text/html");
1311 assert_eq!(mime.as_str(), "text/html");
1312 }
1313
1314 #[test]
1315 fn test_mime_type_from_string() {
1316 let mime: MimeType = String::from("application/json").into();
1317 assert_eq!(mime.as_str(), "application/json");
1318 }
1319
1320 #[test]
1321 fn test_mime_type_from_str() {
1322 let mime: MimeType = "text/plain".into();
1323 assert_eq!(mime.as_str(), "text/plain");
1324 }
1325
1326 #[test]
1327 fn test_mime_type_deref() {
1328 let mime = MimeType::new("text/html");
1329 assert_eq!(mime.len(), 9);
1330 assert!(mime.starts_with("text/"));
1331 }
1332
1333 #[test]
1334 fn test_mime_type_default() {
1335 let mime = MimeType::default();
1336 assert_eq!(mime.as_str(), "");
1337 }
1338
1339 #[test]
1340 fn test_mime_type_clone() {
1341 let mime1 = MimeType::new("application/xml");
1342 let mime2 = mime1.clone();
1343 assert_eq!(mime1, mime2);
1344 }
1346
1347 #[test]
1348 fn test_mime_type_constants() {
1349 assert_eq!(MimeType::TEXT_HTML, "text/html");
1350 assert_eq!(MimeType::TEXT_PLAIN, "text/plain");
1351 assert_eq!(MimeType::APPLICATION_XML, "application/xml");
1352 assert_eq!(MimeType::APPLICATION_JSON, "application/json");
1353 }
1354
1355 #[test]
1356 fn test_email_new() {
1357 let email = Email::new("user@example.com");
1358 assert_eq!(email.as_str(), "user@example.com");
1359 }
1360
1361 #[test]
1362 fn test_email_from_string() {
1363 let email: Email = String::from("user@example.com").into();
1364 assert_eq!(email.as_str(), "user@example.com");
1365 }
1366
1367 #[test]
1368 fn test_email_from_str() {
1369 let email: Email = "user@example.com".into();
1370 assert_eq!(email.as_str(), "user@example.com");
1371 }
1372
1373 #[test]
1374 fn test_email_deref() {
1375 let email = Email::new("user@example.com");
1376 assert_eq!(email.len(), 16);
1377 assert!(email.contains('@'));
1378 }
1379
1380 #[test]
1381 fn test_email_into_inner() {
1382 let email = Email::new("user@example.com");
1383 let inner = email.into_inner();
1384 assert_eq!(inner, "user@example.com");
1385 }
1386
1387 #[test]
1388 fn test_email_default() {
1389 let email = Email::default();
1390 assert_eq!(email.as_str(), "");
1391 }
1392
1393 #[test]
1394 fn test_email_clone() {
1395 let email1 = Email::new("user@example.com");
1396 let email2 = email1.clone();
1397 assert_eq!(email1, email2);
1398 }
1399}