Current section

Files

Jump to
glrss_parser src glrss_parser rss.gleam
Raw

src/glrss_parser/rss.gleam

//// Look at
//// <https://www.rssboard.org/rss-specification>
//// or
//// <https://www.w3schools.com/xml/xml_rss.asp>
//// for more information.
////
//// The tags and attributes should mostly match the names here.
import gleam/dict
import gleam/int
import gleam/list
import gleam/option.{type Option as O, None, Some}
import gleam/result.{try}
import gleam/string
import glrss_parser/rss/date
import party
import xmlm.{
type Input, type Tag, Data, Dtd, ElementEnd, ElementStart, Name, Tag,
} as xm
pub type Rss {
Rss(version: String, channel: Channel)
}
pub fn empty_rss() {
Rss(version: "", channel: empty_channel())
}
pub type Channel {
Channel(
title: String,
link: String,
description: String,
language: O(String),
copyright: O(String),
managing_editor: O(String),
web_master: O(String),
pub_date: O(date.RssDate),
last_build_date: O(date.RssDate),
category: O(Category),
generator: O(String),
docs: O(String),
cloud: O(Cloud),
ttl: O(Int),
image: O(Image),
rating: O(String),
text_input: O(TextInput),
skip_hours: List(Int),
skip_days: List(date.DayOfWeek),
item: List(Item),
)
}
fn empty_channel() {
Channel(
"",
"",
"",
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
[],
[],
[],
)
}
pub type Cloud {
Cloud(
domain: String,
port: Int,
path: String,
register_procedure: String,
protocol: String,
)
}
// fn empty_cloud() {
// Cloud("", 0, "", "", "")
// }
pub type Image {
Image(
url: String,
title: String,
link: String,
width: O(Int),
height: O(Int),
description: O(String),
)
}
fn empty_image() {
Image("", "", "", None, None, None)
}
pub type TextInput {
TextInput(title: String, description: String, name: String, link: String)
}
fn empty_text_input() {
TextInput("", "", "", "")
}
pub type Item {
Item(
title: O(String),
link: O(String),
description: O(String),
author: O(String),
category: O(Category),
comments: O(String),
enclosure: O(Enclosure),
guid: O(String),
pub_date: O(date.RssDate),
source: O(Source),
)
}
fn empty_item() {
Item(None, None, None, None, None, None, None, None, None, None)
}
pub type Source {
Source(url: String, name: O(String))
}
// fn empty_source() {
// Source("", None)
// }
pub type Enclosure {
Enclosure(url: String, length: Int, type_: String)
}
// fn empty_enclosure() {
// Enclosure("", 0, "")
// }
/// <https://www.w3schools.com/xml/rss_tag_category_channel.asp>
pub type Category {
Category(domain: O(String), categories: List(String))
}
// fn empty_category() {
// Category(None, [])
// }
type PErr =
Nil
pub type ParseError {
UnexpectedSignal(xm.Signal)
UnexpectedAttribute(List(xm.Attribute))
UnexpectedData(String)
XmlmError(xm.InputError)
PartyError(party.ParseError(PErr))
NilError
}
type ParseResult(a) =
Result(#(a, Input), ParseError)
pub fn parse(rss_xml: String, skip_unknown_tags skip) -> Result(Rss, ParseError) {
let input =
rss_xml
|> xm.from_string()
|> xm.with_stripping(True)
do_parse(input, empty_rss(), skip) |> result.map(fn(a) { a.0 })
}
pub fn parse_bits(
rss_xml: BitArray,
skip_unknown_tags skip,
) -> Result(Rss, ParseError) {
let input =
rss_xml
|> xm.from_bit_array()
|> xm.with_stripping(True)
do_parse(input, empty_rss(), skip) |> result.map(fn(a) { a.0 })
}
fn do_parse(in: Input, acc: Rss, skip) -> ParseResult(Rss) {
case xm.signal(in) {
Ok(#(xm.Dtd(_), in)) -> do_parse(in, acc, skip)
_ -> {
use in, attrs, uri, name <- handle_exceptions(in, acc, skip, do_parse)
case uri, name {
"", "rss" -> input_rss(in, attrs, skip)
_, _ -> Error(NilError)
}
}
}
}
fn input_rss(in, attrs, skip) -> ParseResult(Rss) {
case attrs {
[xm.Attribute(name: Name(uri: "", local: "version"), value: version)] -> {
use #(done, in) <- result.map(do_input_rss(in, empty_rss(), skip))
#(Rss(..done, version:), in)
}
_ -> Error(UnexpectedAttribute(attrs))
}
}
fn do_input_rss(in, acc, skip) -> Result(#(Rss, Input), ParseError) {
use in, _attrs, uri, name <- handle_exceptions(in, acc, skip, do_input_rss)
case uri, name {
"", "channel" -> {
use #(channel, in) <- result.try(input_channel(in, skip))
do_input_rss(in, Rss(..acc, channel:), skip)
}
_, _ -> Error(NilError)
}
}
fn input_channel(in, skip) {
do_input_channel(in, empty_channel(), skip)
}
fn do_input_channel(in, acc, skip) -> Result(#(Channel, Input), ParseError) {
use in, attrs, uri, name <- handle_exceptions(in, acc, skip, do_input_channel)
case uri {
"" ->
case name {
"title" -> {
use #(title, in) <- result.try(read_data(in, skip))
do_input_channel(in, Channel(..acc, title:), skip)
}
"link" -> {
use #(link, in) <- result.try(read_data(in, skip))
do_input_channel(in, Channel(..acc, link:), skip)
}
"description" -> {
use #(description, in) <- result.try(read_data(in, skip))
do_input_channel(in, Channel(..acc, description:), skip)
}
"language" -> {
use #(language, in) <- result.try(read_data(in, skip))
do_input_channel(in, Channel(..acc, language: Some(language)), skip)
}
"copyright" -> {
use #(copyright, in) <- result.try(read_data(in, skip))
do_input_channel(in, Channel(..acc, copyright: Some(copyright)), skip)
}
"managingEditor" -> {
use #(managing_editor, in) <- result.try(read_data(in, skip))
do_input_channel(
in,
Channel(..acc, managing_editor: Some(managing_editor)),
skip,
)
}
"webMaster" -> {
use #(web_master, in) <- result.try(read_data(in, skip))
do_input_channel(
in,
Channel(..acc, web_master: Some(web_master)),
skip,
)
}
"pubDate" -> {
use #(pub_date, in) <- result.try(read_data(in, skip))
use pub_date <- result.try(date.parse(pub_date) |> party_error())
do_input_channel(in, Channel(..acc, pub_date: Some(pub_date)), skip)
}
"lastBuildDate" -> {
use #(last_build_date, in) <- result.try(read_data(in, skip))
use last_build_date <- result.try(
date.parse(last_build_date) |> party_error(),
)
do_input_channel(
in,
Channel(..acc, last_build_date: Some(last_build_date)),
skip,
)
}
"category" -> {
use #(categories, in) <- result.try(read_data(in, skip))
let domain = case attrs {
[xm.Attribute(name: Name(uri: "", local: "domain"), value: domain)] ->
Some(domain)
_ -> None
}
let categories = string.split(categories, on: "/")
let category = Some(Category(domain:, categories:))
do_input_channel(in, Channel(..acc, category:), skip)
}
"generator" -> {
use #(generator, in) <- result.try(read_data(in, skip))
do_input_channel(in, Channel(..acc, generator: Some(generator)), skip)
}
"docs" -> {
use #(docs, in) <- result.try(read_data(in, skip))
do_input_channel(in, Channel(..acc, docs: Some(docs)), skip)
}
"cloud" -> {
use #(_, in) <- result.try(read_data(in, skip))
let attrs_d =
attrs
|> list.map(fn(attr) { #(attr.name.local, attr.value) })
|> dict.from_list()
let g = fn(a) { attrs_d |> dict.get(a) }
case
g("domain"),
g("port"),
g("path"),
g("registerProcedure"),
g("protocol")
{
Ok(domain), Ok(port), Ok(path), Ok(register_procedure), Ok(protocol)
-> {
use port <- result.try(
int.parse(port)
|> result.map_error(fn(_) { UnexpectedAttribute(attrs) }),
)
let cloud =
Some(Cloud(
domain:,
port:,
path:,
register_procedure:,
protocol:,
))
do_input_channel(in, Channel(..acc, cloud:), skip)
}
_, _, _, _, _ -> Error(UnexpectedAttribute(attrs))
}
}
"ttl" -> {
use #(ttl, in) <- result.try(read_data(in, skip))
use ttl <- result.try(
int.parse(ttl)
|> result.map_error(fn(_) { UnexpectedData(ttl) }),
)
do_input_channel(in, Channel(..acc, ttl: Some(ttl)), skip)
}
"image" -> {
use #(image, in) <- result.try(input_image(in, skip))
do_input_channel(in, Channel(..acc, image: Some(image)), skip)
}
"rating" -> {
use #(rating, in) <- result.try(read_data(in, skip))
do_input_channel(in, Channel(..acc, rating: Some(rating)), skip)
}
"textInput" -> {
use #(text_input, in) <- result.try(input_text_input(in, skip))
do_input_channel(
in,
Channel(..acc, text_input: Some(text_input)),
skip,
)
}
"skipHours" -> {
use #(skip_hours, in) <- result.try(read_multiple(in, skip))
use skip_hours <- result.try(
list.try_map(skip_hours, fn(a) {
a |> int.parse() |> result.replace_error(a)
})
|> result.map_error(UnexpectedData),
)
do_input_channel(in, Channel(..acc, skip_hours:), skip)
}
"skipDays" -> {
use #(skip_days, in) <- result.try(read_multiple(in, skip))
use skip_days <- result.try(
list.try_map(skip_days, fn(a) {
a |> date.dow_from() |> result.replace_error(a)
})
|> result.map_error(UnexpectedData),
)
do_input_channel(in, Channel(..acc, skip_days:), skip)
}
"item" -> {
use #(item, in) <- result.try(input_item(in, skip))
do_input_channel(in, Channel(..acc, item: [item, ..acc.item]), skip)
}
_ -> Error(NilError)
}
_ -> Error(NilError)
}
}
fn input_image(in, skip) {
do_input_image(in, empty_image(), skip)
}
fn do_input_image(in, acc: Image, skip) {
use in, _attrs, uri, name <- handle_exceptions(in, acc, skip, do_input_image)
case uri {
"" ->
case name {
"url" -> {
use #(url, in) <- result.try(read_data(in, skip))
do_input_image(in, Image(..acc, url:), skip)
}
"title" -> {
use #(title, in) <- result.try(read_data(in, skip))
do_input_image(in, Image(..acc, title:), skip)
}
"link" -> {
use #(link, in) <- result.try(read_data(in, skip))
do_input_image(in, Image(..acc, link:), skip)
}
"width" -> {
use #(width, in) <- result.try(read_data(in, skip))
use width <- result.try(
int.parse(width)
|> result.map_error(fn(_) { UnexpectedData(width) }),
)
do_input_image(in, Image(..acc, width: Some(width)), skip)
}
"height" -> {
use #(height, in) <- result.try(read_data(in, skip))
use height <- result.try(
int.parse(height)
|> result.map_error(fn(_) { UnexpectedData(height) }),
)
do_input_image(in, Image(..acc, height: Some(height)), skip)
}
"description" -> {
use #(description, in) <- result.try(read_data(in, skip))
do_input_image(in, Image(..acc, description: Some(description)), skip)
}
_ -> Error(NilError)
}
_ -> Error(NilError)
}
}
fn input_text_input(in, skip) {
do_input_text_input(in, empty_text_input(), skip)
}
fn do_input_text_input(in, acc, skip) {
use in, _attrs, uri, name <- handle_exceptions(
in,
acc,
skip,
do_input_text_input,
)
case uri {
"" ->
case name {
"title" -> {
use #(title, in) <- try(read_data(in, skip))
do_input_text_input(in, TextInput(..acc, title:), skip)
}
"description" -> {
use #(description, in) <- try(read_data(in, skip))
do_input_text_input(in, TextInput(..acc, description:), skip)
}
"name" -> {
use #(name, in) <- try(read_data(in, skip))
do_input_text_input(in, TextInput(..acc, name:), skip)
}
"link" -> {
use #(link, in) <- try(read_data(in, skip))
do_input_text_input(in, TextInput(..acc, link:), skip)
}
_ -> Error(NilError)
}
_ -> Error(NilError)
}
}
fn input_item(in, skip) {
do_input_item(in, empty_item(), skip)
}
fn do_input_item(in, acc, skip) {
use in, attrs, uri, name <- handle_exceptions(in, acc, skip, do_input_item)
case uri {
"" ->
case name {
"title" -> {
use #(title, in) <- try(read_data(in, skip))
do_input_item(in, Item(..acc, title: Some(title)), skip)
}
"link" -> {
use #(link, in) <- try(read_data(in, skip))
do_input_item(in, Item(..acc, link: Some(link)), skip)
}
"description" -> {
use #(description, in) <- try(read_data(in, skip))
do_input_item(in, Item(..acc, description: Some(description)), skip)
}
"author" -> {
use #(author, in) <- try(read_data(in, skip))
do_input_item(in, Item(..acc, author: Some(author)), skip)
}
"category" -> {
use #(categories, in) <- result.try(read_data(in, skip))
let domain = case attrs {
[xm.Attribute(name: Name(uri: "", local: "domain"), value: domain)] ->
Some(domain)
_ -> None
}
let categories = string.split(categories, on: "/")
let category = Some(Category(domain:, categories:))
do_input_item(in, Item(..acc, category:), skip)
}
"comments" -> {
use #(comments, in) <- try(read_data(in, skip))
do_input_item(in, Item(..acc, comments: Some(comments)), skip)
}
"enclosure" -> {
use in <- result.try(skip_unknown(in))
let attrs_d =
attrs
|> list.map(fn(attr) { #(attr.name.local, attr.value) })
|> dict.from_list()
let g = fn(a) { attrs_d |> dict.get(a) }
case g("url"), g("length"), g("type") {
Ok(url), Ok(length), Ok(type_) -> {
use length <- result.try(
int.parse(length)
|> result.map_error(fn(_) { UnexpectedAttribute(attrs) }),
)
let enclosure = Some(Enclosure(url:, length:, type_:))
do_input_item(in, Item(..acc, enclosure:), skip)
}
_, _, _ -> Error(UnexpectedAttribute(attrs))
}
}
"guid" -> {
use #(guid, in) <- try(read_data(in, skip))
do_input_item(in, Item(..acc, guid: Some(guid)), skip)
}
"pubDate" -> {
use #(pub_date, in) <- try(read_data(in, skip))
use pub_date <- result.try(date.parse(pub_date) |> party_error())
do_input_item(in, Item(..acc, pub_date: Some(pub_date)), skip)
}
"source" -> {
use #(name, in) <- try(read_data(in, skip))
let name = case name {
"" -> None
a -> Some(a)
}
case attrs {
[xm.Attribute(name: Name(uri: "", local: "url"), value: url)] -> {
let source = Source(url:, name:)
do_input_item(in, Item(..acc, source: Some(source)), skip)
}
_ -> Error(UnexpectedAttribute(attrs))
}
}
_ -> Error(NilError)
}
_ -> Error(NilError)
}
}
fn read_data(in, skip) {
do_read_data(in, "", skip)
}
fn do_read_data(in, acc, skip) {
case xm.signal(in) {
Ok(#(xm.Data(acc), in)) -> do_read_data(in, acc, skip)
Ok(#(ElementEnd, in)) -> Ok(#(acc, in))
Ok(#(sig, in)) -> handle_unknown(in, acc, skip, sig, do_read_data)
Error(e) -> Error(XmlmError(e))
}
}
fn party_error(res: Result(a, party.ParseError(PErr))) {
res |> result.map_error(fn(err) { PartyError(err) })
}
fn handle_exceptions(in, acc, skip, next, fun) {
case xm.signal(in) {
Ok(#(
ElementStart(Tag(Name(uri:, local: name), attributes: attrs)) as sig,
in_1,
)) ->
case fun(in_1, attrs, uri, name) {
Ok(acc) -> Ok(acc)
Error(NilError) -> handle_unknown(in, acc, skip, sig, next)
Error(err) -> Error(err)
}
Ok(#(ElementEnd, in)) -> Ok(#(acc, in))
Ok(#(Dtd(..) as sig, in)) | Ok(#(Data(..) as sig, in)) ->
handle_unknown(in, acc, skip, sig, next)
//handle_unknown(in, acc, skip, sig, do_read_data)
Error(e) -> Error(XmlmError(e))
}
}
/// If skip is true, next. else, error
fn handle_unknown(in, acc: a, skip, sig, next) {
case skip {
True -> result.try(skip_unknown(in), fn(in) { next(in, acc, skip) })
False -> Error(UnexpectedSignal(sig))
}
}
fn skip_unknown(in) {
case xm.signal(in) {
Ok(#(ElementEnd, in)) -> Ok(in)
Ok(#(ElementStart(..), in)) -> result.try(skip_unknown(in), skip_unknown)
Ok(#(_sig, in)) -> skip_unknown(in)
Error(e) -> Error(XmlmError(e))
}
}
fn read_multiple(in, skip) {
do_read_multiple(in, [], skip)
}
fn do_read_multiple(in, acc, skip) {
case xm.signal(in) {
Ok(#(sig, in)) ->
case sig {
ElementStart(..) -> {
use #(data, in) <- result.try(read_data(in, skip))
do_read_multiple(in, [data, ..acc], skip)
}
ElementEnd -> Ok(#(acc, in))
_ -> handle_unknown(in, acc, skip, sig, do_read_multiple)
}
Error(e) -> Error(XmlmError(e))
}
}