Skip to main content

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<String>,
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" if !text.is_empty() => {
93            if feed.syndication.is_none() {
94                feed.syndication = Some(Box::new(SyndicationMeta::default()));
95            }
96            if let Some(syn) = &mut feed.syndication {
97                syn.update_frequency = Some(text.to_string());
98            }
99        }
100        "updateBase" => {
101            if feed.syndication.is_none() {
102                feed.syndication = Some(Box::new(SyndicationMeta::default()));
103            }
104            if let Some(syn) = &mut feed.syndication {
105                syn.update_base = Some(text.to_string());
106            }
107        }
108        _ => {
109            // Ignore unknown syndication elements
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_update_period_parse() {
120        assert_eq!(UpdatePeriod::parse("hourly"), Some(UpdatePeriod::Hourly));
121        assert_eq!(UpdatePeriod::parse("daily"), Some(UpdatePeriod::Daily));
122        assert_eq!(UpdatePeriod::parse("weekly"), Some(UpdatePeriod::Weekly));
123        assert_eq!(UpdatePeriod::parse("monthly"), Some(UpdatePeriod::Monthly));
124        assert_eq!(UpdatePeriod::parse("yearly"), Some(UpdatePeriod::Yearly));
125        assert_eq!(UpdatePeriod::parse("invalid"), None);
126    }
127
128    #[test]
129    fn test_update_period_case_insensitive() {
130        assert_eq!(UpdatePeriod::parse("HOURLY"), Some(UpdatePeriod::Hourly));
131        assert_eq!(UpdatePeriod::parse("Daily"), Some(UpdatePeriod::Daily));
132        assert_eq!(UpdatePeriod::parse("WeeKLY"), Some(UpdatePeriod::Weekly));
133    }
134
135    #[test]
136    fn test_update_period_as_str() {
137        assert_eq!(UpdatePeriod::Hourly.as_str(), "hourly");
138        assert_eq!(UpdatePeriod::Daily.as_str(), "daily");
139        assert_eq!(UpdatePeriod::Weekly.as_str(), "weekly");
140        assert_eq!(UpdatePeriod::Monthly.as_str(), "monthly");
141        assert_eq!(UpdatePeriod::Yearly.as_str(), "yearly");
142    }
143
144    #[test]
145    fn test_handle_update_period() {
146        let mut feed = FeedMeta::default();
147
148        handle_feed_element("updatePeriod", "daily", &mut feed);
149
150        assert!(feed.syndication.is_some());
151        let syn = feed.syndication.as_ref().unwrap();
152        assert_eq!(syn.update_period, Some(UpdatePeriod::Daily));
153    }
154
155    #[test]
156    fn test_handle_update_frequency() {
157        let mut feed = FeedMeta::default();
158
159        handle_feed_element("updateFrequency", "2", &mut feed);
160
161        assert!(feed.syndication.is_some());
162        let syn = feed.syndication.as_ref().unwrap();
163        assert_eq!(syn.update_frequency, Some("2".to_string()));
164    }
165
166    #[test]
167    fn test_handle_update_base() {
168        let mut feed = FeedMeta::default();
169
170        handle_feed_element("updateBase", "2024-12-18T00:00:00Z", &mut feed);
171
172        assert!(feed.syndication.is_some());
173        let syn = feed.syndication.as_ref().unwrap();
174        assert_eq!(syn.update_base.as_deref(), Some("2024-12-18T00:00:00Z"));
175    }
176
177    #[test]
178    fn test_handle_multiple_elements() {
179        let mut feed = FeedMeta::default();
180
181        handle_feed_element("updatePeriod", "hourly", &mut feed);
182        handle_feed_element("updateFrequency", "1", &mut feed);
183        handle_feed_element("updateBase", "2024-01-01T00:00:00Z", &mut feed);
184
185        let syn = feed.syndication.as_ref().unwrap();
186        assert_eq!(syn.update_period, Some(UpdatePeriod::Hourly));
187        assert_eq!(syn.update_frequency, Some("1".to_string()));
188        assert_eq!(syn.update_base.as_deref(), Some("2024-01-01T00:00:00Z"));
189    }
190
191    #[test]
192    fn test_handle_invalid_frequency() {
193        let mut feed = FeedMeta::default();
194
195        handle_feed_element("updateFrequency", "not-a-number", &mut feed);
196
197        // Raw string is stored as-is (matches Python feedparser behavior)
198        let syn = feed.syndication.as_ref().unwrap();
199        assert_eq!(syn.update_frequency, Some("not-a-number".to_string()));
200    }
201
202    #[test]
203    fn test_handle_unknown_element() {
204        let mut feed = FeedMeta::default();
205
206        handle_feed_element("unknown", "value", &mut feed);
207
208        assert!(feed.syndication.is_none());
209    }
210}