rss

RSS syndication format library (port of the Rust rss crate)

moon add marianoguerra/rss@0.1.1
Download zip
Version
0.1.1
License
Apache-2.0
Last updated
12 hours ago
Downloads
2
README

#marianoguerra/rss

An idiomatic MoonBit library for reading and writing the RSS web content syndication format — a port of the Rust rss crate, with full Atom 1.0 support via marianoguerra/atom and a built-in streaming XML engine (marianoguerra/xml).

#Supported versions

  • Reading: RSS 0.90, 0.91, 0.92, 1.0 and 2.0 (plus Atom via from_atom)
  • Writing: RSS 2.0

#Reading

///|
test {
let channel = try! Channel::read_from(
"<?xml version=\"1.0\"?><rss version=\"2.0\"><channel><title>News</title><link>https://example.com/</link><description>An example feed.</description><item><title>Hello</title></item></channel></rss>",
)
inspect(channel.title, content="News")
inspect(channel.items[0].title.unwrap_or(""), content="Hello")
}

As a best effort for invalid feeds, elements declared required by RSS 2.0 default to an empty string.

#Writing

///|
test {
let channel = try! Channel::read_from(
"<rss version=\"2.0\"><channel><title>T</title><link>https://e.com/</link><description>D</description></channel></rss>",
)
let xml = channel.to_xml()
// Writing then parsing reproduces the same model.
let again = try! Channel::read_from(xml)
assert_true(again == channel)
}

#Validation

///|
test {
let channel = try! Channel::read_from(
"<rss version=\"2.0\"><channel><title>T</title><link>notaurl</link><description>D</description></channel></rss>",
)
let verdict = try {
ignore(channel.validate())
"ok"
} catch {
_ => "failed"
}
inspect(verdict, content="failed")
}

#Extensions

Elements in non-default namespaces are collected generically into Channel.extensions / Item.extensions, and the well-known namespaces are additionally extracted into typed structs:

  • iTunes podcast metadata (itunes_ext)
  • Dublin Core (dublin_core_ext)
  • Syndication (syndication_ext)
  • Atom links (atom_links)

#Atom interop

///|
test {
let channel = try! Channel::read_from(
"<rss version=\"2.0\"><channel><title>T</title><link>https://e.com/</link><description>D</description></channel></rss>",
)
let feed = channel.to_atom()
inspect(feed.title.value, content="T")
let back = try! Channel::from_atom(feed)
assert_true(back.title == channel.title)
}

#Testing methodology

This port is validated against the Rust crate itself:

  • Golden parity tests — every fixture from the crate's own test suite is parsed by both implementations; the resulting models must be identical, and our writer's output must be byte-identical to Channel::to_string.
  • Live differential testingscripts/oracle-roundtrip.sh runs four round-trip directions between this library and a vendored Rust oracle CLI (oracle/, pinned to rss = "=2.1.0"). Regenerate goldens after changing fixtures with scripts/gen-goldens.sh (needs cargo).
  • Property-based testsmoonbitlang/quickcheck verifies parse(write(c)) == c and writer idempotence over generated channels whose text exercises escaping edge cases.
  • Fuzzing — deterministic mutation fuzzing over the fixture corpus asserts the parser always returns cleanly and never panics.

The golden files are committed, so moon test runs the full parity suite without a Rust toolchain.

#License

Apache-2.0

#
ExtensionMap

type ExtensionMap = Map[String, Map[String, Array[Extension]]]

A map of extension namespace prefixes to local names to elements.

Mirrors rss::extension::ExtensionMap.

#
RssError

pub(all) suberror RssError {
Xml(err~ :
XmlError
)
InvalidStartTag
Eof
} derive(Eq,
Debug
)

Errors that occur while reading or writing an RSS channel.
impl Show for RssError

#
ValidationError

pub(all) suberror ValidationError {
Invalid(String)
Validation(String)
} derive(Eq,
Debug
)

Errors produced when a value fails RSS-spec validation, mirroring rss::validation::ValidationError.

#
Category

pub(all) struct Category {
domain : String?
name : String
} derive(Eq,
Debug
)

Represents a category in an RSS feed.

#
Category::validate

fn Category::validate(self : Category) -> Unit raise ValidationError

Validate this category against the RSS spec.

#
Channel

pub(all) struct Channel {
title : String
link : String
description : String
language : String?
copyright : String?
managing_editor : String?
webmaster : String?
pub_date : String?
last_build_date : String?
categories : Array[Category]
generator : String?
docs : String?
cloud : Cloud?
rating : String?
ttl : String?
image : Image?
text_input : TextInput?
skip_hours : Array[String]
skip_days : Array[String]
items : Array[Item]
extensions : Map[String, Map[String, Array[Extension]]]
atom_links : Array[
Link
]
itunes_ext : ITunesChannelExtension?
dublin_core_ext : DublinCoreExtension?
syndication_ext : SyndicationExtension?
namespaces : Map[String, String]
} derive(Eq,
Debug
)

Represents the channel of an RSS feed.
impl Show for Channel

#
Channel::from_atom

fn Channel::from_atom(feed :
Feed
) -> Channel raise RssError

Convert an Atom feed into an RSS channel (the inverse of [to_atom]), so Atom feeds can be consumed with the same API.

#
Channel::from_canon

fn Channel::from_canon(json : Json) -> Channel raise RssError

Reconstruct a channel from canonical JSON.

#
Channel::read_from

fn Channel::read_from(input : String) -> Channel raise RssError

Parse an RSS feed from any of the supported versions (RSS 0.90, 0.91, 0.92, 1.0 and 2.0).

Mirrors Channel::read_from. As a best effort for invalid feeds, elements the RSS 2.0 spec declares required default to an empty string.

#
Channel::to_atom

Convert an RSS channel to an Atom 1.0 feed.

Mapping (documented, lossy by nature — Atom has no cloud/textInput/etc.):
  • title<title> (plain text)
  • link<link rel="alternate">
  • description<subtitle>
  • item guid → entry <id>, falling back to the item link
  • item title/link/description/categories map 1:1

#
Channel::to_canon

fn Channel::to_canon(self : Channel) -> Json

Convert a parsed channel to the canonical JSON form.

#
Channel::to_xml

fn Channel::to_xml(self : Channel) -> String

Serialise the channel as RSS 2.0 XML.

#
Channel::validate

fn Channel::validate(self : Channel) -> Unit raise ValidationError

Validate this channel against the RSS specification.

#
Cloud

pub(all) struct Cloud {
domain : String
port : String
path : String
register_procedure : String
protocol : String
} derive(Eq,
Debug
)

Represents a cloud in an RSS feed: a registry for update notifications.

#
Cloud::validate

fn Cloud::validate(self : Cloud) -> Unit raise ValidationError

Validate this cloud against the RSS spec.

#
DublinCoreExtension

pub(all) struct DublinCoreExtension {
contributors : Array[String]
coverages : Array[String]
creators : Array[String]
dates : Array[String]
descriptions : Array[String]
formats : Array[String]
identifiers : Array[String]
languages : Array[String]
publishers : Array[String]
relations : Array[String]
rights : Array[String]
sources : Array[String]
subjects : Array[String]
titles : Array[String]
types : Array[String]
} derive(Eq,
Debug
)

Dublin Core metadata extracted from a channel or item.

Every field is a list because Dublin Core elements are repeatable.

#
DublinCoreExtension::from_map

fn DublinCoreExtension::from_map(map : Map[String, Array[Extension]]) -> DublinCoreExtension

Build a Dublin Core extension from parsed generic extensions, mirroring DublinCoreExtension::from_map.

#
Enclosure

pub(all) struct Enclosure {
url : String
length : String
mime_type : String
} derive(Eq,
Debug
)

Represents an enclosure (media attachment) in an RSS item.

#
Enclosure::validate

fn Enclosure::validate(self : Enclosure) -> Unit raise ValidationError

Validate this enclosure against the RSS spec.

#
Extension

pub(all) struct Extension {
name : String
value : String?
attrs : Map[String, String]
children : Map[String, Array[Extension]]
} derive(Eq,
Debug
)

An element from a non-default namespace, kept generically.

#
Guid

pub(all) struct Guid {
is_permalink : Bool
value : String
} derive(Eq,
Debug
)

Represents the GUID of an RSS item.

#
ITunesCategory

pub(all) struct ITunesCategory {
text : String
subcategory : ITunesCategory?
} derive(Eq,
Debug
)

An iTunes category with an optional nested subcategory.

#
ITunesChannelExtension

pub(all) struct ITunesChannelExtension {
author : String?
block : String?
categories : Array[ITunesCategory]
image : String?
explicit : String?
complete : String?
new_feed_url : String?
owner : ITunesOwner?
subtitle : String?
summary : String?
keywords : String?
itunes_type : String?
} derive(Eq,
Debug
)

iTunes metadata extracted from a channel.

#
ITunesChannelExtension::from_map

Build a channel extension from parsed generic extensions, mirroring ITunesChannelExtension::from_map.

#
ITunesItemExtension

pub(all) struct ITunesItemExtension {
author : String?
block : String?
image : String?
duration : String?
explicit : String?
closed_captioned : String?
order : String?
subtitle : String?
summary : String?
keywords : String?
episode : String?
season : String?
episode_type : String?
} derive(Eq,
Debug
)

iTunes metadata extracted from an item.

#
ITunesItemExtension::from_map

fn ITunesItemExtension::from_map(map : Map[String, Array[Extension]]) -> ITunesItemExtension

Build an item extension from parsed generic extensions, mirroring ITunesItemExtension::from_map.

#
ITunesOwner

pub(all) struct ITunesOwner {
name : String?
email : String?
} derive(Eq,
Debug
)

Contact information for the owner of a podcast.

#
Image

pub(all) struct Image {
url : String
title : String
link : String
width : String?
height : String?
description : String?
} derive(Eq,
Debug
)

Represents an image that can be displayed with a channel.

#
Image::validate

fn Image::validate(self : Image) -> Unit raise ValidationError

Validate this image against the RSS spec.

#
Item

pub(all) struct Item {
title : String?
link : String?
description : String?
author : String?
categories : Array[Category]
comments : String?
enclosure : Enclosure?
guid : Guid?
pub_date : String?
source : Source?
content : String?
extensions : Map[String, Map[String, Array[Extension]]]
atom_links : Array[
Link
]
itunes_ext : ITunesItemExtension?
dublin_core_ext : DublinCoreExtension?
} derive(Eq,
Debug
)

Represents an item in an RSS feed.

#
Item::validate

fn Item::validate(self : Item) -> Unit raise ValidationError

Validate this item against the RSS spec.

#
Source

pub(all) struct Source {
url : String
title : String?
} derive(Eq,
Debug
)

Represents the source of an RSS item.

#
Source::validate

fn Source::validate(self : Source) -> Unit raise ValidationError

Validate this source against the RSS spec.

#
SyndicationExtension

pub(all) struct SyndicationExtension {
period : UpdatePeriod
frequency : Int
base : String
} derive(Eq,
Debug
)

Syndication metadata extracted from a channel (when to refresh, how often, and relative to which base time).

#
SyndicationExtension::from_map

Build a syndication extension from parsed generic extensions, mirroring SyndicationExtension::from_map.

#
TextInput

pub(all) struct TextInput {
title : String
description : String
name : String
link : String
} derive(Eq,
Debug
)

Represents a text input that can be displayed with a channel.

#
TextInput::validate

fn TextInput::validate(self : TextInput) -> Unit raise ValidationError

Validate this text input against the RSS spec.

#
UpdatePeriod

pub(all) enum UpdatePeriod {
Hourly
Daily
Weekly
Monthly
Yearly
} derive(Eq,
Debug
)

The update period of a syndicated feed.

#
DUBLIN_CORE_NAMESPACE

let DUBLIN_CORE_NAMESPACE : String

The Dublin Core namespace.

#
ITUNES_NAMESPACE

let ITUNES_NAMESPACE : String

The iTunes podcast namespace.

#
SYNDICATION_NAMESPACE

let SYNDICATION_NAMESPACE : String

The Syndication namespace.

#
canonical_stringify

fn canonical_stringify(j : Json) -> String

Compact, deterministic serialisation: object keys are emitted in sorted order (matching serde_json over BTreeMaps).

#
extension_entry

fn extension_entry(extensions : Map[String, Map[String, Array[Extension]]], ns : String, name : String) -> Array[Extension]

Record an extension element under ns / name.

#
parse_rfc2822

fn parse_rfc2822(s : String) -> Bool

Parse an RFC 822 / RFC 2822 date-time, returning its components. Accepts the common forms used by feeds: [Weekday,] DD Mon YYYY HH:MM[:SS] Zone.

#
sort_json_keys

fn sort_json_keys(j : Json) -> Json

Recursively sort JSON object keys so that output is byte-identical to serde_json's BTreeMap-based serialisation used by the oracle.

#
str_lexicographic

fn str_lexicographic(a : String, b : String) -> Int

Lexicographic (code-unit wise) string comparison, matching the byte-wise Ord of Rust strings used by the oracle's BTreeMap ordering.