///|
/// Convert an RSS channel to an Atom 1.0 feed.
///
/// Mapping (documented, lossy by nature — Atom has no cloud/textInput/etc.):
/// - `title` → `` (plain text)
/// - `link` → ``
/// - `description` → ``
/// - item guid → entry ``, falling back to the item link
/// - item title/link/description/categories map 1:1
pub fn Channel::to_atom(self : Channel) -> @atom.Feed {
let feed = @atom.Feed::new(
id=self.atom_id(),
title=self.title,
updated=self.atom_updated(),
)
let authors : Array[@atom.Person] = match self.managing_editor {
Some(email) => [{ name: "", email: Some(email), uri: None }]
None => []
}
let entries : Array[@atom.Entry] = []
for item in self.items {
let id = match item.guid {
Some(g) => g.value
None => item.link.unwrap_or("")
}
let links : Array[@atom.Link] = match item.link {
Some(link) => [@atom.Link::alternate(link)]
None => []
}
let entry_categories : Array[@atom.Category] = []
for category in item.categories {
entry_categories.push({
term: category.name,
scheme: category.domain,
label: None,
})
}
let item_authors : Array[@atom.Person] = match item.author {
// An RSS author is an email address; Atom wants a name as well.
Some(email) => [{ name: "", email: Some(email), uri: None }]
None => []
}
entries.push({
..@atom.Entry::new(
id~,
title=item.title.unwrap_or(""),
updated=item.atom_updated(),
),
links,
summary: item.description.map(fn(d) { @atom.Text::plain(d) }),
content: item.content.map(fn(c) { @atom.Content::Html(c) }),
categories: entry_categories,
authors: item_authors,
})
}
{
..feed,
subtitle: Some(@atom.Text::plain(self.description)),
links: [@atom.Link::alternate(self.link)],
authors,
entries,
}
}
///|
fn Channel::atom_id(self : Channel) -> String {
self.link
}
///|
fn Channel::atom_updated(self : Channel) -> String {
self.last_build_date.unwrap_or(self.pub_date.unwrap_or(""))
}
///|
fn Item::atom_updated(self : Item) -> String {
self.pub_date.unwrap_or("")
}
///|
fn first_alternate(links : Array[@atom.Link]) -> @atom.Link? {
for link in links {
if link.rel is None || link.rel is Some("alternate") {
return Some(link)
}
}
None
}
///|
fn enclosure_of(links : Array[@atom.Link]) -> Enclosure? {
for link in links {
if link.rel is Some("enclosure") {
return Some({
url: link.href,
length: link.length.unwrap_or("0"),
mime_type: link.media_type.unwrap_or("application/octet-stream"),
})
}
}
None
}
///|
fn content_to_string(c : @atom.Content) -> String {
match c {
@atom.Content::Text(v) => v
@atom.Content::Html(v) => v
@atom.Content::Xhtml(v) => v
@atom.Content::Src(..) => ""
}
}
///|
/// Convert an Atom feed into an RSS channel (the inverse of [to_atom]),
/// so Atom feeds can be consumed with the same API.
pub fn Channel::from_atom(feed : @atom.Feed) -> Channel raise RssError {
let channel_link = match first_alternate(feed.links) {
Some(link) => link.href
None => ""
}
let description = match feed.subtitle {
Some(subtitle) => subtitle.value
None => ""
}
let copyright : String? = match feed.rights {
Some(rights) => Some(rights.value)
None => None
}
let generator : String? = feed.generator.map(fn(g) { g.value.unwrap_or("") })
let channel : Channel = {
title: feed.title.value,
link: channel_link,
description,
language: None,
copyright,
managing_editor: None,
webmaster: None,
pub_date: None,
last_build_date: Some(feed.updated),
categories: [],
generator,
docs: None,
cloud: None,
rating: None,
ttl: None,
image: None,
text_input: None,
skip_hours: [],
skip_days: [],
items: [],
extensions: {},
atom_links: [],
itunes_ext: None,
dublin_core_ext: None,
syndication_ext: None,
namespaces: {},
}
for category in feed.categories {
channel.categories.push({ domain: category.scheme, name: category.term })
}
for entry in feed.entries {
let alternate_link = first_alternate(entry.links)
let enclosure = enclosure_of(entry.links)
let item_categories : Array[Category] = []
for category in entry.categories {
item_categories.push({ domain: category.scheme, name: category.term })
}
// Authors: prefer entry-level, fall back to feed-level (RFC 4287 §4.1.2).
let authors = if entry.authors.length() > 0 {
entry.authors
} else {
feed.authors
}
let author : String? = if authors.length() > 0 {
let person = authors[0]
match person.email {
Some(email) => Some(email)
None => Some(person.name)
}
} else {
None
}
channel.items.push({
title: Some(entry.title.value),
link: alternate_link.map(fn(l) { l.href }),
description: entry.summary.map(fn(s) { s.value }),
author,
categories: item_categories,
comments: None,
enclosure,
guid: Some({ is_permalink: false, value: entry.id }),
pub_date: Some(entry.updated),
source: None,
content: entry.content.map(fn(c) { content_to_string(c) }),
extensions: {},
atom_links: [],
itunes_ext: None,
dublin_core_ext: None,
})
}
channel
}