Skip to content

Stop entity autodetection from picking up a top-level ::Entity constant#2825

Merged
ericproulx merged 1 commit into
masterfrom
fix-top-level-entity-constant
Jul 27, 2026
Merged

Stop entity autodetection from picking up a top-level ::Entity constant#2825
ericproulx merged 1 commit into
masterfrom
fix-top-level-entity-constant

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Problem

Grape::DSL::Entity#entity_class_for_obj auto-detects a presenter with:

return unless klass.const_defined?(:Entity)
entity = klass.const_get(:Entity)

const_defined? with the default inherit: true walks the full ancestor chain — including Object, which is where Ruby stores top-level constants. So if an application defines any top-level class named Entity (a natural name in apps using grape-entity or similar), it is silently treated as Klass::Entity for every present-ed object that doesn't have its own nested Entity:

class Entity < Grape::Entity  # app-level base class
  expose :id
end

get '/thing' do
  present({ name: 'plain hash' })   # rendered through ::Entity, not returned as-is
end

The same path is reachable for empty collections: object_class([]) resolves to NilClass ([].first.class), whose ancestor chain also reaches Object.

This is the same failure family as #2773, where a top-level ::Options constant broke middleware building via an over-eager constant lookup.

Fix

Walk the ancestors explicitly and ask each one for its own Entity constant (const_defined?(:Entity, false)), skipping Object:

owner = klass.ancestors.detect { |ancestor| ancestor != Object && ancestor.const_defined?(:Entity, false) }
return unless owner

entity = owner.const_get(:Entity, false)
entity if entity.respond_to?(:represent)

Documented behavior is preserved: Klass::Entity on the object's class or any genuine ancestor (including module ancestors) is still found, nearest-ancestor first — which is exactly what the previous inherited lookup resolved. Only the accidental fallback to the global namespace is gone.

The regression spec stubs a top-level ::Entity (a Grape::Entity subclass, so it passes the respond_to?(:represent) check) and asserts it is not used for an object with no nested entity.

🤖 Generated with Claude Code

@ericproulx
ericproulx force-pushed the fix-top-level-entity-constant branch from 7871ad4 to 20fa148 Compare July 26, 2026 19:33
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@ericproulx
ericproulx requested a review from dblock July 26, 2026 19:34
entity_class_for_obj used klass.const_defined?(:Entity), which walks the
ancestor chain up to Object — where top-level constants live. Any app
defining a class named Entity had it silently used as the presenter for
every presented object without its own nested Entity (also reachable for
empty collections, whose lookup class resolves to NilClass).

Walk the ancestors explicitly with const_defined?(:Entity, false),
skipping Object, so only a genuine Klass::Entity (on the class or a real
ancestor) is auto-detected. Same family as the ::Options fix in #2773.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the fix-top-level-entity-constant branch from 20fa148 to a3f6b79 Compare July 27, 2026 05:06
@ericproulx
ericproulx merged commit e603ebe into master Jul 27, 2026
69 checks passed
@ericproulx
ericproulx deleted the fix-top-level-entity-constant branch July 27, 2026 05:09
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.

2 participants