feedparser_rs/types/
common.rs

1use super::generics::{FromAttributes, ParseFrom};
2use crate::util::text::bytes_to_string;
3use compact_str::CompactString;
4use serde_json::Value;
5use std::ops::Deref;
6use std::sync::Arc;
7
8/// Optimized string type for small strings (≤24 bytes stored inline)
9///
10/// Uses `CompactString` which stores strings up to 24 bytes inline without heap allocation.
11/// This significantly reduces allocations for common short strings like language codes,
12/// author names, category terms, and other metadata fields.
13///
14/// `CompactString` implements `Deref<Target=str>`, so it can be used transparently as a string.
15///
16/// # Examples
17///
18/// ```
19/// use feedparser_rs::types::SmallString;
20///
21/// let s: SmallString = "en-US".into();
22/// assert_eq!(s.as_str(), "en-US");
23/// assert_eq!(s.len(), 5); // Stored inline, no heap allocation
24/// ```
25pub type SmallString = CompactString;
26
27/// URL newtype for type-safe URL handling
28///
29/// Provides a semantic wrapper around string URLs without validation.
30/// Following the bozo pattern, URLs are not validated during parsing.
31///
32/// # Examples
33///
34/// ```
35/// use feedparser_rs::Url;
36///
37/// let url = Url::new("https://example.com");
38/// assert_eq!(url.as_str(), "https://example.com");
39///
40/// // Deref coercion allows transparent string access
41/// let len: usize = url.len();
42/// assert_eq!(len, 19);
43/// ```
44#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
45#[serde(transparent)]
46pub struct Url(String);
47
48impl Url {
49    /// Creates a new URL from any type that can be converted to a String
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// use feedparser_rs::Url;
55    ///
56    /// let url1 = Url::new("https://example.com");
57    /// let url2 = Url::new(String::from("https://example.com"));
58    /// assert_eq!(url1, url2);
59    /// ```
60    #[inline]
61    pub fn new(s: impl Into<String>) -> Self {
62        Self(s.into())
63    }
64
65    /// Returns the URL as a string slice
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use feedparser_rs::Url;
71    ///
72    /// let url = Url::new("https://example.com");
73    /// assert_eq!(url.as_str(), "https://example.com");
74    /// ```
75    #[inline]
76    pub fn as_str(&self) -> &str {
77        &self.0
78    }
79
80    /// Consumes the `Url` and returns the inner `String`
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use feedparser_rs::Url;
86    ///
87    /// let url = Url::new("https://example.com");
88    /// let inner: String = url.into_inner();
89    /// assert_eq!(inner, "https://example.com");
90    /// ```
91    #[inline]
92    pub fn into_inner(self) -> String {
93        self.0
94    }
95}
96
97impl Deref for Url {
98    type Target = str;
99
100    #[inline]
101    fn deref(&self) -> &str {
102        &self.0
103    }
104}
105
106impl From<String> for Url {
107    #[inline]
108    fn from(s: String) -> Self {
109        Self(s)
110    }
111}
112
113impl From<&str> for Url {
114    #[inline]
115    fn from(s: &str) -> Self {
116        Self(s.to_string())
117    }
118}
119
120impl AsRef<str> for Url {
121    #[inline]
122    fn as_ref(&self) -> &str {
123        &self.0
124    }
125}
126
127impl std::fmt::Display for Url {
128    #[inline]
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        self.0.fmt(f)
131    }
132}
133
134impl PartialEq<str> for Url {
135    fn eq(&self, other: &str) -> bool {
136        self.0 == other
137    }
138}
139
140impl PartialEq<&str> for Url {
141    fn eq(&self, other: &&str) -> bool {
142        self.0 == *other
143    }
144}
145
146impl PartialEq<String> for Url {
147    fn eq(&self, other: &String) -> bool {
148        &self.0 == other
149    }
150}
151
152/// MIME type newtype with string interning
153///
154/// Uses `Arc<str>` for efficient cloning of common MIME types.
155/// Multiple references to the same MIME type share the same allocation.
156///
157/// # Examples
158///
159/// ```
160/// use feedparser_rs::MimeType;
161///
162/// let mime = MimeType::new("text/html");
163/// assert_eq!(mime.as_str(), "text/html");
164///
165/// // Cloning is cheap (just increments reference count)
166/// let clone = mime.clone();
167/// assert_eq!(mime, clone);
168/// ```
169#[derive(Debug, Clone, PartialEq, Eq, Hash)]
170pub struct MimeType(Arc<str>);
171
172// Custom serde implementation for MimeType since Arc<str> doesn't implement Serialize/Deserialize
173impl serde::Serialize for MimeType {
174    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
175    where
176        S: serde::Serializer,
177    {
178        serializer.serialize_str(&self.0)
179    }
180}
181
182impl<'de> serde::Deserialize<'de> for MimeType {
183    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184    where
185        D: serde::Deserializer<'de>,
186    {
187        let s = <String as serde::Deserialize>::deserialize(deserializer)?;
188        Ok(Self::new(s))
189    }
190}
191
192impl MimeType {
193    /// Creates a new MIME type from any string-like type
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use feedparser_rs::MimeType;
199    ///
200    /// let mime = MimeType::new("application/json");
201    /// assert_eq!(mime.as_str(), "application/json");
202    /// ```
203    #[inline]
204    pub fn new(s: impl AsRef<str>) -> Self {
205        Self(Arc::from(s.as_ref()))
206    }
207
208    /// Returns the MIME type as a string slice
209    ///
210    /// # Examples
211    ///
212    /// ```
213    /// use feedparser_rs::MimeType;
214    ///
215    /// let mime = MimeType::new("text/plain");
216    /// assert_eq!(mime.as_str(), "text/plain");
217    /// ```
218    #[inline]
219    pub fn as_str(&self) -> &str {
220        &self.0
221    }
222
223    /// Common MIME type constants for convenience.
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// use feedparser_rs::MimeType;
229    ///
230    /// let html = MimeType::new(MimeType::TEXT_HTML);
231    /// assert_eq!(html.as_str(), "text/html");
232    /// ```
233    pub const TEXT_HTML: &'static str = "text/html";
234
235    /// `text/plain` MIME type constant
236    pub const TEXT_PLAIN: &'static str = "text/plain";
237
238    /// `application/xml` MIME type constant
239    pub const APPLICATION_XML: &'static str = "application/xml";
240
241    /// `application/json` MIME type constant
242    pub const APPLICATION_JSON: &'static str = "application/json";
243}
244
245impl Default for MimeType {
246    #[inline]
247    fn default() -> Self {
248        Self(Arc::from(""))
249    }
250}
251
252impl Deref for MimeType {
253    type Target = str;
254
255    #[inline]
256    fn deref(&self) -> &str {
257        &self.0
258    }
259}
260
261impl From<String> for MimeType {
262    #[inline]
263    fn from(s: String) -> Self {
264        Self(Arc::from(s.as_str()))
265    }
266}
267
268impl From<&str> for MimeType {
269    #[inline]
270    fn from(s: &str) -> Self {
271        Self(Arc::from(s))
272    }
273}
274
275impl AsRef<str> for MimeType {
276    #[inline]
277    fn as_ref(&self) -> &str {
278        &self.0
279    }
280}
281
282impl std::fmt::Display for MimeType {
283    #[inline]
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        self.0.fmt(f)
286    }
287}
288
289impl PartialEq<str> for MimeType {
290    fn eq(&self, other: &str) -> bool {
291        &*self.0 == other
292    }
293}
294
295impl PartialEq<&str> for MimeType {
296    fn eq(&self, other: &&str) -> bool {
297        &*self.0 == *other
298    }
299}
300
301impl PartialEq<String> for MimeType {
302    fn eq(&self, other: &String) -> bool {
303        &*self.0 == other
304    }
305}
306
307/// Email newtype for type-safe email handling
308///
309/// Provides a semantic wrapper around email addresses without validation.
310/// Following the bozo pattern, emails are not validated during parsing.
311///
312/// # Examples
313///
314/// ```
315/// use feedparser_rs::Email;
316///
317/// let email = Email::new("user@example.com");
318/// assert_eq!(email.as_str(), "user@example.com");
319/// ```
320#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
321#[serde(transparent)]
322pub struct Email(String);
323
324impl Email {
325    /// Creates a new email from any type that can be converted to a String
326    ///
327    /// # Examples
328    ///
329    /// ```
330    /// use feedparser_rs::Email;
331    ///
332    /// let email = Email::new("user@example.com");
333    /// assert_eq!(email.as_str(), "user@example.com");
334    /// ```
335    #[inline]
336    pub fn new(s: impl Into<String>) -> Self {
337        Self(s.into())
338    }
339
340    /// Returns the email as a string slice
341    ///
342    /// # Examples
343    ///
344    /// ```
345    /// use feedparser_rs::Email;
346    ///
347    /// let email = Email::new("user@example.com");
348    /// assert_eq!(email.as_str(), "user@example.com");
349    /// ```
350    #[inline]
351    pub fn as_str(&self) -> &str {
352        &self.0
353    }
354
355    /// Consumes the `Email` and returns the inner `String`
356    ///
357    /// # Examples
358    ///
359    /// ```
360    /// use feedparser_rs::Email;
361    ///
362    /// let email = Email::new("user@example.com");
363    /// let inner: String = email.into_inner();
364    /// assert_eq!(inner, "user@example.com");
365    /// ```
366    #[inline]
367    pub fn into_inner(self) -> String {
368        self.0
369    }
370}
371
372impl Deref for Email {
373    type Target = str;
374
375    #[inline]
376    fn deref(&self) -> &str {
377        &self.0
378    }
379}
380
381impl From<String> for Email {
382    #[inline]
383    fn from(s: String) -> Self {
384        Self(s)
385    }
386}
387
388impl From<&str> for Email {
389    #[inline]
390    fn from(s: &str) -> Self {
391        Self(s.to_string())
392    }
393}
394
395impl AsRef<str> for Email {
396    #[inline]
397    fn as_ref(&self) -> &str {
398        &self.0
399    }
400}
401
402impl std::fmt::Display for Email {
403    #[inline]
404    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405        self.0.fmt(f)
406    }
407}
408
409impl PartialEq<str> for Email {
410    fn eq(&self, other: &str) -> bool {
411        self.0 == other
412    }
413}
414
415impl PartialEq<&str> for Email {
416    fn eq(&self, other: &&str) -> bool {
417        self.0 == *other
418    }
419}
420
421impl PartialEq<String> for Email {
422    fn eq(&self, other: &String) -> bool {
423        &self.0 == other
424    }
425}
426
427/// Link in feed or entry
428#[derive(Debug, Clone, Default)]
429pub struct Link {
430    /// Link URL
431    pub href: Url,
432    /// Link relationship type (e.g., "alternate", "enclosure", "self")
433    /// Stored inline as these are typically short (≤24 bytes)
434    pub rel: Option<SmallString>,
435    /// MIME type of the linked resource
436    pub link_type: Option<MimeType>,
437    /// Human-readable link title
438    pub title: Option<String>,
439    /// Length of the linked resource in bytes
440    pub length: Option<u64>,
441    /// Language of the linked resource (stored inline for lang codes ≤24 bytes)
442    pub hreflang: Option<SmallString>,
443}
444
445impl Link {
446    /// Create a new link with just URL and relation type
447    #[inline]
448    pub fn new(href: impl Into<Url>, rel: impl AsRef<str>) -> Self {
449        Self {
450            href: href.into(),
451            rel: Some(rel.as_ref().into()),
452            link_type: None,
453            title: None,
454            length: None,
455            hreflang: None,
456        }
457    }
458
459    /// Create an alternate link (common for entry URLs)
460    #[inline]
461    pub fn alternate(href: impl Into<Url>) -> Self {
462        Self::new(href, "alternate")
463    }
464
465    /// Create a self link (for feed URLs)
466    #[inline]
467    pub fn self_link(href: impl Into<Url>, mime_type: impl Into<MimeType>) -> Self {
468        Self {
469            href: href.into(),
470            rel: Some("self".into()),
471            link_type: Some(mime_type.into()),
472            title: None,
473            length: None,
474            hreflang: None,
475        }
476    }
477
478    /// Create an enclosure link (for media)
479    #[inline]
480    pub fn enclosure(href: impl Into<Url>, mime_type: Option<MimeType>) -> Self {
481        Self {
482            href: href.into(),
483            rel: Some("enclosure".into()),
484            link_type: mime_type,
485            title: None,
486            length: None,
487            hreflang: None,
488        }
489    }
490
491    /// Create a related link
492    #[inline]
493    pub fn related(href: impl Into<Url>) -> Self {
494        Self::new(href, "related")
495    }
496
497    /// Set MIME type (builder pattern)
498    #[inline]
499    #[must_use]
500    pub fn with_type(mut self, mime_type: impl Into<MimeType>) -> Self {
501        self.link_type = Some(mime_type.into());
502        self
503    }
504}
505
506/// Person (author, contributor, etc.)
507#[derive(Debug, Clone, Default)]
508pub struct Person {
509    /// Person's name (stored inline for names ≤24 bytes)
510    pub name: Option<SmallString>,
511    /// Person's email address
512    pub email: Option<Email>,
513    /// Person's URI/website
514    pub uri: Option<String>,
515}
516
517impl Person {
518    /// Create person from just a name
519    ///
520    /// # Examples
521    ///
522    /// ```
523    /// use feedparser_rs::types::Person;
524    ///
525    /// let person = Person::from_name("John Doe");
526    /// assert_eq!(person.name.as_deref(), Some("John Doe"));
527    /// assert!(person.email.is_none());
528    /// assert!(person.uri.is_none());
529    /// ```
530    #[inline]
531    pub fn from_name(name: impl AsRef<str>) -> Self {
532        Self {
533            name: Some(name.as_ref().into()),
534            email: None,
535            uri: None,
536        }
537    }
538}
539
540/// Tag/category
541#[derive(Debug, Clone)]
542pub struct Tag {
543    /// Tag term/label (stored inline for terms ≤24 bytes)
544    pub term: SmallString,
545    /// Tag scheme/domain (stored inline for schemes ≤24 bytes)
546    pub scheme: Option<SmallString>,
547    /// Human-readable tag label (stored inline for labels ≤24 bytes)
548    pub label: Option<SmallString>,
549}
550
551impl Tag {
552    /// Create a simple tag with just term
553    #[inline]
554    pub fn new(term: impl AsRef<str>) -> Self {
555        Self {
556            term: term.as_ref().into(),
557            scheme: None,
558            label: None,
559        }
560    }
561}
562
563/// Image metadata
564#[derive(Debug, Clone)]
565pub struct Image {
566    /// Image URL
567    pub url: Url,
568    /// Image title
569    pub title: Option<String>,
570    /// Link associated with the image
571    pub link: Option<String>,
572    /// Image width in pixels
573    pub width: Option<u32>,
574    /// Image height in pixels
575    pub height: Option<u32>,
576    /// Image description
577    pub description: Option<String>,
578}
579
580/// Enclosure (attached media file)
581#[derive(Debug, Clone)]
582pub struct Enclosure {
583    /// Enclosure URL
584    pub url: Url,
585    /// File size in bytes
586    pub length: Option<u64>,
587    /// MIME type
588    pub enclosure_type: Option<MimeType>,
589}
590
591/// Content block
592#[derive(Debug, Clone)]
593pub struct Content {
594    /// Content body
595    pub value: String,
596    /// Content MIME type
597    pub content_type: Option<MimeType>,
598    /// Content language (stored inline for lang codes ≤24 bytes)
599    pub language: Option<SmallString>,
600    /// Base URL for relative links
601    pub base: Option<String>,
602}
603
604impl Content {
605    /// Create HTML content
606    #[inline]
607    pub fn html(value: impl Into<String>) -> Self {
608        Self {
609            value: value.into(),
610            content_type: Some(MimeType::new(MimeType::TEXT_HTML)),
611            language: None,
612            base: None,
613        }
614    }
615
616    /// Create plain text content
617    #[inline]
618    pub fn plain(value: impl Into<String>) -> Self {
619        Self {
620            value: value.into(),
621            content_type: Some(MimeType::new(MimeType::TEXT_PLAIN)),
622            language: None,
623            base: None,
624        }
625    }
626}
627
628/// Text construct type (Atom-style)
629#[derive(Debug, Clone, Copy, PartialEq, Eq)]
630pub enum TextType {
631    /// Plain text
632    Text,
633    /// HTML content
634    Html,
635    /// XHTML content
636    Xhtml,
637}
638
639/// Text construct with metadata
640#[derive(Debug, Clone)]
641pub struct TextConstruct {
642    /// Text content
643    pub value: String,
644    /// Content type
645    pub content_type: TextType,
646    /// Content language (stored inline for lang codes ≤24 bytes)
647    pub language: Option<SmallString>,
648    /// Base URL for relative links
649    pub base: Option<String>,
650}
651
652impl TextConstruct {
653    /// Create plain text construct
654    #[inline]
655    pub fn text(value: impl Into<String>) -> Self {
656        Self {
657            value: value.into(),
658            content_type: TextType::Text,
659            language: None,
660            base: None,
661        }
662    }
663
664    /// Create HTML text construct
665    #[inline]
666    pub fn html(value: impl Into<String>) -> Self {
667        Self {
668            value: value.into(),
669            content_type: TextType::Html,
670            language: None,
671            base: None,
672        }
673    }
674
675    /// Set language (builder pattern)
676    #[inline]
677    #[must_use]
678    pub fn with_language(mut self, language: impl AsRef<str>) -> Self {
679        self.language = Some(language.as_ref().into());
680        self
681    }
682}
683
684/// Generator metadata
685#[derive(Debug, Clone)]
686pub struct Generator {
687    /// Generator name
688    pub value: String,
689    /// Generator URI
690    pub uri: Option<String>,
691    /// Generator version (stored inline for versions ≤24 bytes)
692    pub version: Option<SmallString>,
693}
694
695/// Source reference (for entries)
696#[derive(Debug, Clone)]
697pub struct Source {
698    /// Source title
699    pub title: Option<String>,
700    /// Source link
701    pub link: Option<String>,
702    /// Source ID
703    pub id: Option<String>,
704}
705
706/// Media RSS thumbnail
707#[derive(Debug, Clone)]
708pub struct MediaThumbnail {
709    /// Thumbnail URL
710    ///
711    /// # Security Warning
712    ///
713    /// This URL comes from untrusted feed input and has NOT been validated for SSRF.
714    /// Applications MUST validate URLs before fetching to prevent SSRF attacks.
715    pub url: Url,
716    /// Thumbnail width in pixels
717    pub width: Option<u32>,
718    /// Thumbnail height in pixels
719    pub height: Option<u32>,
720}
721
722/// Media RSS content
723#[derive(Debug, Clone)]
724pub struct MediaContent {
725    /// Media URL
726    ///
727    /// # Security Warning
728    ///
729    /// This URL comes from untrusted feed input and has NOT been validated for SSRF.
730    /// Applications MUST validate URLs before fetching to prevent SSRF attacks.
731    pub url: Url,
732    /// MIME type
733    pub content_type: Option<MimeType>,
734    /// File size in bytes
735    pub filesize: Option<u64>,
736    /// Media width in pixels
737    pub width: Option<u32>,
738    /// Media height in pixels
739    pub height: Option<u32>,
740    /// Duration in seconds (for audio/video)
741    pub duration: Option<u64>,
742}
743
744impl FromAttributes for Link {
745    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
746    where
747        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
748    {
749        let mut href = None;
750        let mut rel = None;
751        let mut link_type = None;
752        let mut title = None;
753        let mut hreflang = None;
754        let mut length = None;
755
756        for attr in attrs {
757            if attr.value.len() > max_attr_length {
758                continue;
759            }
760            match attr.key.as_ref() {
761                b"href" => href = Some(bytes_to_string(&attr.value)),
762                b"rel" => rel = Some(bytes_to_string(&attr.value)),
763                b"type" => link_type = Some(bytes_to_string(&attr.value)),
764                b"title" => title = Some(bytes_to_string(&attr.value)),
765                b"hreflang" => hreflang = Some(bytes_to_string(&attr.value)),
766                b"length" => length = bytes_to_string(&attr.value).parse().ok(),
767                _ => {}
768            }
769        }
770
771        href.map(|href| Self {
772            href: Url::new(href),
773            rel: rel
774                .map(std::convert::Into::into)
775                .or_else(|| Some("alternate".into())),
776            link_type: link_type.map(MimeType::new),
777            title,
778            length,
779            hreflang: hreflang.map(std::convert::Into::into),
780        })
781    }
782}
783
784impl FromAttributes for Tag {
785    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
786    where
787        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
788    {
789        let mut term = None;
790        let mut scheme = None;
791        let mut label = None;
792
793        for attr in attrs {
794            if attr.value.len() > max_attr_length {
795                continue;
796            }
797
798            match attr.key.as_ref() {
799                b"term" => term = Some(bytes_to_string(&attr.value)),
800                b"scheme" | b"domain" => scheme = Some(bytes_to_string(&attr.value)),
801                b"label" => label = Some(bytes_to_string(&attr.value)),
802                _ => {}
803            }
804        }
805
806        term.map(|term| Self {
807            term: term.into(),
808            scheme: scheme.map(std::convert::Into::into),
809            label: label.map(std::convert::Into::into),
810        })
811    }
812}
813
814impl FromAttributes for Enclosure {
815    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
816    where
817        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
818    {
819        let mut url = None;
820        let mut length = None;
821        let mut enclosure_type = None;
822
823        for attr in attrs {
824            if attr.value.len() > max_attr_length {
825                continue;
826            }
827
828            match attr.key.as_ref() {
829                b"url" => url = Some(bytes_to_string(&attr.value)),
830                b"length" => length = bytes_to_string(&attr.value).parse().ok(),
831                b"type" => enclosure_type = Some(bytes_to_string(&attr.value)),
832                _ => {}
833            }
834        }
835
836        url.map(|url| Self {
837            url: Url::new(url),
838            length,
839            enclosure_type: enclosure_type.map(MimeType::new),
840        })
841    }
842}
843
844impl FromAttributes for MediaThumbnail {
845    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
846    where
847        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
848    {
849        let mut url = None;
850        let mut width = None;
851        let mut height = None;
852
853        for attr in attrs {
854            if attr.value.len() > max_attr_length {
855                continue;
856            }
857
858            match attr.key.as_ref() {
859                b"url" => url = Some(bytes_to_string(&attr.value)),
860                b"width" => width = bytes_to_string(&attr.value).parse().ok(),
861                b"height" => height = bytes_to_string(&attr.value).parse().ok(),
862                _ => {}
863            }
864        }
865
866        url.map(|url| Self {
867            url: Url::new(url),
868            width,
869            height,
870        })
871    }
872}
873
874impl FromAttributes for MediaContent {
875    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
876    where
877        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
878    {
879        let mut url = None;
880        let mut content_type = None;
881        let mut filesize = None;
882        let mut width = None;
883        let mut height = None;
884        let mut duration = None;
885
886        for attr in attrs {
887            if attr.value.len() > max_attr_length {
888                continue;
889            }
890
891            match attr.key.as_ref() {
892                b"url" => url = Some(bytes_to_string(&attr.value)),
893                b"type" => content_type = Some(bytes_to_string(&attr.value)),
894                b"fileSize" => filesize = bytes_to_string(&attr.value).parse().ok(),
895                b"width" => width = bytes_to_string(&attr.value).parse().ok(),
896                b"height" => height = bytes_to_string(&attr.value).parse().ok(),
897                b"duration" => duration = bytes_to_string(&attr.value).parse().ok(),
898                _ => {}
899            }
900        }
901
902        url.map(|url| Self {
903            url: Url::new(url),
904            content_type: content_type.map(MimeType::new),
905            filesize,
906            width,
907            height,
908            duration,
909        })
910    }
911}
912
913// ParseFrom implementations for JSON Feed parsing
914
915impl ParseFrom<&Value> for Person {
916    /// Parse Person from JSON Feed author object
917    ///
918    /// JSON Feed format: `{"name": "...", "url": "...", "avatar": "..."}`
919    fn parse_from(json: &Value) -> Option<Self> {
920        json.as_object().map(|obj| Self {
921            name: obj
922                .get("name")
923                .and_then(Value::as_str)
924                .map(std::convert::Into::into),
925            email: None, // JSON Feed doesn't have email field
926            uri: obj.get("url").and_then(Value::as_str).map(String::from),
927        })
928    }
929}
930
931impl ParseFrom<&Value> for Enclosure {
932    /// Parse Enclosure from JSON Feed attachment object
933    ///
934    /// JSON Feed format: `{"url": "...", "mime_type": "...", "size_in_bytes": ...}`
935    fn parse_from(json: &Value) -> Option<Self> {
936        let obj = json.as_object()?;
937        let url = obj.get("url").and_then(Value::as_str)?;
938        Some(Self {
939            url: Url::new(url),
940            length: obj.get("size_in_bytes").and_then(Value::as_u64),
941            enclosure_type: obj
942                .get("mime_type")
943                .and_then(Value::as_str)
944                .map(MimeType::new),
945        })
946    }
947}
948
949#[cfg(test)]
950mod tests {
951    use super::*;
952    use serde_json::json;
953
954    #[test]
955    fn test_link_default() {
956        let link = Link::default();
957        assert!(link.href.is_empty());
958        assert!(link.rel.is_none());
959    }
960
961    #[test]
962    fn test_link_builders() {
963        let link = Link::alternate("https://example.com");
964        assert_eq!(link.href, "https://example.com");
965        assert_eq!(link.rel.as_deref(), Some("alternate"));
966
967        let link = Link::self_link("https://example.com/feed", "application/feed+json");
968        assert_eq!(link.rel.as_deref(), Some("self"));
969        assert_eq!(link.link_type.as_deref(), Some("application/feed+json"));
970
971        let link = Link::enclosure("https://example.com/audio.mp3", Some("audio/mpeg".into()));
972        assert_eq!(link.rel.as_deref(), Some("enclosure"));
973        assert_eq!(link.link_type.as_deref(), Some("audio/mpeg"));
974
975        let link = Link::related("https://other.com");
976        assert_eq!(link.rel.as_deref(), Some("related"));
977    }
978
979    #[test]
980    fn test_tag_builder() {
981        let tag = Tag::new("rust");
982        assert_eq!(tag.term, "rust");
983        assert!(tag.scheme.is_none());
984    }
985
986    #[test]
987    fn test_text_construct_builders() {
988        let text = TextConstruct::text("Hello");
989        assert_eq!(text.value, "Hello");
990        assert_eq!(text.content_type, TextType::Text);
991
992        let html = TextConstruct::html("<p>Hello</p>");
993        assert_eq!(html.content_type, TextType::Html);
994
995        let with_lang = TextConstruct::text("Hello").with_language("en");
996        assert_eq!(with_lang.language.as_deref(), Some("en"));
997    }
998
999    #[test]
1000    fn test_content_builders() {
1001        let html = Content::html("<p>Content</p>");
1002        assert_eq!(html.content_type.as_deref(), Some("text/html"));
1003
1004        let plain = Content::plain("Content");
1005        assert_eq!(plain.content_type.as_deref(), Some("text/plain"));
1006    }
1007
1008    #[test]
1009    fn test_person_default() {
1010        let person = Person::default();
1011        assert!(person.name.is_none());
1012        assert!(person.email.is_none());
1013        assert!(person.uri.is_none());
1014    }
1015
1016    #[test]
1017    fn test_person_parse_from_json() {
1018        let json = json!({"name": "John Doe", "url": "https://example.com"});
1019        let person = Person::parse_from(&json).unwrap();
1020        assert_eq!(person.name.as_deref(), Some("John Doe"));
1021        assert_eq!(person.uri.as_deref(), Some("https://example.com"));
1022        assert!(person.email.is_none());
1023    }
1024
1025    #[test]
1026    fn test_person_parse_from_empty_json() {
1027        let json = json!({});
1028        let person = Person::parse_from(&json).unwrap();
1029        assert!(person.name.is_none());
1030    }
1031
1032    #[test]
1033    fn test_enclosure_parse_from_json() {
1034        let json = json!({
1035            "url": "https://example.com/file.mp3",
1036            "mime_type": "audio/mpeg",
1037            "size_in_bytes": 12345
1038        });
1039        let enclosure = Enclosure::parse_from(&json).unwrap();
1040        assert_eq!(enclosure.url, "https://example.com/file.mp3");
1041        assert_eq!(enclosure.enclosure_type.as_deref(), Some("audio/mpeg"));
1042        assert_eq!(enclosure.length, Some(12345));
1043    }
1044
1045    #[test]
1046    fn test_enclosure_parse_from_json_missing_url() {
1047        let json = json!({"mime_type": "audio/mpeg"});
1048        assert!(Enclosure::parse_from(&json).is_none());
1049    }
1050
1051    #[test]
1052    fn test_text_type_equality() {
1053        assert_eq!(TextType::Text, TextType::Text);
1054        assert_ne!(TextType::Text, TextType::Html);
1055    }
1056
1057    // Newtype tests
1058
1059    #[test]
1060    fn test_url_new() {
1061        let url = Url::new("https://example.com");
1062        assert_eq!(url.as_str(), "https://example.com");
1063    }
1064
1065    #[test]
1066    fn test_url_from_string() {
1067        let url: Url = String::from("https://example.com").into();
1068        assert_eq!(url.as_str(), "https://example.com");
1069    }
1070
1071    #[test]
1072    fn test_url_from_str() {
1073        let url: Url = "https://example.com".into();
1074        assert_eq!(url.as_str(), "https://example.com");
1075    }
1076
1077    #[test]
1078    fn test_url_deref() {
1079        let url = Url::new("https://example.com");
1080        // Deref allows calling str methods directly
1081        assert_eq!(url.len(), 19);
1082        assert!(url.starts_with("https://"));
1083    }
1084
1085    #[test]
1086    fn test_url_into_inner() {
1087        let url = Url::new("https://example.com");
1088        let inner = url.into_inner();
1089        assert_eq!(inner, "https://example.com");
1090    }
1091
1092    #[test]
1093    fn test_url_default() {
1094        let url = Url::default();
1095        assert_eq!(url.as_str(), "");
1096    }
1097
1098    #[test]
1099    fn test_url_clone() {
1100        let url1 = Url::new("https://example.com");
1101        let url2 = url1.clone();
1102        assert_eq!(url1, url2);
1103    }
1104
1105    #[test]
1106    fn test_mime_type_new() {
1107        let mime = MimeType::new("text/html");
1108        assert_eq!(mime.as_str(), "text/html");
1109    }
1110
1111    #[test]
1112    fn test_mime_type_from_string() {
1113        let mime: MimeType = String::from("application/json").into();
1114        assert_eq!(mime.as_str(), "application/json");
1115    }
1116
1117    #[test]
1118    fn test_mime_type_from_str() {
1119        let mime: MimeType = "text/plain".into();
1120        assert_eq!(mime.as_str(), "text/plain");
1121    }
1122
1123    #[test]
1124    fn test_mime_type_deref() {
1125        let mime = MimeType::new("text/html");
1126        assert_eq!(mime.len(), 9);
1127        assert!(mime.starts_with("text/"));
1128    }
1129
1130    #[test]
1131    fn test_mime_type_default() {
1132        let mime = MimeType::default();
1133        assert_eq!(mime.as_str(), "");
1134    }
1135
1136    #[test]
1137    fn test_mime_type_clone() {
1138        let mime1 = MimeType::new("application/xml");
1139        let mime2 = mime1.clone();
1140        assert_eq!(mime1, mime2);
1141        // Arc cloning is cheap - just increments refcount
1142    }
1143
1144    #[test]
1145    fn test_mime_type_constants() {
1146        assert_eq!(MimeType::TEXT_HTML, "text/html");
1147        assert_eq!(MimeType::TEXT_PLAIN, "text/plain");
1148        assert_eq!(MimeType::APPLICATION_XML, "application/xml");
1149        assert_eq!(MimeType::APPLICATION_JSON, "application/json");
1150    }
1151
1152    #[test]
1153    fn test_email_new() {
1154        let email = Email::new("user@example.com");
1155        assert_eq!(email.as_str(), "user@example.com");
1156    }
1157
1158    #[test]
1159    fn test_email_from_string() {
1160        let email: Email = String::from("user@example.com").into();
1161        assert_eq!(email.as_str(), "user@example.com");
1162    }
1163
1164    #[test]
1165    fn test_email_from_str() {
1166        let email: Email = "user@example.com".into();
1167        assert_eq!(email.as_str(), "user@example.com");
1168    }
1169
1170    #[test]
1171    fn test_email_deref() {
1172        let email = Email::new("user@example.com");
1173        assert_eq!(email.len(), 16);
1174        assert!(email.contains('@'));
1175    }
1176
1177    #[test]
1178    fn test_email_into_inner() {
1179        let email = Email::new("user@example.com");
1180        let inner = email.into_inner();
1181        assert_eq!(inner, "user@example.com");
1182    }
1183
1184    #[test]
1185    fn test_email_default() {
1186        let email = Email::default();
1187        assert_eq!(email.as_str(), "");
1188    }
1189
1190    #[test]
1191    fn test_email_clone() {
1192        let email1 = Email::new("user@example.com");
1193        let email2 = email1.clone();
1194        assert_eq!(email1, email2);
1195    }
1196}