Skip to main content

feedparser_rs/namespace/
dublin_core.rs

1/// Dublin Core Metadata Element Set (DCMES)
2///
3/// Namespace: <http://purl.org/dc/elements/1.1/>
4/// Prefix: dc
5///
6/// This module provides parsing support for Dublin Core elements commonly
7/// found in RSS 1.0 and other feeds.
8///
9/// Common mappings:
10/// - `dc:creator` → author
11/// - `dc:date` → updated/published
12/// - `dc:subject` → tags
13/// - `dc:description` → subtitle/summary
14/// - `dc:publisher` → publisher
15/// - `dc:rights` → rights
16/// - `dc:title` → title (fallback if RSS/Atom title not present)
17/// - `dc:language` → language
18/// - `dc:identifier` → id (fallback)
19use crate::types::{Entry, FeedMeta, Person, Tag};
20use crate::util::date::parse_date;
21
22/// Dublin Core namespace URI
23pub const DC_NAMESPACE: &str = "http://purl.org/dc/elements/1.1/";
24
25/// Handle Dublin Core element at feed level
26///
27/// # Arguments
28///
29/// * `element` - Local name of the element (without namespace prefix)
30/// * `text` - Text content of the element
31/// * `feed` - Feed metadata to update
32pub fn handle_feed_element(element: &str, text: &str, feed: &mut FeedMeta) {
33    match element {
34        "creator" => {
35            // dc:creator → author (always overrides)
36            feed.author = Some(text.into());
37            // Store in dc_creator field
38            feed.dc_creator = Some(text.into());
39            // Also add to authors list
40            feed.authors.push(Person::from_name(text));
41        }
42        "date" => {
43            if let Some(dt) = parse_date(text) {
44                if feed.published.is_none() {
45                    feed.published = Some(dt);
46                    feed.published_str = Some(text.to_string());
47                }
48                // dc:date → updated (always overrides pubDate promotion)
49                feed.updated = Some(dt);
50                feed.updated_str = Some(text.to_string());
51            }
52        }
53        "subject" => {
54            // dc:subject → tags
55            feed.tags.push(Tag::new(text));
56        }
57        "description" if feed.subtitle.is_none() => {
58            feed.subtitle = Some(text.to_string());
59        }
60        "publisher" => {
61            if feed.publisher.is_none() {
62                feed.publisher = Some(text.into());
63            }
64            feed.dc_publisher = Some(text.into());
65        }
66        "rights" => {
67            if feed.rights.is_none() {
68                feed.rights = Some(text.to_string());
69            }
70            feed.dc_rights = Some(text.to_string());
71        }
72        "title" if feed.title.is_none() => {
73            feed.title = Some(text.to_string());
74        }
75        "language" if feed.language.is_none() => {
76            feed.language = Some(text.into());
77        }
78        "identifier" if feed.id.is_none() => {
79            feed.id = Some(text.to_string());
80        }
81        "contributor" => {
82            // dc:contributor → contributors
83            feed.contributors.push(Person::from_name(text));
84        }
85        _ => {
86            // Ignore unknown DC elements (source, type, format, coverage, etc.)
87        }
88    }
89}
90
91/// Handle Dublin Core element at entry level
92///
93/// # Arguments
94///
95/// * `element` - Local name of the element (without namespace prefix)
96/// * `text` - Text content of the element
97/// * `entry` - Entry to update
98pub fn handle_entry_element(element: &str, text: &str, entry: &mut Entry) {
99    match element {
100        "creator" => {
101            // dc:creator takes precedence over <author>; reconciled at </item>
102            entry.dc_creator = Some(text.into());
103            entry.authors.push(Person::from_name(text));
104        }
105        "date" => {
106            if let Some(dt) = parse_date(text) {
107                entry.dc_date = Some(dt);
108                if entry.published.is_none() {
109                    entry.published = Some(dt);
110                    entry.published_str = Some(text.to_string());
111                }
112                // dc:date is more authoritative than pubDate for updated; always override
113                entry.updated = Some(dt);
114                entry.updated_str = Some(text.to_string());
115            }
116        }
117        "subject" => {
118            entry.dc_subject.push(text.to_string());
119            entry.tags.push(Tag::new(text));
120        }
121        "description" if entry.summary.is_none() => {
122            entry.summary = Some(text.to_string());
123        }
124        "title" if entry.title.is_none() => {
125            entry.title = Some(text.to_string());
126        }
127        "identifier" if entry.id.is_none() => {
128            entry.id = Some(text.into());
129        }
130        "contributor" => {
131            entry.contributors.push(Person::from_name(text));
132        }
133        "rights" => {
134            if entry.rights.is_none() {
135                entry.rights = Some(text.to_string());
136            }
137            entry.dc_rights = Some(text.to_string());
138        }
139        _ => {
140            // Ignore unknown DC elements
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_dc_creator_feed() {
151        let mut feed = FeedMeta::default();
152        handle_feed_element("creator", "John Doe", &mut feed);
153
154        assert_eq!(feed.author.as_deref(), Some("John Doe"));
155        assert_eq!(feed.authors.len(), 1);
156        assert_eq!(feed.authors[0].name.as_deref(), Some("John Doe"));
157    }
158
159    #[test]
160    fn test_dc_multiple_creators() {
161        let mut feed = FeedMeta::default();
162        handle_feed_element("creator", "Alice", &mut feed);
163        handle_feed_element("creator", "Bob", &mut feed);
164
165        // Last dc:creator wins (always overrides)
166        assert_eq!(feed.author.as_deref(), Some("Bob"));
167        // Both are in authors list
168        assert_eq!(feed.authors.len(), 2);
169    }
170
171    #[test]
172    fn test_dc_date() {
173        let mut feed = FeedMeta::default();
174        handle_feed_element("date", "2024-01-15T10:30:00Z", &mut feed);
175
176        assert!(feed.updated.is_some());
177    }
178
179    #[test]
180    fn test_dc_subject() {
181        let mut feed = FeedMeta::default();
182        handle_feed_element("subject", "Technology", &mut feed);
183        handle_feed_element("subject", "Programming", &mut feed);
184
185        assert_eq!(feed.tags.len(), 2);
186        assert_eq!(feed.tags[0].term, "Technology");
187        assert_eq!(feed.tags[1].term, "Programming");
188    }
189
190    #[test]
191    fn test_dc_description() {
192        let mut feed = FeedMeta::default();
193        handle_feed_element("description", "Test description", &mut feed);
194
195        assert_eq!(feed.subtitle.as_deref(), Some("Test description"));
196    }
197
198    #[test]
199    fn test_dc_fallback_title() {
200        let mut feed = FeedMeta::default();
201        handle_feed_element("title", "DC Title", &mut feed);
202
203        assert_eq!(feed.title.as_deref(), Some("DC Title"));
204
205        // Set RSS title - should not be overwritten by DC
206        feed.title = Some("RSS Title".to_string());
207        handle_feed_element("title", "DC Title 2", &mut feed);
208
209        assert_eq!(feed.title.as_deref(), Some("RSS Title"));
210    }
211
212    #[test]
213    fn test_entry_dc_elements() {
214        let mut entry = Entry::default();
215
216        handle_entry_element("creator", "Jane Doe", &mut entry);
217        handle_entry_element("subject", "Tech", &mut entry);
218        handle_entry_element("description", "Entry summary", &mut entry);
219
220        // dc:creator is stored in dc_creator; entry.author is reconciled at </item> in rss.rs
221        assert_eq!(entry.dc_creator.as_deref(), Some("Jane Doe"));
222        assert_eq!(entry.tags.len(), 1);
223        assert_eq!(entry.summary.as_deref(), Some("Entry summary"));
224    }
225
226    #[test]
227    fn test_entry_updated_and_published_from_dc_date() {
228        let mut entry = Entry::default();
229        handle_entry_element("date", "2024-01-15T10:30:00Z", &mut entry);
230
231        assert!(entry.updated.is_some());
232        assert!(entry.published.is_some());
233    }
234
235    #[test]
236    fn test_entry_dc_rights_sets_both_fields() {
237        let mut entry = Entry::default();
238        handle_entry_element("rights", "Copyright 2024 Example", &mut entry);
239
240        assert_eq!(entry.rights.as_deref(), Some("Copyright 2024 Example"));
241        assert_eq!(entry.dc_rights.as_deref(), Some("Copyright 2024 Example"));
242    }
243
244    #[test]
245    fn test_entry_dc_rights_does_not_overwrite_rights() {
246        let mut entry = Entry {
247            rights: Some("Atom rights".to_string()),
248            ..Default::default()
249        };
250        handle_entry_element("rights", "DC rights", &mut entry);
251
252        // dc:rights should not overwrite rights already set (e.g. by Atom <rights>)
253        assert_eq!(entry.rights.as_deref(), Some("Atom rights"));
254        assert_eq!(entry.dc_rights.as_deref(), Some("DC rights"));
255    }
256}