Skip to content

Commit 574ec7e

Browse files
committed
feat(bindings): add syndication and Dublin Core support to Python and Node.js bindings
Add complete support for syndication module (RSS 1.0) and Dublin Core metadata fields in both Python (PyO3) and Node.js (napi-rs) bindings. Changes: - Export SyndicationMeta and UpdatePeriod from core library - Python bindings: - New PySyndicationMeta wrapper class with update_period, update_frequency, update_base getters - Added syndication, dc_creator, dc_publisher, dc_rights getters to PyFeedMeta - Added comprehensive test suite for syndication and Dublin Core fields - Node.js bindings: - New SyndicationMeta struct with automatic camelCase conversion (updatePeriod, etc.) - Added syndication, dcCreator, dcPublisher, dcRights fields to FeedMeta - Added test suite for syndication and Dublin Core fields - All tests passing - Clippy clean with no warnings This completes Phase 3 syndication bindings implementation.
1 parent 9df92a3 commit 574ec7e

File tree

8 files changed

+332
-1
lines changed

8 files changed

+332
-1
lines changed

crates/feedparser-rs-core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ pub use types::{
7272
TextType, parse_duration, parse_explicit,
7373
};
7474

75+
pub use namespace::syndication::{SyndicationMeta, UpdatePeriod};
76+
7577
#[cfg(feature = "http")]
7678
pub use http::{FeedHttpClient, FeedHttpResponse};
7779

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, it } from 'node:test';
2+
import assert from 'node:assert';
3+
import { parse } from '../index.js';
4+
5+
describe('syndication', () => {
6+
it('should parse syndication updatePeriod', () => {
7+
const xml = `<?xml version="1.0"?>
8+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
9+
xmlns="http://purl.org/rss/1.0/"
10+
xmlns:syn="http://purl.org/rss/1.0/modules/syndication/">
11+
<channel>
12+
<title>Test Feed</title>
13+
<link>https://example.com</link>
14+
<syn:updatePeriod>daily</syn:updatePeriod>
15+
</channel>
16+
</rdf:RDF>`;
17+
18+
const feed = parse(xml);
19+
assert.ok(feed.feed.syndication);
20+
assert.strictEqual(feed.feed.syndication.updatePeriod, 'daily');
21+
});
22+
23+
it('should parse syndication updateFrequency', () => {
24+
const xml = `<?xml version="1.0"?>
25+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
26+
xmlns="http://purl.org/rss/1.0/"
27+
xmlns:syn="http://purl.org/rss/1.0/modules/syndication/">
28+
<channel>
29+
<title>Test Feed</title>
30+
<link>https://example.com</link>
31+
<syn:updateFrequency>2</syn:updateFrequency>
32+
</channel>
33+
</rdf:RDF>`;
34+
35+
const feed = parse(xml);
36+
assert.ok(feed.feed.syndication);
37+
assert.strictEqual(feed.feed.syndication.updateFrequency, 2);
38+
});
39+
40+
it('should parse complete syndication metadata', () => {
41+
const xml = `<?xml version="1.0"?>
42+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
43+
xmlns="http://purl.org/rss/1.0/"
44+
xmlns:syn="http://purl.org/rss/1.0/modules/syndication/">
45+
<channel>
46+
<title>Test Feed</title>
47+
<link>https://example.com</link>
48+
<syn:updatePeriod>hourly</syn:updatePeriod>
49+
<syn:updateFrequency>1</syn:updateFrequency>
50+
<syn:updateBase>2024-01-01T00:00:00Z</syn:updateBase>
51+
</channel>
52+
</rdf:RDF>`;
53+
54+
const feed = parse(xml);
55+
const syn = feed.feed.syndication;
56+
assert.ok(syn);
57+
assert.strictEqual(syn.updatePeriod, 'hourly');
58+
assert.strictEqual(syn.updateFrequency, 1);
59+
assert.strictEqual(syn.updateBase, '2024-01-01T00:00:00Z');
60+
});
61+
62+
it('should return undefined when syndication data is missing', () => {
63+
const xml = `<?xml version="1.0"?>
64+
<rss version="2.0">
65+
<channel>
66+
<title>Test Feed</title>
67+
<link>https://example.com</link>
68+
</channel>
69+
</rss>`;
70+
71+
const feed = parse(xml);
72+
assert.strictEqual(feed.feed.syndication, undefined);
73+
});
74+
75+
it('should parse Dublin Core fields', () => {
76+
const xml = `<?xml version="1.0"?>
77+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
78+
xmlns="http://purl.org/rss/1.0/"
79+
xmlns:dc="http://purl.org/dc/elements/1.1/">
80+
<channel rdf:about="https://example.com">
81+
<title>Test Feed</title>
82+
<link>https://example.com</link>
83+
<dc:creator>John Doe</dc:creator>
84+
<dc:publisher>ACME Corp</dc:publisher>
85+
<dc:rights>Copyright 2024</dc:rights>
86+
</channel>
87+
</rdf:RDF>`;
88+
89+
const feed = parse(xml);
90+
assert.strictEqual(feed.feed.dcCreator, 'John Doe');
91+
assert.strictEqual(feed.feed.dcPublisher, 'ACME Corp');
92+
assert.strictEqual(feed.feed.dcRights, 'Copyright 2024');
93+
});
94+
});

crates/feedparser-rs-node/index.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ export interface FeedMeta {
143143
ttl?: number
144144
/** License URL (Creative Commons, etc.) */
145145
license?: string
146+
/** Syndication module metadata (RSS 1.0) */
147+
syndication?: SyndicationMeta
148+
/** Dublin Core creator (author fallback) */
149+
dcCreator?: string
150+
/** Dublin Core publisher */
151+
dcPublisher?: string
152+
/** Dublin Core rights (copyright) */
153+
dcRights?: string
146154
}
147155

148156
/** Generator metadata */
@@ -369,6 +377,16 @@ export interface Source {
369377
id?: string
370378
}
371379

380+
/** Syndication module metadata (RSS 1.0) */
381+
export interface SyndicationMeta {
382+
/** Update period (hourly, daily, weekly, monthly, yearly) */
383+
updatePeriod?: string
384+
/** Number of times updated per period */
385+
updateFrequency?: number
386+
/** Base date for update schedule (ISO 8601) */
387+
updateBase?: string
388+
}
389+
372390
/** Tag/category */
373391
export interface Tag {
374392
/** Tag term/label */

crates/feedparser-rs-node/src/lib.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use feedparser_rs::{
99
FeedMeta as CoreFeedMeta, Generator as CoreGenerator, Image as CoreImage, Link as CoreLink,
1010
ParsedFeed as CoreParsedFeed, ParserLimits, Person as CorePerson,
1111
PodcastPerson as CorePodcastPerson, PodcastTranscript as CorePodcastTranscript,
12-
Source as CoreSource, Tag as CoreTag, TextConstruct as CoreTextConstruct, TextType,
12+
Source as CoreSource, SyndicationMeta as CoreSyndicationMeta, Tag as CoreTag,
13+
TextConstruct as CoreTextConstruct, TextType,
1314
};
1415

1516
/// Default maximum feed size (100 MB) - prevents DoS attacks
@@ -264,6 +265,27 @@ impl From<CoreParsedFeed> for ParsedFeed {
264265
}
265266
}
266267

268+
/// Syndication module metadata (RSS 1.0)
269+
#[napi(object)]
270+
pub struct SyndicationMeta {
271+
/// Update period (hourly, daily, weekly, monthly, yearly)
272+
pub update_period: Option<String>,
273+
/// Number of times updated per period
274+
pub update_frequency: Option<u32>,
275+
/// Base date for update schedule (ISO 8601)
276+
pub update_base: Option<String>,
277+
}
278+
279+
impl From<CoreSyndicationMeta> for SyndicationMeta {
280+
fn from(core: CoreSyndicationMeta) -> Self {
281+
Self {
282+
update_period: core.update_period.map(|p| p.as_str().to_string()),
283+
update_frequency: core.update_frequency,
284+
update_base: core.update_base,
285+
}
286+
}
287+
}
288+
267289
/// Feed metadata
268290
#[napi(object)]
269291
pub struct FeedMeta {
@@ -319,6 +341,14 @@ pub struct FeedMeta {
319341
pub ttl: Option<u32>,
320342
/// License URL (Creative Commons, etc.)
321343
pub license: Option<String>,
344+
/// Syndication module metadata (RSS 1.0)
345+
pub syndication: Option<SyndicationMeta>,
346+
/// Dublin Core creator (author fallback)
347+
pub dc_creator: Option<String>,
348+
/// Dublin Core publisher
349+
pub dc_publisher: Option<String>,
350+
/// Dublin Core rights (copyright)
351+
pub dc_rights: Option<String>,
322352
}
323353

324354
impl From<CoreFeedMeta> for FeedMeta {
@@ -350,6 +380,10 @@ impl From<CoreFeedMeta> for FeedMeta {
350380
id: core.id,
351381
ttl: core.ttl,
352382
license: core.license,
383+
syndication: core.syndication.map(SyndicationMeta::from),
384+
dc_creator: core.dc_creator,
385+
dc_publisher: core.dc_publisher,
386+
dc_rights: core.dc_rights,
353387
}
354388
}
355389
}

crates/feedparser-rs-py/src/types/feed_meta.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use pyo3::prelude::*;
44
use super::common::{PyGenerator, PyImage, PyLink, PyPerson, PyTag, PyTextConstruct};
55
use super::datetime::optional_datetime_to_struct_time;
66
use super::podcast::{PyItunesFeedMeta, PyPodcastMeta};
7+
use super::syndication::PySyndicationMeta;
78

89
#[pyclass(name = "FeedMeta", module = "feedparser_rs")]
910
#[derive(Clone)]
@@ -212,6 +213,29 @@ impl PyFeedMeta {
212213
self.inner.license.as_deref()
213214
}
214215

216+
#[getter]
217+
fn syndication(&self) -> Option<PySyndicationMeta> {
218+
self.inner
219+
.syndication
220+
.as_ref()
221+
.map(|s| PySyndicationMeta::from_core(s.clone()))
222+
}
223+
224+
#[getter]
225+
fn dc_creator(&self) -> Option<&str> {
226+
self.inner.dc_creator.as_deref()
227+
}
228+
229+
#[getter]
230+
fn dc_publisher(&self) -> Option<&str> {
231+
self.inner.dc_publisher.as_deref()
232+
}
233+
234+
#[getter]
235+
fn dc_rights(&self) -> Option<&str> {
236+
self.inner.dc_rights.as_deref()
237+
}
238+
215239
fn __repr__(&self) -> String {
216240
format!(
217241
"FeedMeta(title='{}', link='{}')",

crates/feedparser-rs-py/src/types/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ pub mod entry;
44
pub mod feed_meta;
55
pub mod parsed_feed;
66
pub mod podcast;
7+
pub mod syndication;
78

89
pub use parsed_feed::PyParsedFeed;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use feedparser_rs::SyndicationMeta as CoreSyndicationMeta;
2+
use pyo3::prelude::*;
3+
4+
/// Syndication module metadata
5+
#[pyclass(name = "SyndicationMeta", module = "feedparser_rs")]
6+
#[derive(Clone)]
7+
pub struct PySyndicationMeta {
8+
inner: CoreSyndicationMeta,
9+
}
10+
11+
impl PySyndicationMeta {
12+
pub fn from_core(core: CoreSyndicationMeta) -> Self {
13+
Self { inner: core }
14+
}
15+
}
16+
17+
#[pymethods]
18+
impl PySyndicationMeta {
19+
/// Update period (hourly, daily, weekly, monthly, yearly)
20+
#[getter]
21+
fn update_period(&self) -> Option<String> {
22+
self.inner.update_period.map(|p| p.as_str().to_string())
23+
}
24+
25+
/// Number of times updated per period
26+
#[getter]
27+
fn update_frequency(&self) -> Option<u32> {
28+
self.inner.update_frequency
29+
}
30+
31+
/// Base date for update schedule (ISO 8601)
32+
#[getter]
33+
fn update_base(&self) -> Option<&str> {
34+
self.inner.update_base.as_deref()
35+
}
36+
37+
fn __repr__(&self) -> String {
38+
format!(
39+
"SyndicationMeta(update_period={:?}, update_frequency={:?}, update_base={:?})",
40+
self.update_period(),
41+
self.update_frequency(),
42+
self.update_base()
43+
)
44+
}
45+
}

0 commit comments

Comments
 (0)