Skip to main content

feedparser_rs/types/
common.rs

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
9/// Optimized string type for small strings (≤24 bytes stored inline)
10///
11/// Uses `CompactString` which stores strings up to 24 bytes inline without heap allocation.
12/// This significantly reduces allocations for common short strings like language codes,
13/// author names, category terms, and other metadata fields.
14///
15/// `CompactString` implements `Deref<Target=str>`, so it can be used transparently as a string.
16///
17/// # Examples
18///
19/// ```
20/// use feedparser_rs::types::SmallString;
21///
22/// let s: SmallString = "en-US".into();
23/// assert_eq!(s.as_str(), "en-US");
24/// assert_eq!(s.len(), 5); // Stored inline, no heap allocation
25/// ```
26pub type SmallString = CompactString;
27
28/// URL newtype for type-safe URL handling
29///
30/// Provides a semantic wrapper around string URLs without validation.
31/// Following the bozo pattern, URLs are not validated during parsing.
32///
33/// # Examples
34///
35/// ```
36/// use feedparser_rs::Url;
37///
38/// let url = Url::new("https://example.com");
39/// assert_eq!(url.as_str(), "https://example.com");
40///
41/// // Deref coercion allows transparent string access
42/// let len: usize = url.len();
43/// assert_eq!(len, 19);
44/// ```
45#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
46#[serde(transparent)]
47pub struct Url(String);
48
49impl Url {
50    /// Creates a new URL from any type that can be converted to a String
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use feedparser_rs::Url;
56    ///
57    /// let url1 = Url::new("https://example.com");
58    /// let url2 = Url::new(String::from("https://example.com"));
59    /// assert_eq!(url1, url2);
60    /// ```
61    #[inline]
62    pub fn new(s: impl Into<String>) -> Self {
63        Self(s.into())
64    }
65
66    /// Returns the URL as a string slice
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use feedparser_rs::Url;
72    ///
73    /// let url = Url::new("https://example.com");
74    /// assert_eq!(url.as_str(), "https://example.com");
75    /// ```
76    #[inline]
77    pub fn as_str(&self) -> &str {
78        &self.0
79    }
80
81    /// Consumes the `Url` and returns the inner `String`
82    ///
83    /// # Examples
84    ///
85    /// ```
86    /// use feedparser_rs::Url;
87    ///
88    /// let url = Url::new("https://example.com");
89    /// let inner: String = url.into_inner();
90    /// assert_eq!(inner, "https://example.com");
91    /// ```
92    #[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/// MIME type newtype with string interning
154///
155/// Uses `Arc<str>` for efficient cloning of common MIME types.
156/// Multiple references to the same MIME type share the same allocation.
157///
158/// # Examples
159///
160/// ```
161/// use feedparser_rs::MimeType;
162///
163/// let mime = MimeType::new("text/html");
164/// assert_eq!(mime.as_str(), "text/html");
165///
166/// // Cloning is cheap (just increments reference count)
167/// let clone = mime.clone();
168/// assert_eq!(mime, clone);
169/// ```
170#[derive(Debug, Clone, PartialEq, Eq, Hash)]
171pub struct MimeType(Arc<str>);
172
173// Custom serde implementation for MimeType since Arc<str> doesn't implement Serialize/Deserialize
174impl 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    /// Creates a new MIME type from any string-like type
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use feedparser_rs::MimeType;
200    ///
201    /// let mime = MimeType::new("application/json");
202    /// assert_eq!(mime.as_str(), "application/json");
203    /// ```
204    #[inline]
205    pub fn new(s: impl AsRef<str>) -> Self {
206        Self(Arc::from(s.as_ref()))
207    }
208
209    /// Returns the MIME type as a string slice
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// use feedparser_rs::MimeType;
215    ///
216    /// let mime = MimeType::new("text/plain");
217    /// assert_eq!(mime.as_str(), "text/plain");
218    /// ```
219    #[inline]
220    pub fn as_str(&self) -> &str {
221        &self.0
222    }
223
224    /// Common MIME type constants for convenience.
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use feedparser_rs::MimeType;
230    ///
231    /// let html = MimeType::new(MimeType::TEXT_HTML);
232    /// assert_eq!(html.as_str(), "text/html");
233    /// ```
234    pub const TEXT_HTML: &'static str = "text/html";
235
236    /// `text/plain` MIME type constant
237    pub const TEXT_PLAIN: &'static str = "text/plain";
238
239    /// `application/xml` MIME type constant
240    pub const APPLICATION_XML: &'static str = "application/xml";
241
242    /// `application/json` MIME type constant
243    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/// Email newtype for type-safe email handling
309///
310/// Provides a semantic wrapper around email addresses without validation.
311/// Following the bozo pattern, emails are not validated during parsing.
312///
313/// # Examples
314///
315/// ```
316/// use feedparser_rs::Email;
317///
318/// let email = Email::new("user@example.com");
319/// assert_eq!(email.as_str(), "user@example.com");
320/// ```
321#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
322#[serde(transparent)]
323pub struct Email(String);
324
325impl Email {
326    /// Creates a new email from any type that can be converted to a String
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use feedparser_rs::Email;
332    ///
333    /// let email = Email::new("user@example.com");
334    /// assert_eq!(email.as_str(), "user@example.com");
335    /// ```
336    #[inline]
337    pub fn new(s: impl Into<String>) -> Self {
338        Self(s.into())
339    }
340
341    /// Returns the email as a string slice
342    ///
343    /// # Examples
344    ///
345    /// ```
346    /// use feedparser_rs::Email;
347    ///
348    /// let email = Email::new("user@example.com");
349    /// assert_eq!(email.as_str(), "user@example.com");
350    /// ```
351    #[inline]
352    pub fn as_str(&self) -> &str {
353        &self.0
354    }
355
356    /// Consumes the `Email` and returns the inner `String`
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// use feedparser_rs::Email;
362    ///
363    /// let email = Email::new("user@example.com");
364    /// let inner: String = email.into_inner();
365    /// assert_eq!(inner, "user@example.com");
366    /// ```
367    #[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/// Link in feed or entry
429#[derive(Debug, Clone, Default)]
430pub struct Link {
431    /// Link URL
432    pub href: Url,
433    /// Link relationship type (e.g., "alternate", "enclosure", "self")
434    /// Stored inline as these are typically short (≤24 bytes)
435    pub rel: Option<SmallString>,
436    /// MIME type of the linked resource
437    pub link_type: Option<MimeType>,
438    /// Human-readable link title
439    pub title: Option<String>,
440    /// Length of the linked resource in bytes
441    pub length: Option<u64>,
442    /// Language of the linked resource (stored inline for lang codes ≤24 bytes)
443    pub hreflang: Option<SmallString>,
444    /// RFC 4685 §4: number of replies at the IRI
445    pub thr_count: Option<u32>,
446    /// RFC 4685 §4: when the reply resource was last modified
447    pub thr_updated: Option<DateTime<Utc>>,
448}
449
450impl Link {
451    /// Create a new link with just URL and relation type
452    #[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    /// Create an alternate link (common for entry URLs)
467    #[inline]
468    pub fn alternate(href: impl Into<Url>) -> Self {
469        Self::new(href, "alternate")
470    }
471
472    /// Create a self link (for feed URLs)
473    #[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    /// Create an enclosure link (for media)
488    #[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    /// Create a related link
503    #[inline]
504    pub fn related(href: impl Into<Url>) -> Self {
505        Self::new(href, "related")
506    }
507
508    /// Create a banner image link (JSON Feed `banner_image`, project-internal convention)
509    ///
510    /// Note: `rel="banner"` is not a standard link relation. It is used here as a project
511    /// convention to store JSON Feed `banner_image` in the existing `Link` type. Consumers
512    /// must search `entry.links` for `rel="banner"` to retrieve this field.
513    #[inline]
514    pub fn banner(href: impl Into<Url>) -> Self {
515        Self::new(href, "banner")
516    }
517
518    /// Set MIME type (builder pattern)
519    #[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/// Person (author, contributor, etc.)
528#[derive(Debug, Clone, Default)]
529pub struct Person {
530    /// Person's name (stored inline for names ≤24 bytes)
531    pub name: Option<SmallString>,
532    /// Person's email address
533    pub email: Option<Email>,
534    /// Person's URI/website
535    pub uri: Option<String>,
536}
537
538impl Person {
539    /// Create person from just a name
540    ///
541    /// # Examples
542    ///
543    /// ```
544    /// use feedparser_rs::types::Person;
545    ///
546    /// let person = Person::from_name("John Doe");
547    /// assert_eq!(person.name.as_deref(), Some("John Doe"));
548    /// assert!(person.email.is_none());
549    /// assert!(person.uri.is_none());
550    /// ```
551    #[inline]
552    pub fn from_name(name: impl AsRef<str>) -> Self {
553        Self {
554            name: Some(name.as_ref().into()),
555            email: None,
556            uri: None,
557        }
558    }
559}
560
561/// Tag/category
562#[derive(Debug, Clone)]
563pub struct Tag {
564    /// Tag term/label (stored inline for terms ≤24 bytes)
565    pub term: SmallString,
566    /// Tag scheme/domain (stored inline for schemes ≤24 bytes)
567    pub scheme: Option<SmallString>,
568    /// Human-readable tag label (stored inline for labels ≤24 bytes)
569    pub label: Option<SmallString>,
570}
571
572impl Tag {
573    /// Create a simple tag with just term
574    #[inline]
575    pub fn new(term: impl AsRef<str>) -> Self {
576        Self {
577            term: term.as_ref().into(),
578            scheme: None,
579            label: None,
580        }
581    }
582}
583
584/// Image metadata
585#[derive(Debug, Clone)]
586pub struct Image {
587    /// Image URL
588    pub url: Url,
589    /// Image title
590    pub title: Option<String>,
591    /// Link associated with the image
592    pub link: Option<String>,
593    /// Image width in pixels
594    pub width: Option<u32>,
595    /// Image height in pixels
596    pub height: Option<u32>,
597    /// Image description
598    pub description: Option<String>,
599}
600
601/// Enclosure (attached media file)
602#[derive(Debug, Clone)]
603pub struct Enclosure {
604    /// Enclosure URL
605    pub url: Url,
606    /// File size in bytes
607    pub length: Option<u64>,
608    /// MIME type
609    pub enclosure_type: Option<MimeType>,
610}
611
612/// Content block
613#[derive(Debug, Clone)]
614pub struct Content {
615    /// Content body
616    pub value: String,
617    /// Content MIME type
618    pub content_type: Option<MimeType>,
619    /// Content language (stored inline for lang codes ≤24 bytes)
620    pub language: Option<SmallString>,
621    /// Base URL for relative links
622    pub base: Option<String>,
623}
624
625impl Content {
626    /// Create HTML content
627    #[inline]
628    pub fn html(value: impl Into<String>) -> Self {
629        Self {
630            value: value.into(),
631            content_type: Some(MimeType::new(MimeType::TEXT_HTML)),
632            language: None,
633            base: None,
634        }
635    }
636
637    /// Create plain text content
638    #[inline]
639    pub fn plain(value: impl Into<String>) -> Self {
640        Self {
641            value: value.into(),
642            content_type: Some(MimeType::new(MimeType::TEXT_PLAIN)),
643            language: None,
644            base: None,
645        }
646    }
647}
648
649/// Text construct type (Atom-style)
650#[derive(Debug, Clone, Copy, PartialEq, Eq)]
651pub enum TextType {
652    /// Plain text
653    Text,
654    /// HTML content
655    Html,
656    /// XHTML content
657    Xhtml,
658}
659
660/// Text construct with metadata
661#[derive(Debug, Clone)]
662pub struct TextConstruct {
663    /// Text content
664    pub value: String,
665    /// Content type
666    pub content_type: TextType,
667    /// Content language (stored inline for lang codes ≤24 bytes)
668    pub language: Option<SmallString>,
669    /// Base URL for relative links
670    pub base: Option<String>,
671}
672
673impl TextConstruct {
674    /// Create plain text construct
675    #[inline]
676    pub fn text(value: impl Into<String>) -> Self {
677        Self {
678            value: value.into(),
679            content_type: TextType::Text,
680            language: None,
681            base: None,
682        }
683    }
684
685    /// Create HTML text construct
686    #[inline]
687    pub fn html(value: impl Into<String>) -> Self {
688        Self {
689            value: value.into(),
690            content_type: TextType::Html,
691            language: None,
692            base: None,
693        }
694    }
695
696    /// Set language (builder pattern)
697    #[inline]
698    #[must_use]
699    pub fn with_language(mut self, language: impl AsRef<str>) -> Self {
700        self.language = Some(language.as_ref().into());
701        self
702    }
703}
704
705/// Generator metadata
706#[derive(Debug, Clone)]
707pub struct Generator {
708    /// Generator name
709    pub value: String,
710    /// Generator URI
711    pub uri: Option<String>,
712    /// Generator version (stored inline for versions ≤24 bytes)
713    pub version: Option<SmallString>,
714}
715
716/// Source reference (for entries)
717#[derive(Debug, Clone)]
718pub struct Source {
719    /// Source title
720    pub title: Option<String>,
721    /// Source link
722    pub link: Option<String>,
723    /// Source ID
724    pub id: Option<String>,
725}
726
727/// Media RSS thumbnail
728#[derive(Debug, Clone)]
729pub struct MediaThumbnail {
730    /// Thumbnail URL
731    ///
732    /// # Security Warning
733    ///
734    /// This URL comes from untrusted feed input and has NOT been validated for SSRF.
735    /// Applications MUST validate URLs before fetching to prevent SSRF attacks.
736    pub url: Url,
737    /// Thumbnail width in pixels
738    pub width: Option<u32>,
739    /// Thumbnail height in pixels
740    pub height: Option<u32>,
741}
742
743/// Media RSS content
744#[derive(Debug, Clone)]
745pub struct MediaContent {
746    /// Media URL
747    ///
748    /// # Security Warning
749    ///
750    /// This URL comes from untrusted feed input and has NOT been validated for SSRF.
751    /// Applications MUST validate URLs before fetching to prevent SSRF attacks.
752    pub url: Url,
753    /// MIME type
754    pub content_type: Option<MimeType>,
755    /// File size in bytes
756    pub filesize: Option<u64>,
757    /// Media width in pixels
758    pub width: Option<u32>,
759    /// Media height in pixels
760    pub height: Option<u32>,
761    /// Duration in seconds (for audio/video)
762    pub duration: Option<u64>,
763}
764
765impl FromAttributes for Link {
766    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
767    where
768        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
769    {
770        let mut href = None;
771        let mut rel = None;
772        let mut link_type = None;
773        let mut title = None;
774        let mut hreflang = None;
775        let mut length = None;
776        let mut thr_count = None;
777        let mut thr_updated = None;
778
779        for attr in attrs {
780            if attr.value.len() > max_attr_length {
781                continue;
782            }
783            match attr.key.as_ref() {
784                b"href" => href = Some(bytes_to_string(&attr.value)),
785                b"rel" => rel = Some(bytes_to_string(&attr.value)),
786                b"type" => link_type = Some(bytes_to_string(&attr.value)),
787                b"title" => title = Some(bytes_to_string(&attr.value)),
788                b"hreflang" => hreflang = Some(bytes_to_string(&attr.value)),
789                b"length" => length = bytes_to_string(&attr.value).parse().ok(),
790                b"thr:count" => {
791                    thr_count = bytes_to_string(&attr.value).trim().parse::<u32>().ok();
792                }
793                b"thr:updated" => {
794                    thr_updated = parse_date(bytes_to_string(&attr.value).trim());
795                }
796                _ => {}
797            }
798        }
799
800        href.map(|href| Self {
801            href: Url::new(href),
802            rel: rel
803                .map(std::convert::Into::into)
804                .or_else(|| Some("alternate".into())),
805            link_type: link_type.map(MimeType::new),
806            title,
807            length,
808            hreflang: hreflang.map(std::convert::Into::into),
809            thr_count,
810            thr_updated,
811        })
812    }
813}
814
815impl FromAttributes for Tag {
816    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
817    where
818        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
819    {
820        let mut term = None;
821        let mut scheme = None;
822        let mut label = None;
823
824        for attr in attrs {
825            if attr.value.len() > max_attr_length {
826                continue;
827            }
828
829            match attr.key.as_ref() {
830                b"term" => term = Some(bytes_to_string(&attr.value)),
831                b"scheme" | b"domain" => scheme = Some(bytes_to_string(&attr.value)),
832                b"label" => label = Some(bytes_to_string(&attr.value)),
833                _ => {}
834            }
835        }
836
837        term.map(|term| Self {
838            term: term.into(),
839            scheme: scheme.map(std::convert::Into::into),
840            label: label.map(std::convert::Into::into),
841        })
842    }
843}
844
845impl FromAttributes for Enclosure {
846    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
847    where
848        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
849    {
850        let mut url = None;
851        let mut length = None;
852        let mut enclosure_type = None;
853
854        for attr in attrs {
855            if attr.value.len() > max_attr_length {
856                continue;
857            }
858
859            match attr.key.as_ref() {
860                b"url" => url = Some(bytes_to_string(&attr.value)),
861                b"length" => length = bytes_to_string(&attr.value).parse().ok(),
862                b"type" => enclosure_type = Some(bytes_to_string(&attr.value)),
863                _ => {}
864            }
865        }
866
867        url.map(|url| Self {
868            url: Url::new(url),
869            length,
870            enclosure_type: enclosure_type.map(MimeType::new),
871        })
872    }
873}
874
875impl FromAttributes for MediaThumbnail {
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 url = None;
881        let mut width = None;
882        let mut height = None;
883
884        for attr in attrs {
885            if attr.value.len() > max_attr_length {
886                continue;
887            }
888
889            match attr.key.as_ref() {
890                b"url" => url = Some(bytes_to_string(&attr.value)),
891                b"width" => width = bytes_to_string(&attr.value).parse().ok(),
892                b"height" => height = bytes_to_string(&attr.value).parse().ok(),
893                _ => {}
894            }
895        }
896
897        url.map(|url| Self {
898            url: Url::new(url),
899            width,
900            height,
901        })
902    }
903}
904
905impl FromAttributes for MediaContent {
906    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
907    where
908        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
909    {
910        let mut url = None;
911        let mut content_type = None;
912        let mut filesize = None;
913        let mut width = None;
914        let mut height = None;
915        let mut duration = None;
916
917        for attr in attrs {
918            if attr.value.len() > max_attr_length {
919                continue;
920            }
921
922            match attr.key.as_ref() {
923                b"url" => url = Some(bytes_to_string(&attr.value)),
924                b"type" => content_type = Some(bytes_to_string(&attr.value)),
925                b"fileSize" => filesize = bytes_to_string(&attr.value).parse().ok(),
926                b"width" => width = bytes_to_string(&attr.value).parse().ok(),
927                b"height" => height = bytes_to_string(&attr.value).parse().ok(),
928                b"duration" => duration = bytes_to_string(&attr.value).parse().ok(),
929                _ => {}
930            }
931        }
932
933        url.map(|url| Self {
934            url: Url::new(url),
935            content_type: content_type.map(MimeType::new),
936            filesize,
937            width,
938            height,
939            duration,
940        })
941    }
942}
943
944// ParseFrom implementations for JSON Feed parsing
945
946impl ParseFrom<&Value> for Person {
947    /// Parse Person from JSON Feed author object
948    ///
949    /// JSON Feed format: `{"name": "...", "url": "...", "avatar": "..."}`
950    fn parse_from(json: &Value) -> Option<Self> {
951        json.as_object().map(|obj| Self {
952            name: obj
953                .get("name")
954                .and_then(Value::as_str)
955                .map(std::convert::Into::into),
956            email: None, // JSON Feed doesn't have email field
957            uri: obj.get("url").and_then(Value::as_str).map(String::from),
958        })
959    }
960}
961
962impl ParseFrom<&Value> for Enclosure {
963    /// Parse Enclosure from JSON Feed attachment object
964    ///
965    /// JSON Feed format: `{"url": "...", "mime_type": "...", "size_in_bytes": ...}`
966    fn parse_from(json: &Value) -> Option<Self> {
967        let obj = json.as_object()?;
968        let url = obj.get("url").and_then(Value::as_str)?;
969        Some(Self {
970            url: Url::new(url),
971            length: obj.get("size_in_bytes").and_then(Value::as_u64),
972            enclosure_type: obj
973                .get("mime_type")
974                .and_then(Value::as_str)
975                .map(MimeType::new),
976        })
977    }
978}
979
980#[cfg(test)]
981mod tests {
982    use super::*;
983    use serde_json::json;
984
985    #[test]
986    fn test_link_default() {
987        let link = Link::default();
988        assert!(link.href.is_empty());
989        assert!(link.rel.is_none());
990    }
991
992    #[test]
993    fn test_link_builders() {
994        let link = Link::alternate("https://example.com");
995        assert_eq!(link.href, "https://example.com");
996        assert_eq!(link.rel.as_deref(), Some("alternate"));
997
998        let link = Link::self_link("https://example.com/feed", "application/feed+json");
999        assert_eq!(link.rel.as_deref(), Some("self"));
1000        assert_eq!(link.link_type.as_deref(), Some("application/feed+json"));
1001
1002        let link = Link::enclosure("https://example.com/audio.mp3", Some("audio/mpeg".into()));
1003        assert_eq!(link.rel.as_deref(), Some("enclosure"));
1004        assert_eq!(link.link_type.as_deref(), Some("audio/mpeg"));
1005
1006        let link = Link::related("https://other.com");
1007        assert_eq!(link.rel.as_deref(), Some("related"));
1008    }
1009
1010    #[test]
1011    fn test_tag_builder() {
1012        let tag = Tag::new("rust");
1013        assert_eq!(tag.term, "rust");
1014        assert!(tag.scheme.is_none());
1015    }
1016
1017    #[test]
1018    fn test_text_construct_builders() {
1019        let text = TextConstruct::text("Hello");
1020        assert_eq!(text.value, "Hello");
1021        assert_eq!(text.content_type, TextType::Text);
1022
1023        let html = TextConstruct::html("<p>Hello</p>");
1024        assert_eq!(html.content_type, TextType::Html);
1025
1026        let with_lang = TextConstruct::text("Hello").with_language("en");
1027        assert_eq!(with_lang.language.as_deref(), Some("en"));
1028    }
1029
1030    #[test]
1031    fn test_content_builders() {
1032        let html = Content::html("<p>Content</p>");
1033        assert_eq!(html.content_type.as_deref(), Some("text/html"));
1034
1035        let plain = Content::plain("Content");
1036        assert_eq!(plain.content_type.as_deref(), Some("text/plain"));
1037    }
1038
1039    #[test]
1040    fn test_person_default() {
1041        let person = Person::default();
1042        assert!(person.name.is_none());
1043        assert!(person.email.is_none());
1044        assert!(person.uri.is_none());
1045    }
1046
1047    #[test]
1048    fn test_person_parse_from_json() {
1049        let json = json!({"name": "John Doe", "url": "https://example.com"});
1050        let person = Person::parse_from(&json).unwrap();
1051        assert_eq!(person.name.as_deref(), Some("John Doe"));
1052        assert_eq!(person.uri.as_deref(), Some("https://example.com"));
1053        assert!(person.email.is_none());
1054    }
1055
1056    #[test]
1057    fn test_person_parse_from_empty_json() {
1058        let json = json!({});
1059        let person = Person::parse_from(&json).unwrap();
1060        assert!(person.name.is_none());
1061    }
1062
1063    #[test]
1064    fn test_enclosure_parse_from_json() {
1065        let json = json!({
1066            "url": "https://example.com/file.mp3",
1067            "mime_type": "audio/mpeg",
1068            "size_in_bytes": 12345
1069        });
1070        let enclosure = Enclosure::parse_from(&json).unwrap();
1071        assert_eq!(enclosure.url, "https://example.com/file.mp3");
1072        assert_eq!(enclosure.enclosure_type.as_deref(), Some("audio/mpeg"));
1073        assert_eq!(enclosure.length, Some(12345));
1074    }
1075
1076    #[test]
1077    fn test_enclosure_parse_from_json_missing_url() {
1078        let json = json!({"mime_type": "audio/mpeg"});
1079        assert!(Enclosure::parse_from(&json).is_none());
1080    }
1081
1082    #[test]
1083    fn test_text_type_equality() {
1084        assert_eq!(TextType::Text, TextType::Text);
1085        assert_ne!(TextType::Text, TextType::Html);
1086    }
1087
1088    // Newtype tests
1089
1090    #[test]
1091    fn test_url_new() {
1092        let url = Url::new("https://example.com");
1093        assert_eq!(url.as_str(), "https://example.com");
1094    }
1095
1096    #[test]
1097    fn test_url_from_string() {
1098        let url: Url = String::from("https://example.com").into();
1099        assert_eq!(url.as_str(), "https://example.com");
1100    }
1101
1102    #[test]
1103    fn test_url_from_str() {
1104        let url: Url = "https://example.com".into();
1105        assert_eq!(url.as_str(), "https://example.com");
1106    }
1107
1108    #[test]
1109    fn test_url_deref() {
1110        let url = Url::new("https://example.com");
1111        // Deref allows calling str methods directly
1112        assert_eq!(url.len(), 19);
1113        assert!(url.starts_with("https://"));
1114    }
1115
1116    #[test]
1117    fn test_url_into_inner() {
1118        let url = Url::new("https://example.com");
1119        let inner = url.into_inner();
1120        assert_eq!(inner, "https://example.com");
1121    }
1122
1123    #[test]
1124    fn test_url_default() {
1125        let url = Url::default();
1126        assert_eq!(url.as_str(), "");
1127    }
1128
1129    #[test]
1130    fn test_url_clone() {
1131        let url1 = Url::new("https://example.com");
1132        let url2 = url1.clone();
1133        assert_eq!(url1, url2);
1134    }
1135
1136    #[test]
1137    fn test_mime_type_new() {
1138        let mime = MimeType::new("text/html");
1139        assert_eq!(mime.as_str(), "text/html");
1140    }
1141
1142    #[test]
1143    fn test_mime_type_from_string() {
1144        let mime: MimeType = String::from("application/json").into();
1145        assert_eq!(mime.as_str(), "application/json");
1146    }
1147
1148    #[test]
1149    fn test_mime_type_from_str() {
1150        let mime: MimeType = "text/plain".into();
1151        assert_eq!(mime.as_str(), "text/plain");
1152    }
1153
1154    #[test]
1155    fn test_mime_type_deref() {
1156        let mime = MimeType::new("text/html");
1157        assert_eq!(mime.len(), 9);
1158        assert!(mime.starts_with("text/"));
1159    }
1160
1161    #[test]
1162    fn test_mime_type_default() {
1163        let mime = MimeType::default();
1164        assert_eq!(mime.as_str(), "");
1165    }
1166
1167    #[test]
1168    fn test_mime_type_clone() {
1169        let mime1 = MimeType::new("application/xml");
1170        let mime2 = mime1.clone();
1171        assert_eq!(mime1, mime2);
1172        // Arc cloning is cheap - just increments refcount
1173    }
1174
1175    #[test]
1176    fn test_mime_type_constants() {
1177        assert_eq!(MimeType::TEXT_HTML, "text/html");
1178        assert_eq!(MimeType::TEXT_PLAIN, "text/plain");
1179        assert_eq!(MimeType::APPLICATION_XML, "application/xml");
1180        assert_eq!(MimeType::APPLICATION_JSON, "application/json");
1181    }
1182
1183    #[test]
1184    fn test_email_new() {
1185        let email = Email::new("user@example.com");
1186        assert_eq!(email.as_str(), "user@example.com");
1187    }
1188
1189    #[test]
1190    fn test_email_from_string() {
1191        let email: Email = String::from("user@example.com").into();
1192        assert_eq!(email.as_str(), "user@example.com");
1193    }
1194
1195    #[test]
1196    fn test_email_from_str() {
1197        let email: Email = "user@example.com".into();
1198        assert_eq!(email.as_str(), "user@example.com");
1199    }
1200
1201    #[test]
1202    fn test_email_deref() {
1203        let email = Email::new("user@example.com");
1204        assert_eq!(email.len(), 16);
1205        assert!(email.contains('@'));
1206    }
1207
1208    #[test]
1209    fn test_email_into_inner() {
1210        let email = Email::new("user@example.com");
1211        let inner = email.into_inner();
1212        assert_eq!(inner, "user@example.com");
1213    }
1214
1215    #[test]
1216    fn test_email_default() {
1217        let email = Email::default();
1218        assert_eq!(email.as_str(), "");
1219    }
1220
1221    #[test]
1222    fn test_email_clone() {
1223        let email1 = Email::new("user@example.com");
1224        let email2 = email1.clone();
1225        assert_eq!(email1, email2);
1226    }
1227}