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