Skip to content

Add normalized RSS and Atom entries - #65

Merged
cardmagic merged 3 commits into
masterfrom
feature/normalized-entries
Sep 14, 2026
Merged

cardmagic merged 3 commits into
masterfrom
feature/normalized-entries

Conversation

@cardmagic

@cardmagic cardmagic commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner

Applications currently need format-specific branches to extract an article URL, content, dates, categories, and attachments. Add feed.normalized_entries, an optional immutable RSS/Atom view, so one importer or digest can consume those fields. Keep existing raw items, custom tags, serialization, merge/diff identity, and date helpers compatible.

  • Prefer Atom HTML alternate links; resolve URL references with scoped xml:base and a supplied or fetched source URL. Fetch retains the final URL and resolves relative redirects while preserving conditional GET behavior.
  • Keep publication/update times and full content/summary representations separate. Preserve IDs as opaque strings. Expose original item snapshots, entry XML, field sources, and issues for invalid or unsupported values.
  • Collect categories with raw labels/schemes and associated attachment records, including multiple enclosures and Media RSS content. Support local full-text and explicit keyword mappings, plus Atom author inheritance.
  • Reuse the existing XML tokenizer, add a format-independent value object, and include the new files in both packaging paths. Document the mapping contract and provide a runnable digest example.

The same consumer works with RSS or Atom XML:

require "simple-rss"

feed = SimpleRSS.parse(
  File.read("feed.xml"),
  source_url: "https://example.com/feed.xml"
)

feed.normalized_entries.each do |entry|
  article = {
    identifier: entry.identifier,
    title: entry.title,
    url: entry.url,
    published_at: entry.published_at,
    updated_at: entry.updated_at,
    content_html: entry.content_html,
    content_text: entry.content_text,
    summary: entry.summary,
    categories: entry.categories
  }

  p article

  entry.attachments.each do |attachment|
    p attachment.slice(
      :url, :media_type, :size_in_bytes, :duration_in_seconds
    )
  end
end

For an Atom entry with a self/API link and an HTML alternate, entry.url selects the HTML alternate. RSS item links map to the same field. Relative references use the applicable base URL, publication and update times stay separate, and summaries remain separate from full content.

Custom full-text and keyword mappings apply to one call without modifying global tags:

entries = feed.normalized_entries(mappings: {
  content_html: "full-text",
  content_text: "{urn:example:content}plain",
  categories: [
    { tag: "dc:subject", separator: ";" },
    { tag: "media:keywords", separator: "," }
  ]
})

entry = entries.first
entry.raw
entry.raw_xml
entry.field_sources
entry.category_details
entry.issues

raw is an immutable snapshot of the existing item hash; raw_xml retains unconfigured extensions. field_sources identifies the selected source tags, category_details retains labels/schemes and duplicates, and issues exposes invalid or unsupported values.

Fetching supplies the final response URL automatically, including after redirects:

feed = SimpleRSS.fetch("https://example.com/feed.xml")
entries = feed&.normalized_entries

The safe navigation preserves the existing nil result for HTTP 304. The runnable digest example produces identical output for the equivalent RSS and Atom fixtures:

ruby -Ilib examples/digest.rb test/data/normalized_rss.xml
ruby -Ilib examples/digest.rb test/data/normalized_atom.xml

Validation: the initial public API tests failed before implementation. Additional red/green regressions cover XML comments leaking into text, Atom author inheritance, prefixed XHTML, content media-type parameters, Media RSS group boundaries, and invalid attachment-duration precedence. The complete Ruby 3.4.8 suite passes with 164 tests and 502 assertions (3 existing network-test omissions), alongside RuboCop, RBS generation/validation, and Steep. Local HTTP fixtures cover relative redirects, final URL resolution, conditional 304 responses, source-URL precedence with shared options, and request counts. Equivalent RSS/Atom fixtures produce identical digest output; the built gem loads and normalizes successfully, and the Rake package includes all new library/example files. Article URLs and category/subject collections match independent namespace-aware XML extraction for all three entries in the archived fork fixture.

Normalization is tolerant extraction, not standards validation or HTML sanitization. Relative values without a usable base and unsupported content remain inspectable; normalization does not fetch external content. JSON Feed parsing and website discovery remain separate issues (#60 and #61).

Review note: Hound uses defaults that conflict with this repository's .rubocop.yml: the project requires double quotes, allows 160 columns and larger parser methods, disables class-documentation/magic-comment/expanded-class-style rules, and excludes Rakefile. All reported metric values are within the configured limits. The configured CI lint check passes; those conflicting Hound threads are resolved without changing project conventions.

Closes #59.

Give RSS and Atom consumers one optional entry interface without changing
raw items, serialization, or global tag configuration. Preserve source
metadata and report normalization limits instead of inventing values.

Track XML namespace and base scopes, keep attachment metadata associated,
and support per-call content and keyword mappings. Retain the final fetch
URL and resolve relative redirects so relative feed links have a base.

Add equivalent format fixtures, boundary and HTTP regressions, a digest
example, mapping documentation, and an Unreleased changelog entry.

Closes #59.
Comment thread Rakefile
Comment thread examples/digest.rb
Comment thread examples/digest.rb
Comment thread examples/digest.rb
Comment thread examples/digest.rb
Comment thread lib/simple-rss/entry_normalizer.rb
Comment thread lib/simple-rss/entry_normalizer.rb
Comment thread lib/simple-rss/entry_normalizer.rb Outdated
Comment thread lib/simple-rss/entry_normalizer.rb Outdated
Comment thread lib/simple-rss/entry_normalizer.rb Outdated
Only collect Media RSS content from Media RSS groups. Nested RSS or Atom
enclosures are not direct entry metadata and must not add attachments.

Preserve an invalid attachment-specific duration as nil with its issue,
rather than replacing it with a broader item-level iTunes duration.

Add failing regressions for both cases and verify the complete suite.
Comment thread lib/simple-rss/normalized_entry.rb
Comment thread lib/simple-rss/normalized_entry.rb
Comment thread lib/simple-rss/normalized_entry.rb
Comment thread lib/simple-rss/normalized_entry.rb
Comment thread lib/simple-rss/normalized_entry.rb
Comment thread lib/simple-rss/entry_normalizer.rb Outdated
Comment thread lib/simple-rss/entry_normalizer.rb Outdated
Comment thread lib/simple-rss/entry_normalizer.rb
Comment thread lib/simple-rss/entry_normalizer.rb Outdated
Comment thread lib/simple-rss/entry_normalizer.rb Outdated
@cardmagic

Copy link
Copy Markdown
Owner Author

@greptileai Please review the current head f1b0216 and provide an updated confidence score. The follow-up commit adds red/green regressions and fixes Media RSS group boundaries and invalid attachment-duration precedence. The full suite passes with 163 tests and 493 assertions, plus RuboCop, RBS validation, and Steep. Please assess the current commit, including the updated tests.

@greptile-apps

greptile-apps Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Greptile Summary

Adds an immutable, format-independent normalized view over RSS and Atom entries while preserving the existing raw parsing and serialization interfaces.

  • Normalizes links, dates, content, summaries, categories, attachments, and inherited authors.
  • Resolves relative references through scoped xml:base values and final fetched URLs.
  • Preserves raw item snapshots, entry XML, source metadata, and tolerant-extraction issues.
  • Includes package configuration, documentation, digest example, and extensive normalization/fetch regressions.
  • The changes since the previous review fix both earlier findings: fetched feeds now retain the final response URL regardless of caller options, and the targeted conditional branches now use guard clauses.

Confidence Score: 5/5

The PR appears safe to merge; both previous findings are fully addressed and no actionable new issue remains.

Fetch parsing now merges the final response URL after caller options, so stale or nil source_url values cannot override it. Content handling, duration validation, and Atom author fallback now use guard clauses and focused helpers, fully resolving the earlier conditional-structure concern.

Important Files Changed

Filename Overview
lib/simple-rss.rb Captures entry source context, exposes normalized entries, and retains the final fetched URL across redirects and caller options.
lib/simple-rss/entry_normalizer.rb Implements tolerant RSS/Atom normalization with flattened content, duration, and author control flow.
lib/simple-rss/normalized_entry.rb Defines the immutable normalized value object and recursively freezes copied values.
lib/simple-rss/xml_element.rb Provides shared direct-child tokenization, namespace/base tracking, text extraction, and XHTML handling.
test/base/normalized_entries_test.rb Covers normalized field contracts, attachment boundaries, invalid values, inheritance, and immutable snapshots.
test/base/normalized_fetch_test.rb Verifies redirects, final source URL precedence, conditional responses, and option immutability.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    XML[RSS or Atom XML] --> Parser[Existing SimpleRSS parser]
    Parser --> Raw[Raw items and serialization]
    Parser --> Context[Entry XML and scoped namespace/base context]
    Raw --> Normalizer[EntryNormalizer]
    Context --> Normalizer
    URL[Supplied or final fetched source URL] --> Normalizer
    Mapping[Per-call mappings] --> Normalizer
    Normalizer --> Entry[Immutable NormalizedEntry]
Loading

Reviews (3): Last reviewed commit: "fix: preserve the fetched source URL" | Re-trigger Greptile

Comment thread lib/simple-rss.rb Outdated
Comment thread lib/simple-rss/entry_normalizer.rb Outdated
Make the final HTTP response URL authoritative even when shared parse
options include a stale or nil source_url. Callers can still override the
base explicitly when requesting a normalized view. Add a local HTTP
regression proving both options preserve the final URL without mutation.

Flatten content handling, duration validation, and author inheritance with
guard clauses and focused helpers to follow the repository style rules.
@cardmagic

Copy link
Copy Markdown
Owner Author

@greptileai Please re-review the current head 80e5146 and update the confidence score. Both findings are addressed: fetch now always retains the final response URL, with a failing-then-passing local HTTP regression for stale and nil source_url options; content handling, duration validation, and author fallback now use guard clauses and focused helpers. The attachment boundary fixes remain included. Full suite: 164 tests, 502 assertions, 3 existing network omissions. RuboCop, RBS generation/validation, and Steep pass.

Comment thread lib/simple-rss/entry_normalizer.rb Outdated
Comment thread lib/simple-rss/entry_normalizer.rb Outdated
@cardmagic
cardmagic merged commit 99e0882 into master Sep 14, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add normalized feed entries

2 participants