From d12c5125b5999cb8d50f92bd4c46d583e49e837f Mon Sep 17 00:00:00 2001 From: Lucas Carlson Date: Mon, 29 Dec 2025 01:16:13 -0800 Subject: [PATCH 1/3] feat: add Enumerable support and convenience accessors Make SimpleRSS more Ruby-idiomatic by including Enumerable module and adding convenience methods for iterating and accessing feed items. New methods: - each: iterate over items, enables map/select/first/count etc. - []: access items by index (rss[0].title) - latest(n): get n most recent items sorted by pubDate/updated Closes #42 --- lib/simple-rss.rb | 27 ++++++++++ test/base/enumerable_test.rb | 101 +++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 test/base/enumerable_test.rb diff --git a/lib/simple-rss.rb b/lib/simple-rss.rb index 6909bee..d5cbd1b 100644 --- a/lib/simple-rss.rb +++ b/lib/simple-rss.rb @@ -4,6 +4,8 @@ require "time" class SimpleRSS + include Enumerable + VERSION = "2.0.0".freeze # @rbs @items: Array[Hash[Symbol, untyped]] @@ -64,6 +66,31 @@ def channel end alias feed channel + # Iterate over all items in the feed + # + # @rbs () { (Hash[Symbol, untyped]) -> void } -> self + # | () -> Enumerator[Hash[Symbol, untyped], self] + def each(&block) + return enum_for(:each) unless block + + items.each(&block) + self + end + + # Access an item by index + # + # @rbs (Integer) -> Hash[Symbol, untyped]? + def [](index) + items[index] + end + + # Get the n most recent items, sorted by date + # + # @rbs (?Integer) -> Array[Hash[Symbol, untyped]] + def latest(count = 10) + items.sort_by { |item| item[:pubDate] || item[:updated] || Time.at(0) }.reverse.first(count) + end + # @rbs (?Hash[Symbol, untyped]) -> Hash[Symbol, untyped] def as_json(_options = {}) hash = {} #: Hash[Symbol, untyped] diff --git a/test/base/enumerable_test.rb b/test/base/enumerable_test.rb new file mode 100644 index 0000000..c3fed6c --- /dev/null +++ b/test/base/enumerable_test.rb @@ -0,0 +1,101 @@ +require "test_helper" + +class EnumerableTest < Test::Unit::TestCase + def setup + @rss20 = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/rss20.xml") + @atom = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/atom.xml") + end + + def test_includes_enumerable + assert_includes SimpleRSS.included_modules, Enumerable + end + + def test_each_iterates_over_items + titles = @rss20.map { |item| item[:title] } + assert_equal @rss20.items.map { |i| i[:title] }, titles + end + + def test_each_returns_enumerator_without_block + enumerator = @rss20.each + assert_kind_of Enumerator, enumerator + assert_equal @rss20.items.size, enumerator.count + end + + def test_each_returns_self_with_block + count = 0 + result = @rss20.each { |_item| count += 1 } + assert_equal @rss20, result + assert_equal @rss20.items.size, count + end + + def test_enumerable_map + titles = @rss20.map { |item| item[:title] } + assert_equal @rss20.items.map { |i| i[:title] }, titles + end + + def test_enumerable_select + items_with_link = @rss20.select { |item| item[:link] } + assert_equal @rss20.items.select { |i| i[:link] }, items_with_link + end + + def test_enumerable_first + assert_equal @rss20.items.first, @rss20.first + assert_equal @rss20.items.first(3), @rss20.first(3) + end + + def test_enumerable_count + assert_equal @rss20.items.size, @rss20.count + end + + def test_index_accessor + assert_equal @rss20.items[0], @rss20[0] + assert_equal @rss20.items[5], @rss20[5] + assert_equal @rss20.items[-1], @rss20[-1] + end + + def test_index_accessor_out_of_bounds + assert_nil @rss20[100] + end + + def test_latest_returns_sorted_items + latest = @rss20.latest(3) + assert_equal 3, latest.size + + dates = latest.map { |item| item[:pubDate] } + assert_equal dates, dates.sort.reverse + end + + def test_latest_default_count + latest = @rss20.latest + assert latest.size <= 10 + end + + def test_latest_with_atom_uses_updated + latest = @atom.latest(1) + assert_equal 1, latest.size + end + + def test_latest_handles_missing_dates + rss_with_missing_dates = SimpleRSS.parse <<~RSS + + + + Test Feed + http://example.com + + No Date + + + Has Date + Wed, 24 Aug 2005 13:33:34 GMT + + + + RSS + + latest = rss_with_missing_dates.latest(2) + assert_equal 2, latest.size + assert_equal "Has Date", latest.first[:title] + assert_equal "No Date", latest.last[:title] + end +end From 139adae51eaccaf7406ddd9f46f60d85bed0eccb Mon Sep 17 00:00:00 2001 From: Lucas Carlson Date: Mon, 29 Dec 2025 01:21:07 -0800 Subject: [PATCH 2/3] docs: add Enumerable support to README Document new iteration methods in What's New section, add usage examples, and update API reference table. --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 0d8c409..6f10891 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ Version 2.0 is a major update with powerful new capabilities: - **Modern Ruby** - Full compatibility with Ruby 3.1 through 4.0, with RBS type annotations and Steep type checking. +- **Enumerable Support** - Iterate feeds naturally with `each`, `map`, `select`, and all Enumerable methods. Access items by index with `rss[0]` and get the latest items sorted by date with `latest(n)`. + ## Installation Add to your Gemfile: @@ -136,6 +138,30 @@ item.pubDate.class # => Time item.pubDate.year # => 2024 ``` +### Iterating with Enumerable + +SimpleRSS includes `Enumerable`, so you can iterate feeds naturally: + +```ruby +feed = SimpleRSS.parse(xml) + +# Iterate over items +feed.each { |item| puts item.title } + +# Use any Enumerable method +titles = feed.map { |item| item.title } +tech_posts = feed.select { |item| item.category == "tech" } +first_five = feed.first(5) +total = feed.count + +# Access items by index +feed[0].title # first item +feed[-1].title # last item + +# Get the n most recent items (sorted by pubDate or updated) +feed.latest(10) +``` + ### JSON Serialization ```ruby @@ -244,6 +270,9 @@ Fetch and parse a feed from a URL. |--------|-------------| | `#channel` / `#feed` | Returns self (for RSS/Atom style access) | | `#items` / `#entries` | Array of parsed items | +| `#each` | Iterate over items (includes `Enumerable`) | +| `#[](index)` | Access item by index | +| `#latest(n = 10)` | Get n most recent items by date | | `#to_json` | JSON string representation | | `#to_hash` / `#as_json` | Hash representation | | `#to_xml(format:)` | XML string (`:rss2` or `:atom`) | From 1e98fb9043d19b51fc44675bb99c9bced3f1601f Mon Sep 17 00:00:00 2001 From: Lucas Carlson Date: Mon, 29 Dec 2025 01:26:26 -0800 Subject: [PATCH 3/3] fix: resolve rubocop and type checking issues - Bump class length limit from 360 to 370 in rubocop config - Add proper RBS type parameter for Enumerable[Hash[Symbol, untyped]] --- .rubocop.yml | 2 +- lib/simple-rss.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index dca3f7f..fbc2972 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -65,7 +65,7 @@ Metrics/PerceivedComplexity: # Class length for SimpleRSS (single-file library) Metrics/ClassLength: - Max: 360 + Max: 370 # Frozen string literal is optional for this gem Style/FrozenStringLiteralComment: diff --git a/lib/simple-rss.rb b/lib/simple-rss.rb index d5cbd1b..c94819f 100644 --- a/lib/simple-rss.rb +++ b/lib/simple-rss.rb @@ -4,8 +4,12 @@ require "time" class SimpleRSS + # @rbs skip include Enumerable + # @rbs! + # include Enumerable[Hash[Symbol, untyped]] + VERSION = "2.0.0".freeze # @rbs @items: Array[Hash[Symbol, untyped]]