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 (raw string value, as in Python feedparser)
441    pub length: Option<String>,
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 (raw string value, as in Python feedparser)
607    pub length: Option<String>,
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 (text content of the `<generator>` element)
709    pub name: String,
710    /// Generator URI (href attribute)
711    pub href: 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 (raw string value, as in Python feedparser)
738    pub width: Option<String>,
739    /// Thumbnail height in pixels (raw string value, as in Python feedparser)
740    pub height: Option<String>,
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    /// Medium type: "image", "video", "audio", "document", "executable"
756    pub medium: Option<String>,
757    /// File size in bytes
758    pub filesize: Option<u64>,
759    /// Media width in pixels (raw string value, as in Python feedparser)
760    pub width: Option<String>,
761    /// Media height in pixels (raw string value, as in Python feedparser)
762    pub height: Option<String>,
763    /// Duration in seconds (raw string value, as in Python feedparser)
764    pub duration: Option<String>,
765    /// Bitrate in kilobits per second (raw string value)
766    pub bitrate: Option<String>,
767    /// Language of the media (lang attribute)
768    pub lang: Option<String>,
769    /// Number of audio channels (raw string value)
770    pub channels: Option<String>,
771    /// Codec used to produce the media (codec attribute)
772    pub codec: Option<String>,
773    /// Expression type: "full", "sample", "nonstop"
774    pub expression: Option<String>,
775    /// Whether this is the default media object (isDefault attribute, raw string)
776    pub isdefault: Option<String>,
777    /// Sampling rate in kHz (raw string value)
778    pub samplingrate: Option<String>,
779}
780
781impl FromAttributes for Link {
782    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
783    where
784        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
785    {
786        let mut href = None;
787        let mut rel = None;
788        let mut link_type = None;
789        let mut title = None;
790        let mut hreflang = None;
791        let mut length = None;
792        let mut thr_count = None;
793        let mut thr_updated = None;
794
795        for attr in attrs {
796            if attr.value.len() > max_attr_length {
797                continue;
798            }
799            match attr.key.as_ref() {
800                b"href" => href = Some(bytes_to_string(&attr.value)),
801                b"rel" => rel = Some(bytes_to_string(&attr.value)),
802                b"type" => link_type = Some(bytes_to_string(&attr.value)),
803                b"title" => title = Some(bytes_to_string(&attr.value)),
804                b"hreflang" => hreflang = Some(bytes_to_string(&attr.value)),
805                b"length" => length = Some(bytes_to_string(&attr.value)),
806                b"thr:count" => {
807                    thr_count = bytes_to_string(&attr.value).trim().parse::<u32>().ok();
808                }
809                b"thr:updated" => {
810                    thr_updated = parse_date(bytes_to_string(&attr.value).trim());
811                }
812                _ => {}
813            }
814        }
815
816        let rel_str: Option<SmallString> = rel
817            .map(std::convert::Into::into)
818            .or_else(|| Some("alternate".into()));
819        let resolved_type = link_type
820            .map(MimeType::new)
821            .or_else(|| match rel_str.as_deref() {
822                Some("self") => Some(MimeType::new("application/atom+xml")),
823                _ => Some(MimeType::new("text/html")),
824            });
825
826        href.map(|href| Self {
827            href: Url::new(href),
828            rel: rel_str,
829            link_type: resolved_type,
830            title,
831            length,
832            hreflang: hreflang.map(std::convert::Into::into),
833            thr_count,
834            thr_updated,
835        })
836    }
837}
838
839impl FromAttributes for Tag {
840    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
841    where
842        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
843    {
844        let mut term = None;
845        let mut scheme = None;
846        let mut label = None;
847
848        for attr in attrs {
849            if attr.value.len() > max_attr_length {
850                continue;
851            }
852
853            match attr.key.as_ref() {
854                b"term" => term = Some(bytes_to_string(&attr.value)),
855                b"scheme" | b"domain" => scheme = Some(bytes_to_string(&attr.value)),
856                b"label" => label = Some(bytes_to_string(&attr.value)),
857                _ => {}
858            }
859        }
860
861        term.map(|term| Self {
862            term: term.into(),
863            scheme: scheme.map(std::convert::Into::into),
864            label: label.map(std::convert::Into::into),
865        })
866    }
867}
868
869impl FromAttributes for Enclosure {
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 url = None;
875        let mut length = None;
876        let mut enclosure_type = None;
877
878        for attr in attrs {
879            if attr.value.len() > max_attr_length {
880                continue;
881            }
882
883            match attr.key.as_ref() {
884                b"url" => url = Some(bytes_to_string(&attr.value)),
885                b"length" => length = Some(bytes_to_string(&attr.value)),
886                b"type" => enclosure_type = Some(bytes_to_string(&attr.value)),
887                _ => {}
888            }
889        }
890
891        url.map(|url| Self {
892            url: Url::new(url),
893            length,
894            enclosure_type: enclosure_type.map(MimeType::new),
895        })
896    }
897}
898
899impl FromAttributes for MediaThumbnail {
900    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
901    where
902        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
903    {
904        let mut url = None;
905        let mut width = None;
906        let mut height = None;
907
908        for attr in attrs {
909            if attr.value.len() > max_attr_length {
910                continue;
911            }
912
913            match attr.key.as_ref() {
914                b"url" => url = Some(bytes_to_string(&attr.value)),
915                b"width" => width = Some(bytes_to_string(&attr.value)),
916                b"height" => height = Some(bytes_to_string(&attr.value)),
917                _ => {}
918            }
919        }
920
921        url.map(|url| Self {
922            url: Url::new(url),
923            width,
924            height,
925        })
926    }
927}
928
929impl FromAttributes for MediaContent {
930    fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
931    where
932        I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
933    {
934        let mut url = None;
935        let mut content_type = None;
936        let mut medium = None;
937        let mut filesize = None;
938        let mut width = None;
939        let mut height = None;
940        let mut duration = None;
941        let mut bitrate = None;
942        let mut lang = None;
943        let mut channels = None;
944        let mut codec = None;
945        let mut expression = None;
946        let mut isdefault = None;
947        let mut samplingrate = None;
948
949        for attr in attrs {
950            if attr.value.len() > max_attr_length {
951                continue;
952            }
953
954            match attr.key.as_ref() {
955                b"url" => url = Some(bytes_to_string(&attr.value)),
956                b"type" => content_type = Some(bytes_to_string(&attr.value)),
957                b"medium" => medium = Some(bytes_to_string(&attr.value)),
958                b"fileSize" => filesize = bytes_to_string(&attr.value).parse().ok(),
959                b"width" => width = Some(bytes_to_string(&attr.value)),
960                b"height" => height = Some(bytes_to_string(&attr.value)),
961                b"duration" => duration = Some(bytes_to_string(&attr.value)),
962                b"bitrate" => bitrate = Some(bytes_to_string(&attr.value)),
963                b"lang" => lang = Some(bytes_to_string(&attr.value)),
964                b"channels" => channels = Some(bytes_to_string(&attr.value)),
965                b"codec" => codec = Some(bytes_to_string(&attr.value)),
966                b"expression" => expression = Some(bytes_to_string(&attr.value)),
967                b"isDefault" => isdefault = Some(bytes_to_string(&attr.value)),
968                b"samplingrate" => samplingrate = Some(bytes_to_string(&attr.value)),
969                _ => {}
970            }
971        }
972
973        url.map(|url| Self {
974            url: Url::new(url),
975            content_type: content_type.map(MimeType::new),
976            medium,
977            filesize,
978            width,
979            height,
980            duration,
981            bitrate,
982            lang,
983            channels,
984            codec,
985            expression,
986            isdefault,
987            samplingrate,
988        })
989    }
990}
991
992// ParseFrom implementations for JSON Feed parsing
993
994impl ParseFrom<&Value> for Person {
995    /// Parse Person from JSON Feed author object
996    ///
997    /// JSON Feed format: `{"name": "...", "url": "...", "avatar": "..."}`
998    fn parse_from(json: &Value) -> Option<Self> {
999        json.as_object().map(|obj| Self {
1000            name: obj
1001                .get("name")
1002                .and_then(Value::as_str)
1003                .map(std::convert::Into::into),
1004            email: None, // JSON Feed doesn't have email field
1005            uri: obj.get("url").and_then(Value::as_str).map(String::from),
1006        })
1007    }
1008}
1009
1010impl ParseFrom<&Value> for Enclosure {
1011    /// Parse Enclosure from JSON Feed attachment object
1012    ///
1013    /// JSON Feed format: `{"url": "...", "mime_type": "...", "size_in_bytes": ...}`
1014    fn parse_from(json: &Value) -> Option<Self> {
1015        let obj = json.as_object()?;
1016        let url = obj.get("url").and_then(Value::as_str)?;
1017        Some(Self {
1018            url: Url::new(url),
1019            length: obj
1020                .get("size_in_bytes")
1021                .and_then(Value::as_u64)
1022                .map(|v| v.to_string()),
1023            enclosure_type: obj
1024                .get("mime_type")
1025                .and_then(Value::as_str)
1026                .map(MimeType::new),
1027        })
1028    }
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033    use super::*;
1034    use serde_json::json;
1035
1036    #[test]
1037    fn test_link_default() {
1038        let link = Link::default();
1039        assert!(link.href.is_empty());
1040        assert!(link.rel.is_none());
1041    }
1042
1043    #[test]
1044    fn test_link_builders() {
1045        let link = Link::alternate("https://example.com");
1046        assert_eq!(link.href, "https://example.com");
1047        assert_eq!(link.rel.as_deref(), Some("alternate"));
1048
1049        let link = Link::self_link("https://example.com/feed", "application/feed+json");
1050        assert_eq!(link.rel.as_deref(), Some("self"));
1051        assert_eq!(link.link_type.as_deref(), Some("application/feed+json"));
1052
1053        let link = Link::enclosure("https://example.com/audio.mp3", Some("audio/mpeg".into()));
1054        assert_eq!(link.rel.as_deref(), Some("enclosure"));
1055        assert_eq!(link.link_type.as_deref(), Some("audio/mpeg"));
1056
1057        let link = Link::related("https://other.com");
1058        assert_eq!(link.rel.as_deref(), Some("related"));
1059    }
1060
1061    #[test]
1062    fn test_tag_builder() {
1063        let tag = Tag::new("rust");
1064        assert_eq!(tag.term, "rust");
1065        assert!(tag.scheme.is_none());
1066    }
1067
1068    #[test]
1069    fn test_text_construct_builders() {
1070        let text = TextConstruct::text("Hello");
1071        assert_eq!(text.value, "Hello");
1072        assert_eq!(text.content_type, TextType::Text);
1073
1074        let html = TextConstruct::html("<p>Hello</p>");
1075        assert_eq!(html.content_type, TextType::Html);
1076
1077        let with_lang = TextConstruct::text("Hello").with_language("en");
1078        assert_eq!(with_lang.language.as_deref(), Some("en"));
1079    }
1080
1081    #[test]
1082    fn test_content_builders() {
1083        let html = Content::html("<p>Content</p>");
1084        assert_eq!(html.content_type.as_deref(), Some("text/html"));
1085
1086        let plain = Content::plain("Content");
1087        assert_eq!(plain.content_type.as_deref(), Some("text/plain"));
1088    }
1089
1090    #[test]
1091    fn test_person_default() {
1092        let person = Person::default();
1093        assert!(person.name.is_none());
1094        assert!(person.email.is_none());
1095        assert!(person.uri.is_none());
1096    }
1097
1098    #[test]
1099    fn test_person_parse_from_json() {
1100        let json = json!({"name": "John Doe", "url": "https://example.com"});
1101        let person = Person::parse_from(&json).unwrap();
1102        assert_eq!(person.name.as_deref(), Some("John Doe"));
1103        assert_eq!(person.uri.as_deref(), Some("https://example.com"));
1104        assert!(person.email.is_none());
1105    }
1106
1107    #[test]
1108    fn test_person_parse_from_empty_json() {
1109        let json = json!({});
1110        let person = Person::parse_from(&json).unwrap();
1111        assert!(person.name.is_none());
1112    }
1113
1114    #[test]
1115    fn test_enclosure_parse_from_json() {
1116        let json = json!({
1117            "url": "https://example.com/file.mp3",
1118            "mime_type": "audio/mpeg",
1119            "size_in_bytes": 12345
1120        });
1121        let enclosure = Enclosure::parse_from(&json).unwrap();
1122        assert_eq!(enclosure.url, "https://example.com/file.mp3");
1123        assert_eq!(enclosure.enclosure_type.as_deref(), Some("audio/mpeg"));
1124        assert_eq!(enclosure.length.as_deref(), Some("12345"));
1125    }
1126
1127    #[test]
1128    fn test_enclosure_parse_from_json_missing_url() {
1129        let json = json!({"mime_type": "audio/mpeg"});
1130        assert!(Enclosure::parse_from(&json).is_none());
1131    }
1132
1133    #[test]
1134    fn test_text_type_equality() {
1135        assert_eq!(TextType::Text, TextType::Text);
1136        assert_ne!(TextType::Text, TextType::Html);
1137    }
1138
1139    // Newtype tests
1140
1141    #[test]
1142    fn test_url_new() {
1143        let url = Url::new("https://example.com");
1144        assert_eq!(url.as_str(), "https://example.com");
1145    }
1146
1147    #[test]
1148    fn test_url_from_string() {
1149        let url: Url = String::from("https://example.com").into();
1150        assert_eq!(url.as_str(), "https://example.com");
1151    }
1152
1153    #[test]
1154    fn test_url_from_str() {
1155        let url: Url = "https://example.com".into();
1156        assert_eq!(url.as_str(), "https://example.com");
1157    }
1158
1159    #[test]
1160    fn test_url_deref() {
1161        let url = Url::new("https://example.com");
1162        // Deref allows calling str methods directly
1163        assert_eq!(url.len(), 19);
1164        assert!(url.starts_with("https://"));
1165    }
1166
1167    #[test]
1168    fn test_url_into_inner() {
1169        let url = Url::new("https://example.com");
1170        let inner = url.into_inner();
1171        assert_eq!(inner, "https://example.com");
1172    }
1173
1174    #[test]
1175    fn test_url_default() {
1176        let url = Url::default();
1177        assert_eq!(url.as_str(), "");
1178    }
1179
1180    #[test]
1181    fn test_url_clone() {
1182        let url1 = Url::new("https://example.com");
1183        let url2 = url1.clone();
1184        assert_eq!(url1, url2);
1185    }
1186
1187    #[test]
1188    fn test_mime_type_new() {
1189        let mime = MimeType::new("text/html");
1190        assert_eq!(mime.as_str(), "text/html");
1191    }
1192
1193    #[test]
1194    fn test_mime_type_from_string() {
1195        let mime: MimeType = String::from("application/json").into();
1196        assert_eq!(mime.as_str(), "application/json");
1197    }
1198
1199    #[test]
1200    fn test_mime_type_from_str() {
1201        let mime: MimeType = "text/plain".into();
1202        assert_eq!(mime.as_str(), "text/plain");
1203    }
1204
1205    #[test]
1206    fn test_mime_type_deref() {
1207        let mime = MimeType::new("text/html");
1208        assert_eq!(mime.len(), 9);
1209        assert!(mime.starts_with("text/"));
1210    }
1211
1212    #[test]
1213    fn test_mime_type_default() {
1214        let mime = MimeType::default();
1215        assert_eq!(mime.as_str(), "");
1216    }
1217
1218    #[test]
1219    fn test_mime_type_clone() {
1220        let mime1 = MimeType::new("application/xml");
1221        let mime2 = mime1.clone();
1222        assert_eq!(mime1, mime2);
1223        // Arc cloning is cheap - just increments refcount
1224    }
1225
1226    #[test]
1227    fn test_mime_type_constants() {
1228        assert_eq!(MimeType::TEXT_HTML, "text/html");
1229        assert_eq!(MimeType::TEXT_PLAIN, "text/plain");
1230        assert_eq!(MimeType::APPLICATION_XML, "application/xml");
1231        assert_eq!(MimeType::APPLICATION_JSON, "application/json");
1232    }
1233
1234    #[test]
1235    fn test_email_new() {
1236        let email = Email::new("user@example.com");
1237        assert_eq!(email.as_str(), "user@example.com");
1238    }
1239
1240    #[test]
1241    fn test_email_from_string() {
1242        let email: Email = String::from("user@example.com").into();
1243        assert_eq!(email.as_str(), "user@example.com");
1244    }
1245
1246    #[test]
1247    fn test_email_from_str() {
1248        let email: Email = "user@example.com".into();
1249        assert_eq!(email.as_str(), "user@example.com");
1250    }
1251
1252    #[test]
1253    fn test_email_deref() {
1254        let email = Email::new("user@example.com");
1255        assert_eq!(email.len(), 16);
1256        assert!(email.contains('@'));
1257    }
1258
1259    #[test]
1260    fn test_email_into_inner() {
1261        let email = Email::new("user@example.com");
1262        let inner = email.into_inner();
1263        assert_eq!(inner, "user@example.com");
1264    }
1265
1266    #[test]
1267    fn test_email_default() {
1268        let email = Email::default();
1269        assert_eq!(email.as_str(), "");
1270    }
1271
1272    #[test]
1273    fn test_email_clone() {
1274        let email1 = Email::new("user@example.com");
1275        let email2 = email1.clone();
1276        assert_eq!(email1, email2);
1277    }
1278}