├── .gitignore ├── .travis.yml ├── README.md ├── config └── config.exs ├── lib ├── feeder_ex.ex └── feeder_ex │ └── parser.ex ├── mix.exs ├── mix.lock └── test ├── feeder_ex └── parser_test.exs ├── feeder_ex_test.exs ├── sample-rss-2.xml └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /doc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | notifications: 3 | email: 4 | - mk@manukall.de 5 | elixir: 6 | - 1.2.6 7 | - 1.3.4 8 | env: MIX_ENV=test 9 | otp_release: 10 | - 18.3 11 | - 19.2 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FeederEx 2 | ======== 3 | [![Build Status](https://travis-ci.org/manukall/feeder_ex.svg?branch=master)](https://travis-ci.org/manukall/feeder_ex) 4 | 5 | Simple wrapper for https://github.com/michaelnisi/feeder 6 | 7 | ## Usage 8 | 9 | ``` 10 | iex> HTTPoison.start 11 | iex> {:ok, %HTTPoison.Response{body: body}} = HTTPoison.get("https://www.rssboard.org/files/sample-rss-2.xml") 12 | iex> {:ok, feed, _} = FeederEx.parse(body) 13 | ... 14 | iex> feed.title 15 | "Liftoff News" 16 | iex> Enum.map feed.entries, fn(entry) -> entry.title end 17 | ["Star City", :undefined, "The Engine That Does More", 18 | "Astronauts' Dirty Laundry"] 19 | ``` 20 | 21 | ## Results 22 | 23 | ### FeederEx.Feed 24 | 25 | - author 26 | - id 27 | - image 28 | - link 29 | - language 30 | - subtitle 31 | - summary 32 | - title 33 | - updated 34 | - url 35 | - entries 36 | 37 | ### FeederEx.Entry 38 | 39 | - author 40 | - categories 41 | - duration 42 | - enclosure 43 | - id 44 | - image 45 | - link 46 | - subtitle 47 | - summary 48 | - title 49 | - updated 50 | 51 | 52 | ### FeederEx.Enclosure 53 | 54 | - url 55 | - size 56 | - type 57 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /lib/feeder_ex.ex: -------------------------------------------------------------------------------- 1 | defmodule FeederEx do 2 | defmodule Feed do 3 | defstruct author: nil, id: nil, image: nil, link: nil, language: nil, 4 | subtitle: nil, summary: nil, title: nil, updated: nil, url: nil, entries: [] 5 | end 6 | 7 | defmodule Entry do 8 | defstruct author: nil, categories: [], duration: nil, enclosure: nil, id: nil, 9 | image: nil, link: nil, subtitle: nil, summary: nil, title: nil, 10 | updated: nil 11 | end 12 | 13 | defmodule Enclosure do 14 | defstruct url: nil, size: nil, type: nil 15 | end 16 | 17 | def parse_file(filename) do 18 | :feeder.file filename, opts() 19 | end 20 | 21 | def parse(feed_bin) do 22 | :feeder.stream feed_bin, opts() 23 | end 24 | 25 | def parse!(feed_bin) do 26 | {:ok, feed, _} = parse(feed_bin) 27 | feed 28 | end 29 | 30 | defp opts do 31 | [event_state: {nil, []}, event_fun: &FeederEx.Parser.event/2] 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /lib/feeder_ex/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule FeederEx.Parser do 2 | 3 | def event({:feed, 4 | {:feed, author, id, image, language, link, subtitle, summary, title, updated, url}}, 5 | {_, entries}) do 6 | feed = %FeederEx.Feed{ 7 | author: undefined_to_nil(author), 8 | id: undefined_to_nil(id), 9 | image: undefined_to_nil(image), 10 | language: undefined_to_nil(language), 11 | link: undefined_to_nil(link), 12 | subtitle: undefined_to_nil(subtitle), 13 | summary: undefined_to_nil(summary), 14 | title: undefined_to_nil(title), 15 | updated: undefined_to_nil(updated), 16 | url: undefined_to_nil(url) 17 | } 18 | {feed, entries} 19 | end 20 | 21 | def event({:entry, 22 | {:entry, author, categories, duration, enclosure, id, image, link, subtitle, summary, title, updated}}, 23 | {feed, entries}) do 24 | entry = %FeederEx.Entry{ 25 | author: undefined_to_nil(author), 26 | categories: undefined_to_list(categories), 27 | duration: undefined_to_nil(duration), 28 | enclosure: parse_enclosure(enclosure), 29 | id: undefined_to_nil(id), 30 | image: undefined_to_nil(image), 31 | link: undefined_to_nil(link), 32 | subtitle: undefined_to_nil(subtitle), 33 | summary: undefined_to_nil(summary), 34 | title: undefined_to_nil(title), 35 | updated: undefined_to_nil(updated) 36 | } 37 | {feed, [entry | entries]} 38 | end 39 | 40 | def event(:endFeed, {nil, entries}), do: %FeederEx.Feed{entries: entries} 41 | def event(:endFeed, {feed, entries}) do 42 | %{feed | entries: Enum.reverse(entries)} 43 | end 44 | 45 | defp undefined_to_list(:undefined), do: [] 46 | defp undefined_to_list(value), do: value 47 | 48 | defp undefined_to_nil(:undefined), do: nil 49 | defp undefined_to_nil(value), do: value 50 | 51 | defp parse_enclosure(:undefined), do: nil 52 | defp parse_enclosure({:enclosure, url, size, type}) do 53 | %FeederEx.Enclosure{ 54 | url: undefined_to_nil(url), 55 | size: undefined_to_nil(size), 56 | type: undefined_to_nil(type) 57 | } 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule FeederEx.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :feeder_ex, 6 | version: "1.1.0", 7 | elixir: "~> 1.0", 8 | deps: deps(), 9 | package: package()] 10 | end 11 | 12 | # Configuration for the OTP application 13 | # 14 | # Type `mix help compile.app` for more information 15 | def application do 16 | [applications: [:logger, :feeder]] 17 | end 18 | 19 | # Dependencies can be Hex packages: 20 | # 21 | # {:mydep, "~> 0.3.0"} 22 | # 23 | # Or git/path repositories: 24 | # 25 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 26 | # 27 | # Type `mix help deps` for more examples and options 28 | defp deps do 29 | [{:feeder, "~> 2.2"}, 30 | {:ex_doc, ">= 0.0.0", only: :dev}] 31 | end 32 | 33 | defp package do 34 | [maintainers: ["Manuel Kallenbach"], 35 | licenses: ["MIT"], 36 | description: "RSS feed parser. Simple wrapper for feeder.", 37 | links: %{"GitHub" => "https://github.com/manukall/feeder_ex", 38 | "Feeder" => "https://github.com/michaelnisi/feeder"}] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []}, 2 | "ex_doc": {:hex, :ex_doc, "0.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, 3 | "feeder": {:hex, :feeder, "2.2.1", "9b1236d32cf971a049968b4c3955fa4808bd132b347af6e30a06fc2093751796", [:make], []}} 4 | -------------------------------------------------------------------------------- /test/feeder_ex/parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FeederEx.ParserExTest do 2 | use ExUnit.Case 3 | 4 | test "feed event" do 5 | event_data = { 6 | :feed, 7 | "the author", 8 | "the id", 9 | "the image", 10 | "the language", 11 | "the link", 12 | "the subtitle", 13 | "the summary", 14 | "the title", 15 | "the updated date", 16 | "the feed url" 17 | } 18 | {feed, :entries} = FeederEx.Parser.event({:feed, event_data}, {nil, :entries}) 19 | 20 | assert feed.author == "the author" 21 | assert feed.id == "the id" 22 | assert feed.image == "the image" 23 | assert feed.language == "the language" 24 | assert feed.link == "the link" 25 | assert feed.subtitle == "the subtitle" 26 | assert feed.summary == "the summary" 27 | assert feed.title == "the title" 28 | assert feed.updated == "the updated date" 29 | assert feed.url == "the feed url" 30 | end 31 | 32 | test "entry event without enclosure" do 33 | event_data = { 34 | :entry, 35 | "the author", 36 | ["News"], 37 | "the duration", 38 | :undefined, 39 | "the id", 40 | "the image", 41 | "the link", 42 | "the subtitle", 43 | "the summary", 44 | "the title", 45 | "the updated date" 46 | } 47 | entries = [:entry_1, :entry_2] 48 | {:feed, [entry, :entry_1, :entry_2]} = 49 | FeederEx.Parser.event({:entry, event_data}, {:feed, entries}) 50 | 51 | assert entry.author == "the author" 52 | assert entry.duration == "the duration" 53 | assert entry.enclosure == nil 54 | assert entry.id == "the id" 55 | assert entry.image == "the image" 56 | assert entry.link == "the link" 57 | assert entry.subtitle == "the subtitle" 58 | assert entry.summary == "the summary" 59 | assert entry.title == "the title" 60 | assert entry.updated == "the updated date" 61 | end 62 | 63 | test "entry event with enclosure" do 64 | event_data = { 65 | :entry, 66 | "the author", 67 | ["News"], 68 | "the duration", 69 | {:enclosure, "http://www.example.com/enclosure.mp3", "123456", "audio/mpeg"}, 70 | "the id", 71 | "the image", 72 | "the link", 73 | "the subtitle", 74 | "the summary", 75 | "the title", 76 | "the updated date" 77 | } 78 | {:feed, [entry]} = 79 | FeederEx.Parser.event({:entry, event_data}, {:feed, []}) 80 | 81 | assert entry.enclosure == %FeederEx.Enclosure{url: "http://www.example.com/enclosure.mp3", 82 | size: "123456", type: "audio/mpeg"} 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /test/feeder_ex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FeederExTest do 2 | use ExUnit.Case 3 | 4 | @sample_file "test/sample-rss-2.xml" 5 | 6 | test "parsing a file" do 7 | {:ok, feed, _} = FeederEx.parse_file(@sample_file) 8 | assert feed.author == nil 9 | assert feed.id == nil 10 | assert feed.image == nil 11 | assert feed.link == "http://liftoff.msfc.nasa.gov/" 12 | assert feed.language == "en-us" 13 | assert feed.subtitle == nil 14 | assert feed.summary == "Liftoff to Space Exploration." 15 | assert feed.title == "Liftoff News" 16 | assert feed.updated == "Tue, 10 Jun 2003 04:00:00 GMT" 17 | assert feed.url == "http://liftoff.msfc.nasa.gov/feed/" 18 | assert length(feed.entries) == 4 19 | 20 | [entry | _] = feed.entries 21 | assert entry.author == nil 22 | assert entry.duration == nil 23 | assert entry.enclosure == nil 24 | assert entry.categories == ["Current Events", "News"] 25 | assert entry.id == "http://liftoff.msfc.nasa.gov/2003/06/03.html#item573" 26 | assert entry.image == nil 27 | assert entry.link == "http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp" 28 | assert entry.subtitle == nil 29 | assert entry.summary == "How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's Star City." 30 | assert entry.title == "Star City" 31 | assert entry.updated == "Tue, 03 Jun 2003 09:39:21 GMT" 32 | 33 | end 34 | 35 | test "parsing a binary" do 36 | bin_feed = File.read! @sample_file 37 | {:ok, feed, _} = FeederEx.parse(bin_feed) 38 | assert feed.title == "Liftoff News" 39 | end 40 | 41 | test "parse! returns just the parsed results on success" do 42 | parsed_feed = FeederEx.parse!(File.read!(@sample_file)) 43 | assert parsed_feed.title == "Liftoff News" 44 | end 45 | 46 | test "parse! raises an error on failure" do 47 | assert_raise MatchError, fn -> 48 | FeederEx.parse!("") 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/sample-rss-2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Liftoff News 5 | http://liftoff.msfc.nasa.gov/ 6 | http://liftoff.msfc.nasa.gov/feed/ 7 | Liftoff to Space Exploration. 8 | en-us 9 | Tue, 10 Jun 2003 04:00:00 GMT 10 | Tue, 10 Jun 2003 09:41:01 GMT 11 | http://blogs.law.harvard.edu/tech/rss 12 | Weblog Editor 2.0 13 | editor@example.com 14 | webmaster@example.com 15 | 16 | Star City 17 | 18 | 19 | http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp 20 | How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>. 21 | Tue, 03 Jun 2003 09:39:21 GMT 22 | http://liftoff.msfc.nasa.gov/2003/06/03.html#item573 23 | 24 | 25 | 26 | Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm">partial eclipse of the Sun</a> on Saturday, May 31st. 27 | Fri, 30 May 2003 11:06:42 GMT 28 | http://liftoff.msfc.nasa.gov/2003/05/30.html#item572 29 | 30 | 31 | The Engine That Does More 32 | http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp 33 | Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that. 34 | Tue, 27 May 2003 08:37:32 GMT 35 | http://liftoff.msfc.nasa.gov/2003/05/27.html#item571 36 | 37 | 38 | Astronauts' Dirty Laundry 39 | 40 | http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp 41 | Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options. 42 | Tue, 20 May 2003 08:56:02 GMT 43 | http://liftoff.msfc.nasa.gov/2003/05/20.html#item570 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------