Skip to content

Surface pagination-aware repository errors (backend-4.2.2)#356

Open
leynos wants to merge 6 commits into
mainfrom
backend-4-2-2-surface-pagination-aware-errors
Open

Surface pagination-aware repository errors (backend-4.2.2)#356
leynos wants to merge 6 commits into
mainfrom
backend-4-2-2-surface-pagination-aware-errors

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented May 20, 2026

Summary

  • Implements backend roadmap item 4.2.2 by surfacing pagination-aware repository errors through the user repository port and HTTP-safe error mapping.
  • Distinguishes invalid cursor payloads from unsupported cursor directions, returning HTTP 400 Bad Request with stable details.code values of invalid_cursor and unsupported_direction.
  • Adds rstest unit coverage and rstest-bdd behavioural coverage for the users list pagination unhappy path while preserving existing happy paths.
  • Updates the ExecPlan, pagination design, backend architecture, developers guide, user guide, documentation index, and roadmap entry.

Review Walkthrough

  • Start with docs/execplans/backend-4-2-2-surface-pagination-aware-errors.md for the approved plan, decisions, progress, and validation record.
  • Review backend/src/domain/ports/user_repository.rs and backend/src/outbound/persistence/user_persistence_error_mapping.rs for the port-level error vocabulary and HTTP mapping.
  • Review backend/crates/pagination/src/cursor/mod.rs and backend/crates/pagination/src/cursor/tests.rs for cursor decode classification.
  • Review backend/tests/features/users_list_pagination.feature and backend/tests/users_list_pagination_bdd.rs for externally observable behaviour.

Validation

  • make check-fmt
  • make lint
  • make test
  • make fmt
  • make markdownlint
  • coderabbit review --agent implementation milestone: 0 findings
  • coderabbit review --agent documentation and roadmap closure: 0 findings

References

Summary by Sourcery

Surface pagination-aware errors for users list pagination and document the new error contract.

New Features:

  • Introduce a pagination-aware user repository error type and wrap it in user persistence errors so adapters can surface semantic pagination failures.
  • Extend the shared pagination crate with explicit unsupported-direction cursor error classification and direction-aware cursor encoding/decoding.
  • Add a user-facing server users guide documenting users list pagination behaviour and error semantics.

Enhancements:

  • Map pagination-originated repository and cursor errors to HTTP 400 invalid-request responses with stable detail codes for invalid cursors and unsupported directions.
  • Refine users list pagination HTTP parsing to distinguish malformed cursor tokens from unsupported cursor directions while preserving existing success paths.
  • Expand behavioural and unit test coverage for users cursor decoding, repository error mapping, and users list pagination unhappy paths.
  • Update pagination design, backend architecture, developers guide, documentation index, and roadmap to reflect the pagination-aware error contract.

Documentation:

  • Document the users list pagination response and error behaviour in a new Wildside server users guide and update existing design/architecture docs with pagination error mapping rules.

Tests:

  • Add rstest unit tests for cursor encoding/decoding, pagination error mapping, and user repository error constructors, plus rstest-bdd scenarios for invalid and unsupported users cursors.

Draft the pre-implementation plan for backend roadmap item 4.2.2.
The plan captures the repository-layer pagination error gap, the
hexagonal boundary constraints, validation strategy, and approval gate
before any implementation work proceeds.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Review Change Stack

Implementation of Pagination-Aware Repository Error Surfacing

Implements backend roadmap item 4.2.2 to surface pagination-aware repository errors as HTTP-safe 400 Bad Request responses, distinguishing invalid cursor payloads (invalid_cursor) from unsupported cursor directions (unsupported_direction) through a stable error envelope contract.

Core Changes

Domain Layer

  • Introduces UserPaginationError enum in the user repository port, covering cursor validation failures and unsupported direction handling.
  • Extends UserPersistenceError with a new Pagination variant to wrap pagination-specific errors from the repository boundary.

Cursor Implementation

  • Restructures cursor module from cursor.rs to cursor/mod.rs with comprehensive error classification:
    • CursorError::InvalidBase64 and CursorError::Deserialize for malformed tokens
    • CursorError::UnsupportedDirection for rejected direction values
  • Maintains backward compatibility through serde(default) for direction field (defaults to Next)

HTTP Error Mapping

  • Implements map_user_pagination_error in user_persistence_error_mapping.rs to convert pagination failures to HTTP 400 Bad Request with details identifying the cursor field and appropriate error codes.
  • Updates cursor decoding in users_pagination.rs with map_cursor_error to classify distinct failure modes during inbound parsing.

Test Coverage

Adds comprehensive rstest and rstest-bdd coverage:

  • Unit tests for cursor encode/decode behaviour, direction semantics, and error cases
  • BDD scenario for rejecting unsupported cursor directions in the users list pagination API
  • Extended test fixtures in diesel_users_query.rs to verify pagination error mapping

Documentation

  • New ExecPlan: docs/execplans/backend-4-2-2-surface-pagination-aware-errors.md documents the implementation strategy, constraints, risks, and decisions
  • Keyset Pagination Design: Adds "Pagination error mapping" section specifying HTTP status codes and error codes for each failure category
  • Users Guide: New operational documentation for GET /api/v1/users pagination including error responses and client guidance
  • Backend Architecture: Documents the pagination-aware error contract and domain-to-HTTP mapping requirements
  • Developers Guide: Adds conventions for pagination-aware port errors and BDD cursor fixtures
  • Backend Roadmap: Marks item 4.2.2 as completed

Validation

  • All validation gates passed: make check-fmt, make lint, make test, make fmt, make markdownlint
  • CodeRabbit review (--agent) reported zero findings for both implementation and documentation/roadmap closure

Walkthrough

Refactors the pagination cursor implementation to classify direction-related failures separately from malformed tokens, then propagates these distinctions through domain error types, inbound HTTP parsing, and outbound persistence mapping to produce HTTP 400 Bad Request responses with cursor-specific error codes.

Changes

Pagination-aware error handling

Layer / File(s) Summary
Pagination cursor refactoring
backend/crates/pagination/src/cursor/mod.rs, backend/crates/pagination/src/cursor/tests.rs, backend/crates/pagination/src/lib.rs
Restructures cursor types from cursor.rs into cursor/mod.rs, introducing Direction::Next/Direction::Prev with JSON serde defaults, Cursor<Key> encoding to base64url JSON payloads, and CursorError variants for serialisation, base64url decoding, deserialisation, and unsupported dir values; comprehensive test coverage validates round-tripping, direction defaults, legacy cursor compatibility, and error classification.
Domain pagination error types
backend/src/domain/ports/user_repository.rs, backend/src/domain/ports/mod.rs
Introduces UserPaginationError enum wrapping cursor-format and unsupported-direction failures; extends UserPersistenceError with Pagination variant to preserve pagination semantics across the domain boundary; includes test assertions for error message formatting and variant construction.
Inbound HTTP cursor error mapping
backend/src/inbound/http/users_pagination.rs
Replaces generic invalid_cursor_error() closure with map_cursor_error() function that dispatches CursorError variants to either unsupported_direction_error() or invalid_cursor_error(), ensuring the parsing boundary correctly classifies cursor failures.
Outbound persistence error mapping
backend/src/outbound/persistence/user_persistence_error_mapping.rs, backend/src/outbound/persistence/diesel_users_query.rs
Adds map_user_pagination_error() that translates UserPaginationError variants to HTTP 400 Invalid Request responses with JSON details (field + code); extends test stubs to cover InvalidCursor and UnsupportedDirection failure cases with rstest parameterised assertions.
BDD test coverage
backend/tests/features/users_list_pagination.feature, backend/tests/users_list_pagination_bdd.rs
Adds feature scenario and BDD step/assertion functions (#[when], #[then]) verifying that unsupported cursor directions trigger HTTP 400 with unsupported_direction details.
Documentation and roadmap
docs/users-guide.md, docs/wildside-backend-architecture.md, docs/keyset-pagination-design.md, docs/developers-guide.md, docs/execplans/backend-4-2-2-surface-pagination-aware-errors.md, docs/backend-roadmap.md, docs/contents.md
Creates users guide for pagination API contract, documents pagination error mapping and architecture constraints, defines pagination-aware port error conventions, records ExecPlan with milestones/risks/decisions, marks roadmap item complete, and indexes new documentation.

Possibly related PRs

  • leynos/wildside#329: Directly overlaps with cursor implementation replacement, moving Direction/Cursor from cursor.rs into cursor/mod.rs.
  • leynos/wildside#327: Supersedes pagination crate's original Cursor<Key> and CursorError implementation by refactoring cursor module structure and encode/decode behaviour.
  • leynos/wildside#349: Connected through keyset users pagination: #349 consumes direction-aware pagination::Cursor whilst this PR refactors the cursor implementation and wires unsupported-direction error handling for the users endpoint.

Suggested labels

Roadmap

Poem

🌀 Cursors now speak direction with clarity—
Invalid formats and unsupported modes distinct.
From pagination crate through domain to HTTP,
Errors flow true; details arrive by design.
Domain boundaries hold; roadmap's checkpoint complete. ✨


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 6 warnings)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Cursor round-trip tests lack property-based invariants. Coding guidelines require property tests for invariants across input ranges. Tests use single example, missing padding edge cases. Add proptest suite verifying decode(encode(cursor)) == cursor across generated FixtureKey values and both Direction variants per review comment.
Unit Architecture ❌ Error Four unaddressed review comments: grammar issues in docs/users-guide.md (lines 8, 14–15, 38) and line-wrapping non-compliance in docs/wildside-backend-architecture.md line 2024 (exceeds 80 columns). Fix grammar: "users" → "user" in three locations (paginated users, previous users, users cursor). Rewrap architecture line 2024 before backtick. Add property-based cursor roundtrip tests.
User-Facing Documentation ⚠️ Warning Users guide documents two pagination errors but omits invalid_limit (implemented in code). Three grammar issues from review comments remain unfixed. Add invalid_limit error to documentation table. Fix grammar: change "users response/list/cursor" to "user-list response/list/cursor" in lines 8, 14–15, and 38.
Developer Documentation ⚠️ Warning Documentation is comprehensive (developers guide, roadmap, ExecPlan, architecture updated), but three unresolved grammar review comments in users-guide.md remain unfixed. Correct grammar in docs/users-guide.md: line 8 'users response'→'user-list', line 14 'users list'→'user-list', line 38 'users cursor'→'user cursor'.
Testing (Unit And Behavioural) ⚠️ Warning Unit tests lack property-based invariants. Review requests proptest suite verifying decode(encode(cursor)) == cursor across generated keys/directions to lock against edge cases. Add proptest suite to verify cursor round-trip invariant across generated FixtureKey values and both Direction variants (Next/Prev).
Testing (Property / Proof) ⚠️ Warning PR introduces encode/decode round-trip invariant for cursors without property-based tests using proptest; only example-based tests via rstest exist, leaving edge cases unverified. Implement property-based tests using proptest to verify cursor round-tripping across generated key types and directions, as suggested in prior review comments.
Testing (Compile-Time / Ui) ⚠️ Warning PR introduces cursor encoding producing base64url text output, verified only through round-trip semantics. Snapshot tests required for format stability but absent; insta used elsewhere in codebase. Add snapshot tests via insta to verify cursor encode() output format stability; consider property-based tests with proptest for comprehensive encode/decode invariant coverage.
Observability ⚠️ Warning Pagination errors captured without logging. CursorError::UnsupportedDirection field discarded via wildcard pattern; no tracing spans or metrics for pagination failures. Add debug logging to map_cursor_error and map_user_pagination_error capturing error details (direction value, message) with operation context.
✅ Passed checks (12 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Surface pagination-aware repository errors (backend-4.2.2)' directly references the roadmap item 4.2.2 and accurately describes the main change in the changeset.
Description check ✅ Passed The description provides detailed and comprehensive coverage of the changeset, including explicit sections for Summary, Review Walkthrough, Validation, and References, all of which are directly related to the pagination-aware error surfacing work.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Module-Level Documentation ✅ Passed All modules in this PR have module-level docstrings explaining purpose, utility, and relationships to other components.
Domain Architecture ✅ Passed Pagination crate remains transport-neutral; domain errors exclude HTTP concerns; error mapping isolated to adapter layer; inbound handler classifies cursor errors without leaking to domain.
Security And Privacy ✅ Passed Cursors are opaque base64url tokens with only keyset position data, error messages sanitised without implementation detail leakage, auth boundaries preserved, no hardcoded secrets, no injection risks.
Performance And Resource Use ✅ Passed No algorithmic regressions or unbounded resource growth. Cursor operations are bounded; error allocations off hot paths; no additional database queries introduced.
Concurrency And State ✅ Passed PR introduces only synchronous, stateless pagination error handling with pure functions and value-semantic error types; no async, shared state, locks, global state, or concurrency patterns introduced.
Architectural Complexity And Maintainability ✅ Passed Transport-neutral pagination crate justified by reuse across adapters; two-error design cleanly separates technical encoding from domain concerns; no circular dependencies; reduces net complexity.
Rust Compiler Lint Integrity ✅ Passed All public APIs are genuinely used; the two #[expect(dead_code)] attributes are properly justified for utoipa OpenAPI schema generation with specific documented reasons.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backend-4-2-2-surface-pagination-aware-errors

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 20, 2026

Reviewer's Guide

Implements roadmap item 4.2.2 by introducing semantic, pagination-aware errors at the user repository port, wiring them through outbound persistence to HTTP-safe error envelopes, enhancing the shared pagination crate to classify unsupported cursor directions, and adding BDD/unit coverage and documentation describing the new invalid_cursor/unsupported_direction behaviour while keeping existing happy paths intact.

Sequence diagram for users pagination error handling

sequenceDiagram
    actor Client
    participant UsersHttpAdapter as UsersHttpAdapter
    participant Cursor as Cursor
    participant UsersQueryAdapter as UsersQueryAdapter
    participant ErrorMapping as ErrorMapping

    rect rgb(230,230,250)
    Client->>UsersHttpAdapter: GET /api/v1/users?cursor=bad
    UsersHttpAdapter->>Cursor: Cursor::decode(cursor)
    Cursor-->>UsersHttpAdapter: CursorError::InvalidBase64
    UsersHttpAdapter->>UsersHttpAdapter: map_cursor_error(CursorError::InvalidBase64)
    UsersHttpAdapter-->>Client: 400 Error { code=invalid_request, details.code=invalid_cursor }
    end

    rect rgb(230,250,230)
    Client->>UsersHttpAdapter: GET /api/v1/users?cursor=unsupported-dir
    UsersHttpAdapter->>Cursor: Cursor::decode(cursor)
    Cursor-->>UsersHttpAdapter: CursorError::UnsupportedDirection
    UsersHttpAdapter->>UsersHttpAdapter: map_cursor_error(CursorError::UnsupportedDirection)
    UsersHttpAdapter-->>Client: 400 Error { code=invalid_request, details.code=unsupported_direction }
    end

    rect rgb(250,230,230)
    Client->>UsersHttpAdapter: GET /api/v1/users?cursor=valid
    UsersHttpAdapter->>UsersQueryAdapter: list_users_page(ListUsersPageRequest)
    UsersQueryAdapter-->>UsersHttpAdapter: Err(UserPersistenceError::Pagination(UserPaginationError::InvalidCursorFormat))
    UsersQueryAdapter->>ErrorMapping: map_user_persistence_error(UserPersistenceError::Pagination)
    ErrorMapping-->>UsersQueryAdapter: Error { code=invalid_request, details.code=invalid_cursor }
    UsersQueryAdapter-->>Client: 400 Error { code=invalid_request, details.code=invalid_cursor }
    end
Loading

File-Level Changes

Change Details Files
Introduce pagination-aware user repository port errors and map them to HTTP-safe invalid-request responses.
  • Extend the user repository port with a UserPaginationError type and a UserPersistenceError::Pagination variant that wraps it, preserving connection/query failures as distinct cases.
  • Export UserPaginationError from the ports module so adapters and tests can construct semantic pagination failures.
  • Map UserPersistenceError::Pagination in user_persistence_error_mapping.rs to Error::invalid_request with stable details.field="cursor" and details.code values of "invalid_cursor" and "unsupported_direction" depending on the wrapped error.
  • Add focused rstest unit tests validating that pagination-originated persistence failures map to ErrorCode::InvalidRequest with the expected message and details payloads.
  • Update diesel_users_query tests to include StubFailure variants for InvalidCursor and UnsupportedDirection and assert they map to InvalidRequest while other failures still map to ServiceUnavailable/InternalError.
backend/src/domain/ports/user_repository.rs
backend/src/domain/ports/mod.rs
backend/src/outbound/persistence/user_persistence_error_mapping.rs
backend/src/outbound/persistence/diesel_users_query.rs
Tighten cursor decoding to distinguish malformed payloads from unsupported directions and map them to distinct HTTP 400 error codes.
  • Refactor the pagination cursor implementation into cursor/mod.rs with a new CursorError::UnsupportedDirection variant and a reject_unsupported_direction helper that inspects the decoded JSON dir field.
  • Ensure Cursor::decode still accepts both padded and unpadded base64url, preserves legacy cursors without dir (defaulting to Direction::Next), and continues to round-trip valid Direction::Next/Prev cursors.
  • Add extensive cursor unit tests in cursor/tests.rs covering round-trip encoding, invalid base64, structurally invalid JSON, unsupported direction values, defaulting behaviour when dir is absent, direction persistence, into_parts semantics, and serialization error handling.
  • Update the inbound users_pagination helper to use map_cursor_error on CursorError, returning invalid_cursor for InvalidBase64/Deserialize, unsupported_direction for UnsupportedDirection, and an internal error for Serialize failures.
backend/crates/pagination/src/cursor/mod.rs
backend/crates/pagination/src/cursor/tests.rs
backend/crates/pagination/src/lib.rs
backend/src/inbound/http/users_pagination.rs
Extend BDD and feature coverage for users list pagination unhappy paths, including unsupported cursor directions.
  • Add a new Gherkin scenario to users_list_pagination.feature asserting that an unsupported users cursor direction is rejected with a 400 response and unsupported_direction details.
  • Extend the users_list_pagination_bdd.rs step definitions with a static unsupported-direction cursor fixture (base64url JSON with an invalid dir value) and a Then-step that asserts the 400/unsupported_direction error envelope.
  • Add a new rstest-bdd scenario function wiring the new feature scenario into the Rust BDD test harness and keep existing pagination scenarios unchanged.
backend/tests/features/users_list_pagination.feature
backend/tests/users_list_pagination_bdd.rs
Document the pagination-aware error contract and expose it in user-facing and internal docs, marking roadmap item 4.2.2 complete.
  • Add a Pagination error mapping section to keyset-pagination-design.md describing how malformed cursors, unsupported directions, connection failures, and query failures map to HTTP status codes and detail codes, and explaining the role of repository-layer pagination errors.
  • Extend wildside-backend-architecture.md to cover the new CursorError::UnsupportedDirection, the mapping of malformed cursors/unsupported directions to invalid_cursor/unsupported_direction, and the use of UserPersistenceError::Pagination/UserPaginationError by repository/query adapters without leaking Diesel types across boundaries.
  • Update developers-guide.md with guidance on modelling pagination-aware repository errors as semantic port errors, and on using static base64url cursor fixtures for BDD tests that need 'static URLs.
  • Create docs/users-guide.md to describe user-visible behaviour of GET /api/v1/users, including the paginated response envelope and a table of pagination input errors mapping to invalid_cursor and unsupported_direction codes.
  • Update docs/contents.md to link the new Wildside server users guide under operational runbooks and mark backend-roadmap.md item 4.2.2 as completed.
docs/keyset-pagination-design.md
docs/wildside-backend-architecture.md
docs/developers-guide.md
docs/users-guide.md
docs/contents.md
docs/backend-roadmap.md
docs/execplans/backend-4-2-2-surface-pagination-aware-errors.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-delta-analysis[bot]

This comment was marked as outdated.

leynos added 5 commits May 26, 2026 12:33
Mark the ExecPlan as in progress after explicit implementation approval.
Capture the clean branch baseline so later milestones can distinguish
planned work from unrelated local state.
Add a user repository pagination error surface so invalid cursor and
unsupported direction failures can travel through the port layer without
being collapsed into generic persistence query failures.

Classify unsupported cursor directions in the pagination crate, map both
repository-originated and inbound cursor failures to HTTP 400 details, and
cover the externally visible users-list behaviour with rstest-bdd.
Record the user-facing and internal pagination error contract.
Mark backend roadmap item 4.2.2 done after the implementation
and validation gates passed.
Update the execution plan with the final CodeRabbit result
and closure status for backend roadmap item 4.2.2.
Update the execution plan with the draft PR handoff
after pushing the completed backend 4.2.2 work.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos leynos marked this pull request as ready for review May 26, 2026 18:31
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 6 issues, and left some high level feedback:

  • In Cursor::decode you decode into serde_json::Value and then immediately from_value into Cursor<Key>, which does a second deserialization step; consider instead deserializing into a small intermediate struct (with an Option<String> dir field) or implementing a custom Deserialize to validate dir without double-parsing the JSON.
  • The construction of invalid_cursor / unsupported_direction error envelopes (message + details.field/details.code) is duplicated between the HTTP pagination adapter and user_persistence_error_mapping; consider extracting small shared helpers to centralise this mapping and avoid drift if the contract changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Cursor::decode` you decode into `serde_json::Value` and then immediately `from_value` into `Cursor<Key>`, which does a second deserialization step; consider instead deserializing into a small intermediate struct (with an `Option<String>` dir field) or implementing a custom `Deserialize` to validate `dir` without double-parsing the JSON.
- The construction of `invalid_cursor` / `unsupported_direction` error envelopes (message + `details.field`/`details.code`) is duplicated between the HTTP pagination adapter and `user_persistence_error_mapping`; consider extracting small shared helpers to centralise this mapping and avoid drift if the contract changes.

## Individual Comments

### Comment 1
<location path="backend/crates/pagination/src/cursor/tests.rs" line_range="117-134" />
<code_context>
+}
+
+#[test]
+fn invalid_direction_value_returns_unsupported_direction_error() {
+    let invalid_cursor_json =
+        r#"{"key":{"created_at":"2026-03-22T10:30:00Z","id":"test-id"},"dir":"Sideways"}"#;
+    let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(invalid_cursor_json);
+
+    let result = Cursor::<FixtureKey>::decode(&encoded);
+
+    assert!(matches!(
+        result,
+        Err(CursorError::UnsupportedDirection { direction }) if direction == "Sideways"
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for unsupported direction values that are not strings

The helper has a dedicated branch for non-string `dir` values via `as_str().map_or_else(|| direction.to_string(), …)`, but the current test only covers the string case (`"Sideways"`). Please add a test where `"dir"` is non-string (e.g. `"dir": 123` or an object) and assert you still get `CursorError::UnsupportedDirection` with the expected `direction` string, so the fallback path and malformed JSON cursors are covered.

```suggestion
#[test]
fn invalid_direction_value_returns_unsupported_direction_error() {
    let invalid_cursor_json =
        r#"{"key":{"created_at":"2026-03-22T10:30:00Z","id":"test-id"},"dir":"Sideways"}"#;
    let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(invalid_cursor_json);

    let result = Cursor::<FixtureKey>::decode(&encoded);

    assert!(matches!(
        result,
        Err(CursorError::UnsupportedDirection { direction }) if direction == "Sideways"
    ));
}

#[test]
fn non_string_direction_value_returns_unsupported_direction_error() {
    let invalid_cursor_json =
        r#"{"key":{"created_at":"2026-03-22T10:30:00Z","id":"test-id"},"dir":123}"#;
    let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(invalid_cursor_json);

    let result = Cursor::<FixtureKey>::decode(&encoded);

    assert!(matches!(
        result,
        Err(CursorError::UnsupportedDirection { direction }) if direction == "123"
    ));
}

#[rstest]
#[case(Direction::Next)]
#[case(Direction::Prev)]
fn into_parts_returns_key_and_direction(#[case] direction: Direction) {
```
</issue_to_address>

### Comment 2
<location path="backend/crates/pagination/src/cursor/mod.rs" line_range="214" />
<code_context>
-    /// Returns [`CursorError::InvalidBase64`] when `value` is not valid
-    /// base64url and [`CursorError::Deserialize`] when the decoded JSON does
-    /// not match the expected cursor shape.
-    pub fn decode(value: &str) -> Result<Self, CursorError> {
-        let payload = URL_SAFE_NO_PAD
-            .decode(value)
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring `Cursor::decode` to use a dedicated wire struct and a base64 helper to streamline decoding logic and remove the JSON `Value` round-trip.

You can simplify `Cursor::decode` without changing behavior by:

1. **Avoiding the `serde_json::Value` round‑trip**

Instead of `Value` + `reject_unsupported_direction` + second `from_value`, deserialize into a small internal wire type and handle `dir` as a `String`/`Option<String>`.

```rust
#[derive(Deserialize)]
struct CursorWire<Key> {
    key: Key,
    #[serde(default)]
    dir: Option<String>,
}

impl<Key> Cursor<Key>
where
    Key: DeserializeOwned,
{
    pub fn decode(value: &str) -> Result<Self, CursorError> {
        let payload = decode_base64_url(value)?;
        let wire: CursorWire<Key> =
            serde_json::from_slice(&payload).map_err(|error| CursorError::Deserialize {
                message: error.to_string(),
            })?;

        let dir = match wire.dir.as_deref() {
            None | Some("Next") => Direction::Next,
            Some("Prev") => Direction::Prev,
            Some(other) => {
                return Err(CursorError::UnsupportedDirection {
                    direction: other.to_owned(),
                })
            }
        };

        Ok(Cursor { key: wire.key, dir })
    }
}
```

This:
- Keeps the `UnsupportedDirection { direction }` behavior intact.
- Removes the extra allocation and double‑decode.
- Makes the decode path linear and easier to read.

You can then delete `reject_unsupported_direction` entirely.

2. **Extracting base64 decoding into a helper**

The dual engine handling is reasonable, but inlining it makes `decode` dense. A small helper keeps behavior while improving readability:

```rust
fn decode_base64_url(value: &str) -> Result<Vec<u8>, CursorError> {
    URL_SAFE_NO_PAD
        .decode(value)
        .or_else(|_| URL_SAFE.decode(value))
        .map_err(|error| CursorError::InvalidBase64 {
            message: error.to_string(),
        })
}
```

Then `Cursor::decode` just does:

```rust
let payload = decode_base64_url(value)?;
```
</issue_to_address>

### Comment 3
<location path="docs/execplans/backend-4-2-2-surface-pagination-aware-errors.md" line_range="14" />
<code_context>
+
+Roadmap task 4.2.2 requires the backend to surface pagination-aware repository
+errors, such as malformed cursor tokens and unsupported pagination direction,
+as HTTP `400 Bad Request` responses instead of treating them as generic
+persistence failures. After this work, a caller of `GET /api/v1/users` can pass
+an invalid pagination cursor and receive the existing Wildside error envelope
</code_context>
<issue_to_address>
**issue (review_instructions):** HTTP is introduced without being expanded on first use in this new document.

Since this is a new ExecPlan file, Hypertext Transfer Protocol (HTTP) should be expanded on its first occurrence, for example: "as Hypertext Transfer Protocol (HTTP) `400 Bad Request` responses". Subsequent uses can then use the acronym alone.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 4
<location path="docs/execplans/backend-4-2-2-surface-pagination-aware-errors.md" line_range="645" />
<code_context>
+  HTTP `400` client errors that identify the problematic parameter.
+- RFC 9457:
+  `https://www.rfc-editor.org/rfc/rfc9457.html`. Relevant point:
+  machine-readable error details are useful for HTTP APIs, but Wildside already
+  has a stable JSON error envelope, so this work should enrich `details`
+  instead of changing media type or top-level shape.
</code_context>
<issue_to_address>
**issue (review_instructions):** API is used without being expanded on first use in this file.

Please expand API on its first appearance in this ExecPlan, for example "application programming interface (API)", then use "API" thereafter. That keeps the document self-contained for readers unfamiliar with the acronym.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 5
<location path="docs/users-guide.md" line_range="3" />
<code_context>
+# Wildside server users guide
+
+This guide records user-visible server behaviour for Wildside API consumers.
+It focuses on contracts that clients can rely on when calling the backend.
+
</code_context>
<issue_to_address>
**issue (review_instructions):** API is not expanded on first use in this new users guide.

As this is a new standalone guide, please expand API on first use, for example: "Wildside application programming interface (API) consumers".

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 6
<location path="docs/users-guide.md" line_range="33" />
<code_context>
+}
+```
+
+Pagination input errors use the standard Wildside error envelope and return
+HTTP `400 Bad Request`:
+
</code_context>
<issue_to_address>
**issue (review_instructions):** HTTP is used on the following line without being expanded on first use in this file.

On the next line, HTTP appears for the first time in this document. Please expand it as "Hypertext Transfer Protocol (HTTP)" at that first occurrence before using the acronym alone.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +117 to +134
#[test]
fn invalid_direction_value_returns_unsupported_direction_error() {
let invalid_cursor_json =
r#"{"key":{"created_at":"2026-03-22T10:30:00Z","id":"test-id"},"dir":"Sideways"}"#;
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(invalid_cursor_json);

let result = Cursor::<FixtureKey>::decode(&encoded);

assert!(matches!(
result,
Err(CursorError::UnsupportedDirection { direction }) if direction == "Sideways"
));
}

#[rstest]
#[case(Direction::Next)]
#[case(Direction::Prev)]
fn into_parts_returns_key_and_direction(#[case] direction: Direction) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test for unsupported direction values that are not strings

The helper has a dedicated branch for non-string dir values via as_str().map_or_else(|| direction.to_string(), …), but the current test only covers the string case ("Sideways"). Please add a test where "dir" is non-string (e.g. "dir": 123 or an object) and assert you still get CursorError::UnsupportedDirection with the expected direction string, so the fallback path and malformed JSON cursors are covered.

Suggested change
#[test]
fn invalid_direction_value_returns_unsupported_direction_error() {
let invalid_cursor_json =
r#"{"key":{"created_at":"2026-03-22T10:30:00Z","id":"test-id"},"dir":"Sideways"}"#;
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(invalid_cursor_json);
let result = Cursor::<FixtureKey>::decode(&encoded);
assert!(matches!(
result,
Err(CursorError::UnsupportedDirection { direction }) if direction == "Sideways"
));
}
#[rstest]
#[case(Direction::Next)]
#[case(Direction::Prev)]
fn into_parts_returns_key_and_direction(#[case] direction: Direction) {
#[test]
fn invalid_direction_value_returns_unsupported_direction_error() {
let invalid_cursor_json =
r#"{"key":{"created_at":"2026-03-22T10:30:00Z","id":"test-id"},"dir":"Sideways"}"#;
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(invalid_cursor_json);
let result = Cursor::<FixtureKey>::decode(&encoded);
assert!(matches!(
result,
Err(CursorError::UnsupportedDirection { direction }) if direction == "Sideways"
));
}
#[test]
fn non_string_direction_value_returns_unsupported_direction_error() {
let invalid_cursor_json =
r#"{"key":{"created_at":"2026-03-22T10:30:00Z","id":"test-id"},"dir":123}"#;
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(invalid_cursor_json);
let result = Cursor::<FixtureKey>::decode(&encoded);
assert!(matches!(
result,
Err(CursorError::UnsupportedDirection { direction }) if direction == "123"
));
}
#[rstest]
#[case(Direction::Next)]
#[case(Direction::Prev)]
fn into_parts_returns_key_and_direction(#[case] direction: Direction) {

/// base64url, [`CursorError::UnsupportedDirection`] when `dir` is present
/// but not one of the supported directions, and [`CursorError::Deserialize`]
/// when the decoded JSON does not match the expected cursor shape.
pub fn decode(value: &str) -> Result<Self, CursorError> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring Cursor::decode to use a dedicated wire struct and a base64 helper to streamline decoding logic and remove the JSON Value round-trip.

You can simplify Cursor::decode without changing behavior by:

  1. Avoiding the serde_json::Value round‑trip

Instead of Value + reject_unsupported_direction + second from_value, deserialize into a small internal wire type and handle dir as a String/Option<String>.

#[derive(Deserialize)]
struct CursorWire<Key> {
    key: Key,
    #[serde(default)]
    dir: Option<String>,
}

impl<Key> Cursor<Key>
where
    Key: DeserializeOwned,
{
    pub fn decode(value: &str) -> Result<Self, CursorError> {
        let payload = decode_base64_url(value)?;
        let wire: CursorWire<Key> =
            serde_json::from_slice(&payload).map_err(|error| CursorError::Deserialize {
                message: error.to_string(),
            })?;

        let dir = match wire.dir.as_deref() {
            None | Some("Next") => Direction::Next,
            Some("Prev") => Direction::Prev,
            Some(other) => {
                return Err(CursorError::UnsupportedDirection {
                    direction: other.to_owned(),
                })
            }
        };

        Ok(Cursor { key: wire.key, dir })
    }
}

This:

  • Keeps the UnsupportedDirection { direction } behavior intact.
  • Removes the extra allocation and double‑decode.
  • Makes the decode path linear and easier to read.

You can then delete reject_unsupported_direction entirely.

  1. Extracting base64 decoding into a helper

The dual engine handling is reasonable, but inlining it makes decode dense. A small helper keeps behavior while improving readability:

fn decode_base64_url(value: &str) -> Result<Vec<u8>, CursorError> {
    URL_SAFE_NO_PAD
        .decode(value)
        .or_else(|_| URL_SAFE.decode(value))
        .map_err(|error| CursorError::InvalidBase64 {
            message: error.to_string(),
        })
}

Then Cursor::decode just does:

let payload = decode_base64_url(value)?;


Roadmap task 4.2.2 requires the backend to surface pagination-aware repository
errors, such as malformed cursor tokens and unsupported pagination direction,
as HTTP `400 Bad Request` responses instead of treating them as generic
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (review_instructions): HTTP is introduced without being expanded on first use in this new document.

Since this is a new ExecPlan file, Hypertext Transfer Protocol (HTTP) should be expanded on its first occurrence, for example: "as Hypertext Transfer Protocol (HTTP) 400 Bad Request responses". Subsequent uses can then use the acronym alone.

Review instructions:

Path patterns: **/*.md

Instructions:
Define uncommon acronyms on first use.

HTTP `400` client errors that identify the problematic parameter.
- RFC 9457:
`https://www.rfc-editor.org/rfc/rfc9457.html`. Relevant point:
machine-readable error details are useful for HTTP APIs, but Wildside already
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (review_instructions): API is used without being expanded on first use in this file.

Please expand API on its first appearance in this ExecPlan, for example "application programming interface (API)", then use "API" thereafter. That keeps the document self-contained for readers unfamiliar with the acronym.

Review instructions:

Path patterns: **/*.md

Instructions:
Define uncommon acronyms on first use.

Comment thread docs/users-guide.md
@@ -0,0 +1,44 @@
# Wildside server users guide

This guide records user-visible server behaviour for Wildside API consumers.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (review_instructions): API is not expanded on first use in this new users guide.

As this is a new standalone guide, please expand API on first use, for example: "Wildside application programming interface (API) consumers".

Review instructions:

Path patterns: **/*.md

Instructions:
Define uncommon acronyms on first use.

Comment thread docs/users-guide.md
}
```

Pagination input errors use the standard Wildside error envelope and return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (review_instructions): HTTP is used on the following line without being expanded on first use in this file.

On the next line, HTTP appears for the first time in this document. Please expand it as "Hypertext Transfer Protocol (HTTP)" at that first occurrence before using the acronym alone.

Review instructions:

Path patterns: **/*.md

Instructions:
Define uncommon acronyms on first use.

@coderabbitai coderabbitai Bot added the Roadmap label May 26, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/crates/pagination/src/cursor/tests.rs`:
- Around line 20-175: Add a proptest-based property test that asserts round-trip
invariants for Cursor: generate arbitrary FixtureKey values (strings for
created_at and id) and both Direction variants, build a Cursor with
Cursor::with_direction(key, direction), encode it with Cursor::encode(), decode
it with Cursor::<FixtureKey>::decode(&encoded) and assert equality (decoded ==
original cursor). Put the test alongside the existing tests (tests.rs), import
proptest macros/strategies (proptest::proptest, proptest::prop_oneof or
prop::strategy::Strategy), and ensure the test covers both directions and
handles encode/decode Result by unwrapping or mapping errors into test failures
so failures surface as test errors.

In `@docs/users-guide.md`:
- Line 38: Update the table row string "Cursor text is not a valid users cursor"
to correct grammar by changing "users" to "user" so it reads "Cursor text is not
a valid user cursor"; keep the rest of the row (the code literals `cursor is
invalid`, `cursor`, `invalid_cursor`) unchanged to preserve identifiers.
- Line 8: The sentence describing the endpoint `GET /api/v1/users` uses the
ambiguous phrase "paginated users response"; change that fragment to a
grammatically correct form such as "paginated user-list response" (or "paginated
users-list response") to fix article–noun agreement and improve clarity in the
docs.
- Around line 14-15: Update the documentation line describing `cursor` to fix
article–noun agreement: replace "a previous users list response" with "a
previous user-list response" so the `cursor` description reads "...an opaque
base64url cursor returned by a previous user-list response." Reference the
`cursor` descriptor in the docs and ensure the hyphenated compound noun
"user-list" is used.

In `@docs/wildside-backend-architecture.md`:
- Line 2024: The Markdown paragraph containing "from generic deserialization
failures with `CursorError::UnsupportedDirection`." exceeds 80 columns; rewrap
that paragraph so the line breaks before the backtick-quoted identifier (e.g.,
split into "from generic deserialization failures with" on one line and
"`CursorError::UnsupportedDirection`." on the next) to comply with the 80-column
limit while preserving the exact identifier.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dcd1394c-adec-4782-9b84-853d1a6f36ed

📥 Commits

Reviewing files that changed from the base of the PR and between b23476f and ca56a3f.

📒 Files selected for processing (18)
  • backend/crates/pagination/src/cursor.rs
  • backend/crates/pagination/src/cursor/mod.rs
  • backend/crates/pagination/src/cursor/tests.rs
  • backend/crates/pagination/src/lib.rs
  • backend/src/domain/ports/mod.rs
  • backend/src/domain/ports/user_repository.rs
  • backend/src/inbound/http/users_pagination.rs
  • backend/src/outbound/persistence/diesel_users_query.rs
  • backend/src/outbound/persistence/user_persistence_error_mapping.rs
  • backend/tests/features/users_list_pagination.feature
  • backend/tests/users_list_pagination_bdd.rs
  • docs/backend-roadmap.md
  • docs/contents.md
  • docs/developers-guide.md
  • docs/execplans/backend-4-2-2-surface-pagination-aware-errors.md
  • docs/keyset-pagination-design.md
  • docs/users-guide.md
  • docs/wildside-backend-architecture.md
💤 Files with no reviewable changes (1)
  • backend/crates/pagination/src/cursor.rs

Comment on lines +20 to +175
#[test]
fn cursor_round_trips_through_opaque_token() {
let cursor = Cursor::new(FixtureKey {
created_at: "2026-03-22T10:30:00Z".to_owned(),
id: "8b116c56-0a58-4c55-b7d7-06ee6bbddb8c".to_owned(),
});

let encoded = cursor.encode().expect("cursor encoding should succeed");
let decoded = Cursor::<FixtureKey>::decode(&encoded).expect("cursor decoding should succeed");

assert_eq!(decoded, cursor);
}

#[test]
fn invalid_base64_cursor_fails_decode() {
let result = Cursor::<FixtureKey>::decode("!!!");

assert!(matches!(result, Err(CursorError::InvalidBase64 { .. })));
}

#[test]
fn padded_base64_cursor_decodes_successfully() {
let cursor = Cursor::new(FixtureKey {
created_at: "2026-03-22T10:30:00Z".to_owned(),
id: "8b116c56-0a58-4c55-b7d7-06ee6bbddb8c".to_owned(),
});
let payload = serde_json::to_vec(&cursor).expect("cursor should serialize");
let encoded = base64::engine::general_purpose::URL_SAFE.encode(payload);

let decoded =
Cursor::<FixtureKey>::decode(&encoded).expect("padded cursor decoding should succeed");

assert_eq!(decoded, cursor);
}

#[test]
fn structurally_invalid_json_cursor_fails_decode() {
let invalid_payload =
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(br#"{"unexpected":true}"#);

let result = Cursor::<FixtureKey>::decode(&invalid_payload);

assert!(matches!(result, Err(CursorError::Deserialize { .. })));
}

#[rstest]
#[case(Direction::Next)]
#[case(Direction::Prev)]
fn direction_round_trips_through_encoding(#[case] direction: Direction) {
let cursor = Cursor::with_direction(
FixtureKey {
created_at: "2026-03-22T10:30:00Z".to_owned(),
id: "test-id".to_owned(),
},
direction,
);
let encoded = cursor.encode().expect("encoding succeeds");
let decoded = Cursor::<FixtureKey>::decode(&encoded).expect("decoding succeeds");

assert_eq!(decoded.direction(), direction);
assert_eq!(decoded.key(), cursor.key());
}

#[test]
fn cursor_without_direction_defaults_to_next() {
let old_cursor_json = r#"{"key":{"created_at":"2026-03-22T10:30:00Z","id":"test-id"}}"#;
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(old_cursor_json);

let decoded = Cursor::<FixtureKey>::decode(&encoded).expect("decoding succeeds");

assert_eq!(decoded.direction(), Direction::Next);
}

#[rstest]
#[case(Direction::Next, "Next")]
#[case(Direction::Prev, "Prev")]
fn new_cursor_includes_direction_in_json(#[case] direction: Direction, #[case] expected: &str) {
let cursor = Cursor::with_direction(
FixtureKey {
created_at: "2026-03-22T10:30:00Z".to_owned(),
id: "test-id".to_owned(),
},
direction,
);
let encoded = cursor.encode().expect("encoding succeeds");
let decoded_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&encoded)
.expect("base64 decoding succeeds");
let json_value: serde_json::Value = serde_json::from_slice(&decoded_bytes).expect("valid JSON");

let dir_value = json_value
.get("dir")
.and_then(|v| v.as_str())
.expect("dir field should exist and be a string");
assert_eq!(dir_value, expected);
}

#[test]
fn invalid_direction_value_returns_unsupported_direction_error() {
let invalid_cursor_json =
r#"{"key":{"created_at":"2026-03-22T10:30:00Z","id":"test-id"},"dir":"Sideways"}"#;
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(invalid_cursor_json);

let result = Cursor::<FixtureKey>::decode(&encoded);

assert!(matches!(
result,
Err(CursorError::UnsupportedDirection { direction }) if direction == "Sideways"
));
}

#[rstest]
#[case(Direction::Next)]
#[case(Direction::Prev)]
fn into_parts_returns_key_and_direction(#[case] direction: Direction) {
let key = FixtureKey {
created_at: "2026-03-22T10:30:00Z".to_owned(),
id: "test-id".to_owned(),
};
let cursor = Cursor::with_direction(key.clone(), direction);

let (returned_key, returned_dir) = cursor.into_parts();

assert_eq!(returned_key, key);
assert_eq!(returned_dir, direction);
}

#[test]
fn cursor_new_uses_next_direction() {
let cursor = Cursor::new(FixtureKey {
created_at: "2026-03-22T10:30:00Z".to_owned(),
id: "test-id".to_owned(),
});

assert_eq!(cursor.direction(), Direction::Next);
}

#[test]
fn encode_returns_serialize_error_when_key_cannot_be_serialized() {
use std::collections::HashMap;
#[derive(Hash, PartialEq, Eq)]
struct FailingKey;
impl Serialize for FailingKey {
fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
Err(serde::ser::Error::custom("fail"))
}
}
let cursor = Cursor {
key: HashMap::from([(FailingKey, String::new())]),
dir: Direction::Next,
};
let Err(CursorError::Serialize { message }) = cursor.encode() else {
panic!("expected Serialize error")
};
assert!(message.contains("fail"));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 🏗️ Heavy lift

Add property-based invariants for cursor round-tripping.

Add a proptest suite that asserts decode(encode(cursor)) == cursor across generated
keys and both directions. Lock the opaque-token contract against edge cases that
example-based tests miss.

♻️ Suggested test shape
+use proptest::prelude::*;
+
+proptest! {
+    #[test]
+    fn cursor_roundtrip_holds_for_generated_inputs(
+        created_at in "[0-9TZ:\\-]{1,40}",
+        id in "[a-zA-Z0-9\\-]{1,64}",
+        dir in prop_oneof![Just(Direction::Next), Just(Direction::Prev)],
+    ) {
+        let key = FixtureKey { created_at, id };
+        let cursor = Cursor::with_direction(key, dir);
+        let encoded = cursor.encode().expect("encode succeeds");
+        let decoded = Cursor::<FixtureKey>::decode(&encoded).expect("decode succeeds");
+        prop_assert_eq!(decoded, cursor);
+    }
+}

As per coding guidelines: "**/*.{rs,py,ts,tsx}: Property tests ... should be recommended when the change introduces an invariant over a range of inputs, states, orderings, or transitions."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/crates/pagination/src/cursor/tests.rs` around lines 20 - 175, Add a
proptest-based property test that asserts round-trip invariants for Cursor:
generate arbitrary FixtureKey values (strings for created_at and id) and both
Direction variants, build a Cursor with Cursor::with_direction(key, direction),
encode it with Cursor::encode(), decode it with
Cursor::<FixtureKey>::decode(&encoded) and assert equality (decoded == original
cursor). Put the test alongside the existing tests (tests.rs), import proptest
macros/strategies (proptest::proptest, proptest::prop_oneof or
prop::strategy::Strategy), and ensure the test covers both directions and
handles encode/decode Result by unwrapping or mapping errors into test failures
so failures surface as test errors.

Comment thread docs/users-guide.md

## Users list pagination

`GET /api/v1/users` returns a paginated users response. Clients should follow
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix grammar: use singular article-noun agreement.

"paginated users response" is grammatically ambiguous. Change to "paginated user-list response" or "paginated users-list response" for clarity.

[type:grammar]

Proposed fix
-`GET /api/v1/users` returns a paginated users response. Clients should follow
+`GET /api/v1/users` returns a paginated user-list response. Clients should follow
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`GET /api/v1/users` returns a paginated users response. Clients should follow
`GET /api/v1/users` returns a paginated user-list response. Clients should follow
🧰 Tools
🪛 LanguageTool

[uncategorized] ~8-~8: It seems likely that a singular genitive (’s) apostrophe is missing.
Context: ...GET /api/v1/users returns a paginated users response. Clients should follow the `li...

(AI_HYDRA_LEO_APOSTROPHE_S_XS)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/users-guide.md` at line 8, The sentence describing the endpoint `GET
/api/v1/users` uses the ambiguous phrase "paginated users response"; change that
fragment to a grammatically correct form such as "paginated user-list response"
(or "paginated users-list response") to fix article–noun agreement and improve
clarity in the docs.

Comment thread docs/users-guide.md
Comment on lines +14 to +15
- `cursor`: an opaque base64url cursor returned by a previous users list
response.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix grammar: resolve article-noun agreement.

"a previous users list response" contains a grammatical disagreement between the article "a" and the plural noun "users". Change to "a previous user-list response" for clarity.

[type:grammar]

Proposed fix
-- `cursor`: an opaque base64url cursor returned by a previous users list
-  response.
+- `cursor`: an opaque base64url cursor returned by a previous user-list
+  response.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~14-~14: Loose punctuation mark.
Context: ...lves. The endpoint accepts: - cursor: an opaque base64url cursor returned by ...

(UNLIKELY_OPENING_PUNCTUATION)


[grammar] ~14-~14: The plural noun “users” cannot be used with the article “a”. Did you mean “a previous user” or “previous users”?
Context: ... an opaque base64url cursor returned by a previous users list response. - limit: page size. ...

(A_NNS)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/users-guide.md` around lines 14 - 15, Update the documentation line
describing `cursor` to fix article–noun agreement: replace "a previous users
list response" with "a previous user-list response" so the `cursor` description
reads "...an opaque base64url cursor returned by a previous user-list response."
Reference the `cursor` descriptor in the docs and ensure the hyphenated compound
noun "user-list" is used.

Comment thread docs/users-guide.md

| Condition | Message | `details.field` | `details.code` |
|--------------------------------------------|-------------------------------------|-----------------|-------------------------|
| Cursor text is not a valid users cursor | `cursor is invalid` | `cursor` | `invalid_cursor` |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix grammar: use singular article-noun agreement.

"a valid users cursor" contains a grammatical disagreement. Change "users" to "user" to match the singular article "a".

[type:grammar]

Proposed fix
-| Cursor text is not a valid users cursor    | `cursor is invalid`                 | `cursor`        | `invalid_cursor`        |
+| Cursor text is not a valid user cursor      | `cursor is invalid`                 | `cursor`        | `invalid_cursor`        |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Cursor text is not a valid users cursor | `cursor is invalid` | `cursor` | `invalid_cursor` |
| Cursor text is not a valid user cursor | `cursor is invalid` | `cursor` | `invalid_cursor` |
🧰 Tools
🪛 LanguageTool

[grammar] ~38-~38: The plural noun “users” cannot be used with the article “a”. Did you mean “a valid user”?
Context: ...-----------------| | Cursor text is not a valid users cursor | cursor is invalid ...

(A_NNS)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/users-guide.md` at line 38, Update the table row string "Cursor text is
not a valid users cursor" to correct grammar by changing "users" to "user" so it
reads "Cursor text is not a valid user cursor"; keep the rest of the row (the
code literals `cursor is invalid`, `cursor`, `invalid_cursor`) unchanged to
preserve identifiers.


Roadmap 4.2.2 adds the pagination-aware error contract for endpoint adoption.
The pagination crate now distinguishes unsupported decoded cursor directions
from generic deserialization failures with `CursorError::UnsupportedDirection`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Rewrap line 2024 to comply with the 80-column limit.

Line 2024 is 83 characters, exceeding the required 80-column wrapping for Markdown paragraphs. Break the line before the backtick-quoted identifier.

Triage: [type:syntax/md]

Proposed fix
-from generic deserialization failures with `CursorError::UnsupportedDirection`.
+from generic deserialization failures with
+`CursorError::UnsupportedDirection`.

As per coding guidelines: Markdown paragraphs and bullet points must be wrapped at 80 columns.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from generic deserialization failures with `CursorError::UnsupportedDirection`.
from generic deserialization failures with
`CursorError::UnsupportedDirection`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/wildside-backend-architecture.md` at line 2024, The Markdown paragraph
containing "from generic deserialization failures with
`CursorError::UnsupportedDirection`." exceeds 80 columns; rewrap that paragraph
so the line breaks before the backtick-quoted identifier (e.g., split into "from
generic deserialization failures with" on one line and
"`CursorError::UnsupportedDirection`." on the next) to comply with the 80-column
limit while preserving the exact identifier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant