Skip to main content

Crate rss_gen

Crate rss_gen 

Source
Expand description

RSS Gen logo

rss-gen

An RSS and Atom 1.0 syndication library for Rust — generate, parse, and validate feeds across every shipping RSS version plus Atom 1.0, with #![forbid(unsafe_code)] and zero panics on the validated path.

Build Crates.io Docs.rs Coverage OpenSSF Scorecard lib.rs

50 000-item RSS feed in ~15 ms · Atom 1.0 in ~114 ms · format detection in 7 µs · 100 % rustdoc coverage · #![forbid(unsafe_code)]


§Contents


§Install

§As a Rust library

cargo add rss-gen

Or pin explicitly in Cargo.toml:

[dependencies]
rss-gen = "0.0.6"

§MSRV

SurfaceMinimum Supported Rust Version
Library1.88.0 (bumped from 1.79.0 in v0.0.6; time 0.3.51 + time-core 0.1.9 declare rust-version = "1.88", the icu_* chain via url/idna pulls 1.86, and the effective floor through current crates.io is 1.88)

CI runs stable on every push, plus a dedicated MSRV lane that pins the 1.88.0 floor. Cross-platform matrix: Ubuntu + macOS + Windows.

§Build from source

git clone https://github.com/sebastienrousseau/rssgen.git
cd rssgen
cargo test --workspace --all-features    # full suite, ~200 tests

§Quick Start

use rss_gen::{generate_rss, RssData, RssItem, RssVersion};

fn main() -> Result<(), rss_gen::RssError> {
    // Construct an RSS 2.0 channel via the builder API.
    let mut feed = RssData::new(Some(RssVersion::RSS2_0))
        .title("My Rust Blog")
        .link("https://example.com")
        .description("A blog about Rust");

    // Items are appended in the order they should appear.
    feed.add_item(
        RssItem::new()
            .title("Hello, world")
            .link("https://example.com/hello")
            .description("First post")
            .guid("https://example.com/hello"),
    );

    let xml = generate_rss(&feed)?;
    assert!(xml.contains("<rss version=\"2.0\""));
    assert!(xml.contains("<title>My Rust Blog</title>"));
    Ok(())
}

§Library Usage

§RSS generation

rss-gen covers every shipping RSS version. Pick the wire format via RssVersion; the builder is the same for all of them.

use rss_gen::{generate_rss, RssData, RssItem, RssVersion};

fn main() -> Result<(), rss_gen::RssError> {
    let mut feed = RssData::new(Some(RssVersion::RSS2_0))
        .title("Engineering")
        .link("https://example.com")
        .description("Posts from the engineering team")
        .language("en-GB")
        .pub_date("Sat, 27 Jun 2026 00:00:00 GMT")
        .generator("rss-gen")
        // `atom_link` advertises the canonical feed URL via the
        // `<atom:link rel="self">` element on the channel.
        .atom_link("https://example.com/feed.xml");

    feed.add_item(
        RssItem::new()
            .title("Release notes")
            .link("https://example.com/release")
            .description("What shipped this week")
            .guid("https://example.com/release")
            .pub_date("Sat, 27 Jun 2026 00:00:00 GMT")
            .author("editor@example.com"),
    );

    let xml = generate_rss(&feed)?;
    // The emitted document is well-formed XML and includes the
    // standard atom:self link required by most feed readers.
    assert!(xml.contains(r#"<atom:link href="https://example.com/feed.xml""#));
    Ok(())
}

RssVersion::{RSS0_90, RSS0_91, RSS0_92, RSS1_0, RSS2_0} are all supported. RSS 1.0 emits the RDF wrapper (<rdf:RDF>); the others emit a <rss> root.

§Atom 1.0 generation

Atom is a sibling code path — independent types, the same ergonomics. RFC 4287 required elements (id, title, updated) are checked at serialise time; entries inherit feed-level authors when none are declared per-entry, per §4.1.1.

use rss_gen::{generate_atom, AtomEntry, AtomFeed};

fn main() -> Result<(), rss_gen::RssError> {
    let feed = AtomFeed::new()
        .id("https://example.com/feed")
        .title("My Atom Feed")
        .updated("2026-06-27T00:00:00Z")
        .author_name("Jane Doe")
        // Convenience: emit `<link rel="self" href="...">`.
        .self_link("https://example.com/atom.xml")
        .add_entry(
            AtomEntry::new()
                .id("https://example.com/post-1")
                .title("First Post")
                .updated("2026-06-27T00:00:00Z")
                // Plain-text summary; use `summary_html` for an
                // explicit `type="html"` payload.
                .summary("Hello, Atom"),
        );

    let xml = generate_atom(&feed)?;
    assert!(xml.contains(r#"<feed xmlns="http://www.w3.org/2005/Atom">"#));
    Ok(())
}

Media enclosures (podcasts, attached video) follow Atom’s typed-link model:

use rss_gen::{generate_atom, AtomEntry, AtomFeed};

fn main() -> Result<(), rss_gen::RssError> {
    let feed = AtomFeed::new()
        .id("https://example.com/podcast")
        .title("Pilot")
        .updated("2026-06-27T00:00:00Z")
        .author_name("Producer")
        .add_entry(
            AtomEntry::new()
                .id("https://example.com/ep-1")
                .title("Episode 1")
                .updated("2026-06-27T00:00:00Z")
                .summary("Pilot episode")
                // Emits <link rel="enclosure" type="audio/mpeg" length="12345678" .../>
                .add_enclosure(
                    "https://example.com/ep-1.mp3",
                    "audio/mpeg",
                    12_345_678,
                ),
        );

    let xml = generate_atom(&feed)?;
    assert!(xml.contains(r#"rel="enclosure""#));
    assert!(xml.contains(r#"type="audio/mpeg""#));
    Ok(())
}

§Parsing existing feeds

parse_rss reads any supported RSS version into the same RssData struct used by the generator — round-tripping is symmetric.

use rss_gen::parse_rss;

fn main() -> Result<(), rss_gen::RssError> {
    let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Example</title>
    <link>https://example.com</link>
    <description>Sample feed</description>
    <item>
      <title>Post</title>
      <link>https://example.com/post</link>
      <description>Body</description>
    </item>
  </channel>
</rss>"#;

    let feed = parse_rss(xml, None)?;
    assert_eq!(feed.title, "Example");
    assert_eq!(feed.items.len(), 1);
    assert_eq!(feed.items[0].title, "Post");
    Ok(())
}

§Format auto-detection

When the caller does not know upfront whether the document is RSS or Atom, detect_feed_format peeks the root element and returns a classifier value. It does not parse the rest of the document.

use rss_gen::{detect_feed_format, FeedFormat};

let rss_xml  = r#"<?xml version="1.0"?><rss version="2.0"><channel/></rss>"#;
let atom_xml = r#"<?xml version="1.0"?><feed xmlns="http://www.w3.org/2005/Atom"><id/></feed>"#;

assert_eq!(detect_feed_format(rss_xml),  FeedFormat::Rss);
assert_eq!(detect_feed_format(atom_xml), FeedFormat::Atom);

The classifier returns FeedFormat::RssRdf for RSS 1.0 (<rdf:RDF>) and FeedFormat::Unknown for documents that match neither root element — including hand-rolled <feed> documents that omit the Atom namespace declaration, so a misclassified payload does not silently flow into the Atom code path.

§Validation diagnostics

Every validation error is prefixed with the element that produced it, so downstream tools (static site generators, CI gates, IDE integrations) can point at the offending field rather than a bare “missing field” message.

ContextExample error string
RSS channelchannel.title is missing, Invalid channel.link: …, Invalid channel.pub_date: …
RSS itemitem.title is missing, item.link is missing, Invalid item.link: …
Atom feedfeed.id is missing, feed.updated is not a valid RFC 3339 timestamp: …
Atom entryentry.0.author is missing (and feed has no feed-level author), entry.2.updated is missing
use rss_gen::{generate_rss, RssData, RssError};

fn main() {
    // Channel without a title, link, or description.
    let feed = RssData::new(None);

    match generate_rss(&feed) {
        Err(RssError::ValidationErrors(errors)) => {
            // Each entry is a `ValidationError { field, message }` —
            // dispatch programmatically on `field` rather than parsing
            // strings; `Display` (`e.to_string()`) still gives the
            // bare message for human-readable output.
            assert!(errors.iter().any(|e| e.field == "channel.title"));
            assert!(errors.iter().any(|e| e.field == "channel.link"));
            assert!(errors.iter().any(|e| e.field == "channel.description"));
        }
        other => panic!("expected ValidationErrors, got {other:?}"),
    }
}

Item-level link follows RSS 2.0 §5.7 — absolute URLs, root-relative paths (/tags/), and bare paths (articles/foo.html) are all accepted; whitespace, control characters, and empty strings are rejected. Channel-level link retains absolute-URL strictness because the spec requires it.

§quick_rss helper

For one-shot generation of a minimal feed, quick_rss validates input bounds (length caps, URL scheme) and returns the serialised XML directly. Useful inside build.rs, snippet generators, and tests.

use rss_gen::quick_rss;

fn main() -> Result<(), rss_gen::RssError> {
    let xml = quick_rss(
        "My Rust Blog",
        "https://example.com",
        "A blog about Rust",
    )?;
    assert!(xml.contains("<title>My Rust Blog</title>"));
    assert!(xml.contains("<item>")); // an example item is included
    Ok(())
}

§Constants and limits

rss-gen exposes its hard input bounds as pub const so callers can validate ahead of time rather than discover the limit at serialise time.

ConstantValueApplies to
MAX_TITLE_LENGTH256RssData::title, RssItem::title
MAX_LINK_LENGTH2 048RssData::link, RssItem::link
MAX_DESCRIPTION_LENGTH100 000RssData::description, RssItem::description
MAX_GENERAL_LENGTH1 024RssData::category and similar single-line fields
MAX_FEED_SIZE1 048 576 (1 MiB)Combined serialised feed size, enforced by RssData::validate_size

The VERSION constant resolves to the crate’s CARGO_PKG_VERSION at compile time.

use rss_gen::{MAX_FEED_SIZE, VERSION};

assert!(VERSION.starts_with(char::is_numeric));
assert_eq!(MAX_FEED_SIZE, 1_048_576);

§Modules

ModuleSurface
rss_gen::atomAtomFeed, AtomEntry, AtomPerson, AtomLink, AtomTextType, FeedFormat, generate_atom, detect_feed_format.
rss_gen::dataRssData, RssItem, RssVersion, plus the RssDataField / RssItemField enums used by set_field / set_item_field.
rss_gen::errorRssError (the crate-wide error enum) and the Result<T> = std::result::Result<T, RssError> alias.
rss_gen::generatorgenerate_rss, sanitize_content, write_element — the RSS serialisation pipeline.
rss_gen::parserparse_rss, plus the ElementHandler trait for callers that need to plug in custom-element extraction.
rss_gen::validatorStandalone validation helpers used by the generator and exposed for callers that want to validate before constructing.
rss_gen::macrosProcedural shortcuts (macro_set_rss_data_fields!, macro_write_element!, …) for terse builder code.
rss_gen::preludeRe-exports the surface most callers need — RssData, RssItem, RssVersion, AtomFeed, AtomEntry, generate_rss, generate_atom, parse_rss, quick_rss, detect_feed_format, and the error types.

§Features

RSS generationAuthor RSS 0.90, 0.91, 0.92, 1.0, and 2.0 feeds through a single RssData builder. RSS 2.0 emits the standard xmlns:atom declaration so feed readers can recognise the <atom:link rel="self"> element on the channel.
Atom 1.0 generationAtomFeed / AtomEntry cover RFC 4287 required elements, multi-author and contributor lists, categories, xml:lang, icon/logo/rights/subtitle, plain-text and HTML payloads for <summary> / <content>, and <link rel="enclosure"> media attachments.
Parsingparse_rss reads RSS 0.9x / 1.0 / 2.0 into RssData and is symmetric with the generator — parse_rss(generate_rss(x)?, None)? round-trips structurally for valid inputs.
Format detectiondetect_feed_format classifies a document as Rss, RssRdf, Atom, or Unknown from the first start element, without parsing the body.
ValidationRequired-element checks at both feed and item / entry level, with channel., item., feed., and entry.<idx>. context prefixes on every error message. RFC 3339 timestamp validation for Atom; relative item links for RSS 2.0 §5.7.
Sanitisationsanitize_content strips invalid XML control characters and is idempotent — round-tripping through it does not double-encode entities.
Robust XML backendquick-xml 0.40 with the serialize feature. Output is UTF-8, well-formed, and includes the <?xml version="1.0" encoding="utf-8"?> declaration.
Memory safety#![forbid(unsafe_code)] at the crate root; no FFI, no raw-pointer dereferences.
Lint posture#![deny(clippy::all, clippy::cargo, clippy::pedantic)] and #![deny(missing_docs)] — every public item is documented; every public surface passes pedantic Clippy.
Test postureUnit tests, integration tests, doctest coverage, property tests (proptest, quickcheck), and structure-aware fuzzers (cargo fuzz) for parsing, generation, date parsing, content sanitisation, and URL validation.

§Development

cargo build                                  # build the library
cargo test --workspace --all-features        # full test suite
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
cargo bench --bench benchmark                # Criterion harness
cargo +nightly fuzz run fuzz_rss_parsing     # structure-aware fuzzing

CI runs through a reusable workflow that gates Clippy, formatting, tests, cargo audit, cargo deny, dependency review, and CodeQL on every push and pull request. See CONTRIBUTING.md for signed-commit policy and PR guidelines.


§Security

  • No unsafe blocks. #![forbid(unsafe_code)] is enforced at the crate root — no FFI to a C XML parser, no raw-pointer dereferences, no unsafe blocks in the generator, parser, validator, or sanitiser.
  • Input bounds. MAX_TITLE_LENGTH, MAX_LINK_LENGTH, MAX_DESCRIPTION_LENGTH, MAX_GENERAL_LENGTH, and MAX_FEED_SIZE cap every text dimension; quick_rss and RssData::validate / RssData::validate_size enforce them. Set these before exposing feed generation to untrusted input.
  • Content sanitisation. sanitize_content removes invalid XML control characters (everything below 0x20 except \n, \r, \t) and escapes &, <, >, ", and ' so user-supplied content cannot break feed well-formedness or smuggle injected markup.
  • URL hygiene. Channel-level link requires an absolute http / https URL. Item-level link follows RSS 2.0 §5.7 and permits relative paths, but rejects whitespace and control characters that would otherwise break feed-reader parsers.
  • Supply chain. cargo audit and cargo deny run on every push via the shared security workflow. Dependabot is wired for the minor-and-patch group on Cargo.toml. CodeQL static analysis runs on every push and pull request.

Coordinated-disclosure contact and policy live in CONTRIBUTING.md.


§Documentation

DocumentCovers
CHANGELOG.mdPer-release notes following Keep a Changelog 1.1.0.
docs/MIGRATION-0.0.5-to-0.0.6.mdMechanical migration guide for the v0.0.6 API breaks (ValidationErrors payload, parse_date return type, removed deps).
docs/adr/Architecture Decision Records — structured ValidationError, time-only date stack, Atom as a sibling module.
CONTRIBUTING.mdSetup, signed-commit policy, PR guidelines.
.github/SECURITY.mdCoordinated-disclosure policy.
AUTHORS.mdContributor roll.
docs.rs/rss-genGenerated API reference for the published version (100 % coverage).
doc.rssgen.coLive docs published from main via actions/deploy-pages.

THE ARCHITECTSebastien Rousseau

THE ENGINEEUXIS — Enterprise Unified Execution Intelligence System


§License

Dual-licensed under Apache 2.0 or MIT, at your option.

Back to Top

Re-exports§

pub use atom::detect_feed_format;
pub use atom::generate_atom;
pub use atom::AtomEntry;
pub use atom::AtomFeed;
pub use atom::AtomPerson;
pub use atom::AtomTextType;
pub use atom::FeedFormat;
pub use data::RssData;
pub use data::RssItem;
pub use data::RssVersion;
pub use error::Result;
pub use error::RssError;
pub use error::ValidationError;
pub use generator::generate_rss;
pub use parser::parse_rss;

Modules§

atom
Atom 1.0 feed generation, validation, and feed-format detection. Atom 1.0 feed generation, validation, and format detection.
data
Contains the main types and data structures used to represent RSS feeds. This module contains the core data structures and functionality for RSS feeds.
error
Defines error types used throughout the library.
generator
Implements RSS feed generation functionality.
macros
Provides procedural macros for simplifying RSS operations. This module provides macros for generating RSS feeds and setting fields for RSS data.
parser
Implements RSS feed parsing functionality. A robust and flexible RSS feed parser.
prelude
Prelude module for convenient importing of common types and functions.
validator
Provides utilities for validating RSS feeds. RSS feed validator module

Macros§

macro_generate_rss
Generates an RSS feed from the given RssData struct.
macro_get_args
macro_get_args Macro
macro_metadata_option
macro_metadata_option Macro
macro_set_rss_data_fields
Sets fields of the RssData struct.
macro_write_element
Writes an XML element with the given name and content.

Constants§

MAX_DESCRIPTION_LENGTH
Maximum length for description fields in the RSS feed.
MAX_FEED_SIZE
Maximum size for the entire RSS feed.
MAX_GENERAL_LENGTH
Maximum length for general fields in the RSS feed.
MAX_LINK_LENGTH
Maximum length for link fields in the RSS feed.
MAX_TITLE_LENGTH
Maximum length for title fields in the RSS feed.
VERSION
The current version of the rss-gen crate, set at compile-time from Cargo.toml.

Functions§

quick_rss
A convenience function to generate a minimal valid RSS 2.0 feed.