From a3f6b7914f564da9ebe77005d37cd037b0b68378 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 26 Jul 2026 21:32:53 +0200 Subject: [PATCH] Stop entity autodetection from picking up a top-level ::Entity constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 1 + lib/grape/dsl/entity.rb | 8 +++++--- spec/integration/grape_entity/entity_spec.rb | 13 +++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53b14f0d7..eda402379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ * [#2817](https://github.com/ruby-grape/grape/pull/2817): Remove request-time mutable state from Array and custom-type coercers - [@ericproulx](https://github.com/ericproulx). * [#2819](https://github.com/ruby-grape/grape/pull/2819): Freeze coercers at construction, synchronize `Grape::Util::Cache` lookups, and assign `Route#regexp_capture_index` eagerly - [@ericproulx](https://github.com/ericproulx). * [#2824](https://github.com/ruby-grape/grape/pull/2824): Fix cascaded routes (`X-Cascade: pass`) leaking `route_info` and path captures into the next matched route's `route` and `params` - [@ericproulx](https://github.com/ericproulx). +* [#2825](https://github.com/ruby-grape/grape/pull/2825): Stop `present` entity autodetection from picking up a top-level `::Entity` constant - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 3.3.4 (2026-07-25) diff --git a/lib/grape/dsl/entity.rb b/lib/grape/dsl/entity.rb index 964c43407..9dbb42d99 100644 --- a/lib/grape/dsl/entity.rb +++ b/lib/grape/dsl/entity.rb @@ -45,7 +45,8 @@ def present(*args, root: nil, with: nil, **options) # Attempt to locate the Entity class for a given object, if not given # explicitly. This is done by looking for the presence of Klass::Entity, # where Klass is the class of the `object` parameter, or one of its - # ancestors. + # ancestors. Object is excluded from the search: top-level constants + # live on it, so a global ::Entity class is not a representer. # @param object [Object] the object to locate the Entity class for # @return [Class] the located Entity class, or nil if none is found def entity_class_for_obj(object) @@ -57,9 +58,10 @@ def entity_class_for_obj(object) return representations[potential] if potential && representations[potential] end - return unless klass.const_defined?(:Entity) + owner = klass.ancestors.detect { |ancestor| ancestor != Object && ancestor.const_defined?(:Entity, false) } + return unless owner - entity = klass.const_get(:Entity) + entity = owner.const_get(:Entity, false) entity if entity.respond_to?(:represent) end diff --git a/spec/integration/grape_entity/entity_spec.rb b/spec/integration/grape_entity/entity_spec.rb index 4dc22ca7b..10b9cd55e 100644 --- a/spec/integration/grape_entity/entity_spec.rb +++ b/spec/integration/grape_entity/entity_spec.rb @@ -133,6 +133,19 @@ def first expect(last_response.body).to eq('Auto-detect!') end + it 'autodetection does not use a top-level ::Entity constant' do + some_model = Class.new + entity = Class.new(Grape::Entity) + stub_const('Entity', entity) + + subject.get '/example' do + present some_model.new + end + + expect(entity).not_to receive(:represent) + get '/example' + end + it 'autodetection does not use Entity if it is not a presenter' do some_model = Class.new entity = Class.new