Skip to content

Fix the test suite against unreleased Grape 4.0 - #985

Merged
numbata merged 4 commits into
ruby-grape:masterfrom
ericproulx:spec-grape-4-compat
Aug 2, 2026
Merged

Fix the test suite against unreleased Grape 4.0#985
numbata merged 4 commits into
ruby-grape:masterfrom
ericproulx:spec-grape-4-compat

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

Two Grape 4.0 changes break the suite against grape=HEAD. Both are intentional upstream and documented there, so the fixes belong here.

Before this PR, GRAPE_VERSION=HEAD bundle exec rspec fails 23 examples; after, the suite is green on both grape=HEAD (4.0.0) and grape 3.3.4.

1. Collection custom types need parse (12 failures)

ruby-grape/grape#2817 builds an Array/Set element coercer eagerly, in the coercer's constructor, so that coercers — shared across requests — create no state at request time. A collection whose element type Grape cannot coerce is therefore rejected while the params block is evaluated, instead of on the first request that supplies the parameter:

ArgumentError: type Entities::ApiError should support coercion via `[]`

Entities::ApiError is an OpenStruct / Representable::Decorator with no parse, so type: Array[Entities::ApiError] now raises as the API class loads. Fixed by giving the fixtures a one-argument parse, which is what the bare type: X fixtures already carry.

Why this was invisible until now. params_array_spec only ever GETs /swagger_doc/... — it never POSTs to the route declaring that type. Grape used to build the element coercer lazily, so nothing ever built it. Had a request supplied the parameter, grape 3.3.4 answered 400 {"error":"array_of_entities is invalid"} — the ArgumentError raised inside the coercer and swallowed by the coercion validator. So this was a latent misdeclaration in the fixtures, not new breakage.

Comment correction. The existing fixture comments credited this to "Grape 3.2+". That's right for bare type: X but wrong for the collection form:

grape type: X Array[X]
3.1.0 OK OK
3.2.0 ArgumentError OK
3.3.4 ArgumentError OK
4.0 ArgumentError ArgumentError

Comments now point at Grape 4.0 / #2817, and UPGRADING's custom-type bullet — which currently only covers the bare form — gains the collection case.

2. StackableValues is a read-only view (11 failures)

ruby-grape/grape#2823 made Grape::Util::StackableValues a read-only view: .new now takes (new_values, inherited_values) and []= is gone. swagger_routing_spec built instances directly and then wrote to them, so it fails on both counts.

combine_namespaces only ever reads keys off the object ([](key) returning an Array, for .last and .join('/')), so these specs now stub that narrow contract rather than constructing a real instance. That's version-agnostic and can't break again on either the constructor or the writer — the same reasoning behind the stackable_values_double helper already in request_param_parsers/route_spec.rb, which covers the other 9.

Note the fixture values changed shape slightly: the old []= pushed, so [:mount_path] = ['//foo/', '/bar'] actually produced [['//foo/', '/bar']]. The doubles use the flat form, which is what real Grape produces (add_mount_path pushes one string per mount level). Assertions are unchanged — Array#join recurses, so both shapes yield the same joined path.

Grape's compatibility surface is unaffected: grape-swagger's lib never constructs a StackableValues, it only reads through one (is_a?, #inherited_values, #new_values, #[]), all of which #2823 preserved deliberately.

Verification

  • Green on grape=HEAD (4.0.0) and grape 3.3.4.
  • Example count unchanged from master (530, verified with --dry-run against both trees) — no coverage dropped.
  • I could not run the grape 2.1 row locally (grape 2.1 doesn't install on Ruby 4.0.5); leaving that to CI.
  • rubocop could not run: .rubocop.yml references Style/OneClassPerFile, which the installed RuboCop rejects. Pre-existing — it fails the same way on untouched files like lib/grape-swagger.rb.

🤖 Generated with Claude Code

Two Grape 4.0 changes break the suite. Both are intentional upstream and
already documented there, so the fixes belong here.

1. ruby-grape/grape#2817 builds an Array/Set element coercer eagerly, in
   the coercer's constructor, so that coercers -- shared across requests --
   create no state at request time. A collection whose element type Grape
   cannot coerce is therefore rejected while the params block is evaluated,
   rather than on the first request that supplies the parameter.

   Entities::ApiError is an OpenStruct/Representable::Decorator with no
   `parse`, so `type: Array[Entities::ApiError]` now raises as the API class
   loads. The suite only ever GETs /swagger_doc, never posting to the route
   that declares it, which is why the misdeclaration was invisible before:
   the element coercer was never built. Give the fixtures a one-argument
   `parse`, as the bare `type: X` fixtures already have.

   The existing fixture comments credited this to "Grape 3.2+", which is
   right for bare `type: X` but not for the collection form: 3.2.0 and 3.3.4
   both accept `type: Array[X]`. Corrected, and UPGRADING's custom-type
   bullet now covers the collection form too.

2. ruby-grape/grape#2823 turned Grape::Util::StackableValues into a
   read-only view: `.new` takes (new_values, inherited_values) and `[]=` is
   gone. swagger_routing_spec built instances directly, so it fails on both
   counts. combine_namespaces only ever reads keys off the object, so stub
   that contract instead of constructing one -- version-agnostic, and it
   cannot break again on either the constructor or the writer.

Grape's read-only compatibility surface is unaffected: grape-swagger's lib
never constructs a StackableValues, only reads through it.

Verified green against grape=HEAD (4.0.0) and grape 3.3.4; example count is
unchanged from master (530).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

The route only exists so the API compiles and yields a real
namespace_stackable to harvest; its body was never called, but an empty
block trips Lint/EmptyBlock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ericproulx

Copy link
Copy Markdown
Contributor Author

@numbata you need to merge this PR first to fix grape=HEAD tests.

@dblock
dblock requested a review from numbata August 2, 2026 21:27
@numbata

numbata commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

@dblock I will merge this and do release.

The harvested StackableValues double stubbed new_values/inherited_values,
but StackableValues#[] reads Grape's own ivars directly, so it silently
returned the harvest probe's real (empty) namespace instead of the fixture
for any caller reading through []. It only worked because Route#parse
happens to read the namespace via new_values and never hits [] for it -
a coincidence that breaks the moment that read path changes. Stub #[] too
so all read paths agree.

Also tightens the UPGRADING.md guidance for collection custom types:
parse(val) = val performs no validation, so it's only safe as a documented
pattern when the type is documentation-only and never actually coerces
client input.
@numbata
numbata merged commit f51f43f into ruby-grape:master Aug 2, 2026
29 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.

3 participants