feedparser_rs/namespace/
syndication.rs

1/// Syndication Module for RSS 1.0
2///
3/// Namespace: <http://purl.org/rss/1.0/modules/syndication/>
4/// Prefix: syn
5///
6/// This module provides parsing support for the Syndication namespace,
7/// used in RSS 1.0 feeds to indicate update schedules and frequencies.
8///
9/// Elements:
10/// - `syn:updatePeriod` → Update period (hourly, daily, weekly, monthly, yearly)
11/// - `syn:updateFrequency` → Number of times per period
12/// - `syn:updateBase` → Base date for update schedule (ISO 8601)
13use crate::types::FeedMeta;
14
15/// Syndication namespace URI
16pub const SYNDICATION_NAMESPACE: &str = "http://purl.org/rss/1.0/modules/syndication/";
17
18/// Valid update period values
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum UpdatePeriod {
21    /// Update hourly
22    Hourly,
23    /// Update daily
24    Daily,
25    /// Update weekly
26    Weekly,
27    /// Update monthly
28    Monthly,
29    /// Update yearly
30    Yearly,
31}
32
33impl UpdatePeriod {
34    /// Parse update period from string (case-insensitive)
35    ///
36    /// Returns `None` if the string doesn't match any valid period.
37    #[must_use]
38    pub fn parse(s: &str) -> Option<Self> {
39        match s.to_lowercase().as_str() {
40            "hourly" => Some(Self::Hourly),
41            "daily" => Some(Self::Daily),
42            "weekly" => Some(Self::Weekly),
43            "monthly" => Some(Self::Monthly),
44            "yearly" => Some(Self::Yearly),
45            _ => None,
46        }
47    }
48
49    /// Convert to string representation
50    #[must_use]
51    pub const fn as_str(&self) -> &'static str {
52        match self {
53            Self::Hourly => "hourly",
54            Self::Daily => "daily",
55            Self::Weekly => "weekly",
56            Self::Monthly => "monthly",
57            Self::Yearly => "yearly",
58        }
59    }
60}
61
62/// Syndication metadata
63#[derive(Debug, Clone, Default)]
64pub struct SyndicationMeta {
65    /// Update period (hourly, daily, weekly, monthly, yearly)
66    pub update_period: Option<UpdatePeriod>,
67    /// Number of times updated per period
68    pub update_frequency: Option<u32>,
69    /// Base date for update schedule (ISO 8601)
70    pub update_base: Option<String>,
71}
72
73/// Handle Syndication namespace element at feed level
74///
75/// # Arguments
76///
77/// * `element` - Local name of the element (without namespace prefix)
78/// * `text` - Text content of the element
79/// * `feed` - Feed metadata to update
80pub fn handle_feed_element(element: &str, text: &str, feed: &mut FeedMeta) {
81    match element {
82        "updatePeriod" => {
83            if let Some(period) = UpdatePeriod::parse(text) {
84                if feed.syndication.is_none() {
85                    feed.syndication = Some(Box::new(SyndicationMeta::default()));
86                }
87                if let Some(syn) = &mut feed.syndication {
88                    syn.update_period = Some(period);
89                }
90            }
91        }
92        "updateFrequency" => {
93            if let Ok(freq) = text.parse::<u32>() {
94                if feed.syndication.is_none() {
95                    feed.syndication = Some(Box::new(SyndicationMeta::default()));
96                }
97                if let Some(syn) = &mut feed.syndication {
98                    syn.update_frequency = Some(freq);
99                }
100            }
101        }
102        "updateBase" => {
103            if feed.syndication.is_none() {
104                feed.syndication = Some(Box::new(SyndicationMeta::default()));
105            }
106            if let Some(syn) = &mut feed.syndication {
107                syn.update_base = Some(text.to_string());
108            }
109        }
110        _ => {
111            // Ignore unknown syndication elements
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_update_period_parse() {
122        assert_eq!(UpdatePeriod::parse("hourly"), Some(UpdatePeriod::Hourly));
123        assert_eq!(UpdatePeriod::parse("daily"), Some(UpdatePeriod::Daily));
124        assert_eq!(UpdatePeriod::parse("weekly"), Some(UpdatePeriod::Weekly));
125        assert_eq!(UpdatePeriod::parse("monthly"), Some(UpdatePeriod::Monthly));
126        assert_eq!(UpdatePeriod::parse("yearly"), Some(UpdatePeriod::Yearly));
127        assert_eq!(UpdatePeriod::parse("invalid"), None);
128    }
129
130    #[test]
131    fn test_update_period_case_insensitive() {
132        assert_eq!(UpdatePeriod::parse("HOURLY"), Some(UpdatePeriod::Hourly));
133        assert_eq!(UpdatePeriod::parse("Daily"), Some(UpdatePeriod::Daily));
134        assert_eq!(UpdatePeriod::parse("WeeKLY"), Some(UpdatePeriod::Weekly));
135    }
136
137    #[test]
138    fn test_update_period_as_str() {
139        assert_eq!(UpdatePeriod::Hourly.as_str(), "hourly");
140        assert_eq!(UpdatePeriod::Daily.as_str(), "daily");
141        assert_eq!(UpdatePeriod::Weekly.as_str(), "weekly");
142        assert_eq!(UpdatePeriod::Monthly.as_str(), "monthly");
143        assert_eq!(UpdatePeriod::Yearly.as_str(), "yearly");
144    }
145
146    #[test]
147    fn test_handle_update_period() {
148        let mut feed = FeedMeta::default();
149
150        handle_feed_element("updatePeriod", "daily", &mut feed);
151
152        assert!(feed.syndication.is_some());
153        let syn = feed.syndication.as_ref().unwrap();
154        assert_eq!(syn.update_period, Some(UpdatePeriod::Daily));
155    }
156
157    #[test]
158    fn test_handle_update_frequency() {
159        let mut feed = FeedMeta::default();
160
161        handle_feed_element("updateFrequency", "2", &mut feed);
162
163        assert!(feed.syndication.is_some());
164        let syn = feed.syndication.as_ref().unwrap();
165        assert_eq!(syn.update_frequency, Some(2));
166    }
167
168    #[test]
169    fn test_handle_update_base() {
170        let mut feed = FeedMeta::default();
171
172        handle_feed_element("updateBase", "2024-12-18T00:00:00Z", &mut feed);
173
174        assert!(feed.syndication.is_some());
175        let syn = feed.syndication.as_ref().unwrap();
176        assert_eq!(syn.update_base.as_deref(), Some("2024-12-18T00:00:00Z"));
177    }
178
179    #[test]
180    fn test_handle_multiple_elements() {
181        let mut feed = FeedMeta::default();
182
183        handle_feed_element("updatePeriod", "hourly", &mut feed);
184        handle_feed_element("updateFrequency", "1", &mut feed);
185        handle_feed_element("updateBase", "2024-01-01T00:00:00Z", &mut feed);
186
187        let syn = feed.syndication.as_ref().unwrap();
188        assert_eq!(syn.update_period, Some(UpdatePeriod::Hourly));
189        assert_eq!(syn.update_frequency, Some(1));
190        assert_eq!(syn.update_base.as_deref(), Some("2024-01-01T00:00:00Z"));
191    }
192
193    #[test]
194    fn test_handle_invalid_frequency() {
195        let mut feed = FeedMeta::default();
196
197        handle_feed_element("updateFrequency", "not-a-number", &mut feed);
198
199        // Should not create syndication metadata for invalid input
200        assert!(feed.syndication.is_none());
201    }
202
203    #[test]
204    fn test_handle_unknown_element() {
205        let mut feed = FeedMeta::default();
206
207        handle_feed_element("unknown", "value", &mut feed);
208
209        assert!(feed.syndication.is_none());
210    }
211}