Repository navigation
Make the pagination trait's cursor style mean something #905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
c74e848
Make the pagination trait's cursor style mean something
d32d842
Refuse the styles nothing implements, and catalogue cursor as cursor
4e0484d
Withhold the pagination key from a cursor operation
b2a4318
Check a cursor key, and make the Ruby test able to fail
jorgemanrubia 487e303
Test the mistyping where it lived, and close Swift's silent pass
jorgemanrubia 8b4d294
Merge main into pagination-cursor-style
jorgemanrubia File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| """The pagination trait's `style` is load-bearing in this generator. | ||
|
|
||
| `has_pagination` decides whether the emitted method follows `Link: rel="next"` | ||
| and flattens the walk. It used to key off the trait's mere presence, so the | ||
| `cursor` style the trait documents would have produced exactly the walk it | ||
| exists to avoid. It now keys off `style`, which makes an unrecognised value | ||
| dangerous in a new way: read as "not paginated" it would silently ship a method | ||
| that never walks. So the parse refuses anything that is not `link` or `cursor`, | ||
| and these cases pin both halves. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) | ||
| from generate_services import parse_operation # noqa: E402 | ||
|
|
||
|
|
||
| def _operation(style): | ||
| operation = { | ||
| "operationId": "PollWidgets", | ||
| "responses": {"200": {"content": {"application/json": {"schema": {"type": "object"}}}}}, | ||
| } | ||
| if style is not None: | ||
| operation["x-basecamp-pagination"] = {"style": style, "key": "events"} | ||
| return operation | ||
|
|
||
|
|
||
| def _parse(style): | ||
| return parse_operation("/{accountId}/widgets.json", "get", _operation(style), {}) | ||
|
|
||
|
|
||
| def test_link_style_auto_paginates(): | ||
| assert _parse("link")["has_pagination"] is True | ||
|
|
||
|
|
||
| def test_cursor_style_does_not_auto_paginate(): | ||
| # One call is one page, carrying its own position; flattening would swallow | ||
| # every position a consumer could resume from. | ||
| assert _parse("cursor")["has_pagination"] is False | ||
|
|
||
|
|
||
| def test_no_trait_at_all_is_simply_unpaginated(): | ||
| assert _parse(None)["has_pagination"] is False | ||
|
|
||
|
|
||
| def test_the_key_is_withheld_from_a_cursor_operation(): | ||
| # The key drives envelope unwrapping in type resolution: set, it turns | ||
| # `{events: [...], position}` into the item type. A cursor operation is | ||
| # typed as its envelope, so the key must not reach those consumers at all. | ||
| assert _parse("link")["pagination_key"] == "events" | ||
| assert _parse("cursor")["pagination_key"] is None | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("style", ["page", "linkk", "Link", "", None]) | ||
| def test_an_unrecognised_style_is_refused_rather_than_read_as_unpaginated(style): | ||
| operation = _operation("link") | ||
| if style is None: | ||
| del operation["x-basecamp-pagination"]["style"] | ||
| else: | ||
| operation["x-basecamp-pagination"]["style"] = style | ||
|
|
||
| with pytest.raises(ValueError, match="unsupported pagination style"): | ||
| parse_operation("/{accountId}/widgets.json", "get", operation, {}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
ruby/test/scripts/generate_services_pagination_style_test.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # The pagination trait's `style` is load-bearing in this generator, and Ruby is | ||
| # the one place where it is not the only gate. | ||
| # | ||
| # `has_pagination` decides whether the emitted method follows `Link: rel="next"` | ||
| # and flattens the walk. It used to key off the trait's mere presence, so the | ||
| # `cursor` style the trait documents would have produced exactly the walk it | ||
| # exists to avoid. But `generate_method` asks `(returns_array || has_pagination)` | ||
| # — so a cursor operation answering a bare array would still have walked, by the | ||
| # other half of that disjunction. `cursor_pagination` is carried for that gate | ||
| # alone, and `test_a_cursor_operation_with_a_bare_array_response_still_does_not_walk` | ||
| # is the case that would otherwise pass unnoticed. | ||
|
|
||
| require "test_helper" | ||
| require_relative "../../scripts/generate-services" | ||
|
|
||
| class GenerateServicesPaginationStyleTest < Minitest::Test | ||
| def setup | ||
| @generator = ServiceGenerator.allocate | ||
| end | ||
|
|
||
| def test_link_style_auto_paginates | ||
| assert parse(style: "link")[:has_pagination] | ||
| end | ||
|
|
||
| def test_cursor_style_does_not_auto_paginate | ||
| assert_not parse(style: "cursor")[:has_pagination] | ||
| assert parse(style: "cursor")[:cursor_pagination] | ||
| end | ||
|
|
||
| def test_no_trait_at_all_is_simply_unpaginated | ||
| assert_not parse(style: :none)[:has_pagination] | ||
| assert_not parse(style: :none)[:cursor_pagination] | ||
| end | ||
|
|
||
| # The key drives envelope unwrapping in type resolution: set, it turns | ||
| # `{events: [...], position}` into the item type. A cursor operation is typed | ||
| # as its envelope, so the key must not reach those consumers at all. | ||
| def test_the_key_is_withheld_from_a_cursor_operation | ||
| assert_equal "events", parse(style: "link")[:pagination_key] | ||
| assert_nil parse(style: "cursor")[:pagination_key] | ||
| end | ||
|
|
||
| # The disjunction Ruby alone carries: a bare-array response sets returns_array, | ||
| # which would send a cursor operation down the Link-following path even though | ||
| # has_pagination is false for it. | ||
| # | ||
| # Asserted against the emitted method rather than against a copy of the | ||
| # predicate. A copy is worth nothing here: delete the cursor guard from | ||
| # generate_method and a test that re-implements it stays green while the | ||
| # generator ships the walk. | ||
| def test_a_cursor_operation_with_a_bare_array_response_still_does_not_walk | ||
| operation = parse(style: "cursor", response_schema: { "type" => "array" }) | ||
| assert operation[:returns_array], "the fixture must exercise the returns_array half" | ||
|
|
||
| emitted = Array(@generator.send(:generate_method, operation, service_name: "Widgets")).join("\n") | ||
|
|
||
| assert_no_match(/wrap_paginated|paginate\(/, emitted, | ||
| "a cursor operation must not be emitted as a Link-following walk") | ||
| end | ||
|
|
||
| # The control: the same response shape under the link style DOES walk, so the | ||
| # assertion above is discriminating rather than matching nothing. | ||
| def test_the_same_shape_under_link_style_does_walk | ||
| operation = parse(style: "link", response_schema: { "type" => "array" }, key: nil) | ||
|
|
||
| emitted = Array(@generator.send(:generate_method, operation, service_name: "Widgets")).join("\n") | ||
|
|
||
| assert_match(/wrap_paginated|paginate\(/, emitted, | ||
| "a link operation over a bare array is exactly what the walk is for") | ||
| end | ||
|
|
||
| def test_an_unrecognised_style_is_refused_rather_than_read_as_unpaginated | ||
| [ "page", "linkk", "Link", "", nil ].each do |style| | ||
| error = assert_raises(RuntimeError) { parse(style: style) } | ||
| assert_match(/unsupported pagination style/, error.message, "style #{style.inspect}") | ||
| end | ||
| end | ||
|
|
||
| private | ||
| def parse(style:, response_schema: { "type" => "object" }, key: "events") | ||
| operation = { | ||
| "operationId" => "PollWidgets", | ||
| "description" => "Poll the widgets.", | ||
| "responses" => { "200" => { "content" => { "application/json" => { "schema" => response_schema } } } } | ||
| } | ||
| unless style == :none | ||
| operation["x-basecamp-pagination"] = {} | ||
| operation["x-basecamp-pagination"]["key"] = key if key | ||
| operation["x-basecamp-pagination"]["style"] = style unless style.nil? | ||
| end | ||
|
|
||
| @generator.send(:parse_operation, "/{accountId}/widgets.json", "get", operation) | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.