feed-parser 2.0.0

A simple RSS 1.0 / RSS 2.0 / Atom feed parser
Documentation
**English** | [日本語]development.ja.md

# Development

## Commands

| Command | Description |
|--|---|
|`cargo make ci` | Run everything CI runs. Use this before pushing. |
|`cargo make test` | Run unit and integration tests using nextest. |
|`cargo make doctest` | Run doctests (nextest does not run these). |
|`cargo make fmt` | Format the source. |
|`cargo make fmt-check` | Verify formatting without rewriting files. |
|`cargo make lint` | Run clippy, treating warnings as errors. |

These are the exact commands CI runs; all three must pass before pushing.

| Command | Description |
|--|---|
|`cargo fmt --all --check` | Verify formatting. |
|`cargo clippy --all-targets --all-features -- -D warnings` | Lint, treating warnings as errors. |
|`cargo test --all-features` | Run unit tests, integration tests and doctests. |

## Layout

| Path | Contents |
|--|---|
|`src/parsers/{rss1,rss2,atom}/` | One parser per feed format, each with its unit tests. |
|`src/parsers/internal.rs` | Helpers shared by the three parsers: HTML escaping, text-run buffering, entry deserialization. |
|`src/parsers/errors.rs` | `ParseError` and `ParseResult`. |
|`tests/error_handling.rs` | Malformed input must error rather than panic. |
|`tests/text_content.rs` | Text, entities, CDATA and whitespace must survive parsing. |

## How the parsers work

Each parser makes two passes.

1. **Escape.** Feeds routinely embed raw HTML in `<title>`, `<description>`,
   `<summary>` and `<content>`, which makes the document ill-formed XML. A regex per
   element escapes that content first, so the reader sees text.
2. **Rewrite and deserialize.** The reader walks the document and a writer re-emits
   each entry with normalized element names (`dc:creator` becomes `creator`,
   `pubDate` becomes `publish_date`), and the result is deserialized into `Feed`.

Two details are easy to get wrong when touching the event loop:

- **Never drop an event kind silently.** quick-xml reports entity references as
  `Event::GeneralRef`, separately from the surrounding text. A wildcard match arm that
  ignores it deletes every escaped character in the entry, and the existing tests will
  not notice — this happened during the 0.37 to 0.41 upgrade.
- **Do not use the reader's `trim_text`.** Because character data is split at entity
  references, trimming each fragment eats the whitespace next to an escaped character.
  `TextRun` buffers a run and trims only its outer edges instead.

When changing this code, compare the output against the previous release on inputs that
mix text, entities, CDATA and nested markup, rather than trusting the unit tests alone.

## Minimum supported Rust version

The crate declares `rust-version = "1.85"`, the floor imposed by edition 2024. The
`msrv` CI job builds against exactly that toolchain on every push.

### Policy: the MSRV stays put

**The MSRV is pinned to whatever the current edition requires, and is not raised just
because time has passed.** Chasing stable would mean a bump, a CI edit and a release
note every few months, none of which helps anyone; leaving the floor alone means it
quietly grows more compatible instead.

Raise it only when one of these is true:

1. The crate moves to a newer edition.
2. A language feature or `std` API is worth the cost — an explicit judgement call, not
   a default. Precedent: let-chains (Rust 1.88) were rejected in
   `src/parsers/internal.rs` in favour of nested `if let`, because saving four lines did
   not justify three releases of MSRV.
3. A dependency raises its own MSRV above ours and cannot be pinned back. The effective
   MSRV is the maximum across this crate and every dependency, so check `rust-version`
   before adding one. As of feed-parser 2.0.0 the highest is quick-xml at 1.79,
   comfortably below our floor.

### A bump is a minor release, not a major one

Because `rust-version` is declared and edition 2024 selects the MSRV-aware resolver
(v3), a user on an older toolchain resolves to the last release that still supports them
rather than hitting a build failure. An MSRV bump therefore degrades gracefully and does
not need a major version. Note it in the changelog so the change is discoverable.

### When you do change it

Update these together — they are the declaration and its proof, and are meaningless
apart:

- `rust-version` in `Cargo.toml`
- the toolchain and job name in the `msrv` job of `.github/workflows/unit_test.yml`

Never take "it compiles on my machine" as evidence. A current rustc accepts old and new
features alike, so it cannot detect an MSRV violation; only the `msrv` job can. To check
locally: `rustup toolchain install 1.85 && cargo +1.85 build --all-features`.

## Releasing

1. Bump `version` in `Cargo.toml` and add an entry to the [changelog]changelog.md.
2. Push a `v*` tag; `.github/workflows/release.yml` runs the tests and publishes. This
   needs a `CARGO_REGISTRY_TOKEN` repository secret.