From 028e9051a0ec71f346b9ea26ea4124481d0dffd1 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Thu, 9 Apr 2026 00:46:27 +0200 Subject: [PATCH 1/3] Add Entity.[] for Grape >= 3.2 param type compatibility Grape 3.2 requires all param types to respond to [] for coercion. Entity classes used as param types (e.g. `type: UserEntity`) crash at route definition time because Grape::DryTypes checks respond_to?(:[]) and raises ArgumentError when it is missing. Add a pass-through [] class method that satisfies the check without altering any existing behavior. --- lib/grape_entity/entity.rb | 6 ++++++ spec/grape_entity/entity_spec.rb | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index 38b2701..b80bc55 100644 --- a/lib/grape_entity/entity.rb +++ b/lib/grape_entity/entity.rb @@ -129,6 +129,12 @@ def hash_access=(value) def delegation_opts @delegation_opts ||= { hash_access: hash_access } end + + # Satisfies the respond_to?(:[]) check in Grape::DryTypes (>= 3.2) + # so Entity subclasses can be used as param types. + def [](val) + val + end end @formatters = {} diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index 7139fec..568096f 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -1010,6 +1010,22 @@ class Parent < Person end end + describe '.[]' do + it 'returns the input unchanged' do + hash = { name: 'Test' } + expect(subject[hash]).to eq(hash) + end + + it 'returns nil unchanged' do + expect(subject[nil]).to be_nil + end + + it 'is inherited by subclasses' do + subclass = Class.new(subject) + expect(subclass[{ id: 1 }]).to eq(id: 1) + end + end + describe '.represent' do it 'returns a single entity if called with one object' do expect(subject.represent(Object.new)).to be_kind_of(subject) From 8a7a12ae1ee5bd441fd6459f1d73df6bf317a6cb Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Thu, 9 Apr 2026 01:17:54 +0200 Subject: [PATCH 2/3] Fix RuboCop offenses and exclude bench from OneClassPerFile Autocorrect SelectByKind and PredicateWithKind offenses in specs. Exclude bench/serializing.rb from Style/OneClassPerFile as it is a benchmark script that intentionally defines multiple modules. --- .rubocop_todo.yml | 5 +++++ spec/grape_entity/entity_spec.rb | 32 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1f31c04..3d372c6 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -29,6 +29,11 @@ Gemspec/RequiredRubyVersion: Exclude: - 'grape-entity.gemspec' +# Offense count: 1 +Style/OneClassPerFile: + Exclude: + - 'bench/serializing.rb' + # Offense count: 6 # This cop supports unsafe autocorrection (--autocorrect-all). Lint/BooleanSymbol: diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index 568096f..0ee975b 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -1039,7 +1039,7 @@ class Parent < Person representation = subject.represent(Array.new(4) { Object.new }) expect(representation).to be_kind_of Array expect(representation.size).to eq(4) - expect(representation.reject { |r| r.is_a?(subject) }).to be_empty + expect(representation.grep_v(subject)).to be_empty end it 'adds the collection: true option if called with a collection' do @@ -1385,7 +1385,7 @@ class Parent < Person expect(representation).to have_key 'things' expect(representation['things']).to be_kind_of Array expect(representation['things'].size).to eq 4 - expect(representation['things'].reject { |r| r.is_a?(subject) }).to be_empty + expect(representation['things'].grep_v(subject)).to be_empty end end @@ -1394,7 +1394,7 @@ class Parent < Person representation = subject.represent(Array.new(4) { Object.new }, root: false) expect(representation).to be_kind_of Array expect(representation.size).to eq 4 - expect(representation.reject { |r| r.is_a?(subject) }).to be_empty + expect(representation.grep_v(subject)).to be_empty end it 'can use a different name' do representation = subject.represent(Array.new(4) { Object.new }, root: 'others') @@ -1402,7 +1402,7 @@ class Parent < Person expect(representation).to have_key 'others' expect(representation['others']).to be_kind_of Array expect(representation['others'].size).to eq 4 - expect(representation['others'].reject { |r| r.is_a?(subject) }).to be_empty + expect(representation['others'].grep_v(subject)).to be_empty end end end @@ -1426,7 +1426,7 @@ class Parent < Person representation = subject.represent(Array.new(4) { Object.new }) expect(representation).to be_kind_of Array expect(representation.size).to eq 4 - expect(representation.reject { |r| r.is_a?(subject) }).to be_empty + expect(representation.grep_v(subject)).to be_empty end end end @@ -1449,7 +1449,7 @@ class Parent < Person expect(representation).to have_key('things') expect(representation['things']).to be_kind_of Array expect(representation['things'].size).to eq 4 - expect(representation['things'].reject { |r| r.is_a?(subject) }).to be_empty + expect(representation['things'].grep_v(subject)).to be_empty end end end @@ -1474,7 +1474,7 @@ class Parent < Person expect(representation).to have_key('things') expect(representation['things']).to be_kind_of Array expect(representation['things'].size).to eq 4 - expect(representation['things'].reject { |r| r.is_a?(child_class) }).to be_empty + expect(representation['things'].grep_v(child_class)).to be_empty end end end @@ -1855,7 +1855,7 @@ def timestamp(date) it 'instantiates a representation if that is called for' do rep = subject.value_for(:friends) - expect(rep.reject { |r| r.is_a?(fresh_class) }).to be_empty + expect(rep.grep_v(fresh_class)).to be_empty expect(rep.first.serializable_hash[:name]).to eq 'Friend 1' expect(rep.last.serializable_hash[:name]).to eq 'Friend 2' end @@ -1877,7 +1877,7 @@ class FriendEntity < Grape::Entity rep = subject.value_for(:friends) expect(rep).to be_kind_of Array - expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty + expect(rep.grep_v(EntitySpec::FriendEntity)).to be_empty expect(rep.first.serializable_hash[:name]).to eq 'Friend 1' expect(rep.last.serializable_hash[:name]).to eq 'Friend 2' end @@ -1898,7 +1898,7 @@ class FriendEntity < Grape::Entity rep = subject.value_for(:custom_friends) expect(rep).to be_kind_of Array - expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty + expect(rep.grep_v(EntitySpec::FriendEntity)).to be_empty expect(rep.first.serializable_hash).to eq(name: 'Friend 1', email: 'friend1@example.com') expect(rep.last.serializable_hash).to eq(name: 'Friend 2', email: 'friend2@example.com') end @@ -1956,7 +1956,7 @@ class CharacteristicsEntity < Grape::Entity rep = subject.value_for(:characteristics) expect(rep).to be_kind_of Array - expect(rep.reject { |r| r.is_a?(EntitySpec::CharacteristicsEntity) }).to be_empty + expect(rep.grep_v(EntitySpec::CharacteristicsEntity)).to be_empty expect(rep.first.serializable_hash[:key]).to eq 'hair_color' expect(rep.first.serializable_hash[:value]).to eq 'brown' end @@ -1976,13 +1976,13 @@ class FriendEntity < Grape::Entity rep = subject.value_for(:friends) expect(rep).to be_kind_of Array - expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty + expect(rep.grep_v(EntitySpec::FriendEntity)).to be_empty expect(rep.first.serializable_hash[:email]).to be_nil expect(rep.last.serializable_hash[:email]).to be_nil rep = subject.value_for(:friends, Grape::Entity::Options.new(user_type: :admin)) expect(rep).to be_kind_of Array - expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty + expect(rep.grep_v(EntitySpec::FriendEntity)).to be_empty expect(rep.first.serializable_hash[:email]).to eq 'friend1@example.com' expect(rep.last.serializable_hash[:email]).to eq 'friend2@example.com' end @@ -2002,7 +2002,7 @@ class FriendEntity < Grape::Entity rep = subject.value_for(:friends, Grape::Entity::Options.new(collection: false)) expect(rep).to be_kind_of Array - expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty + expect(rep.grep_v(EntitySpec::FriendEntity)).to be_empty expect(rep.first.serializable_hash[:email]).to eq 'friend1@example.com' expect(rep.last.serializable_hash[:email]).to eq 'friend2@example.com' end @@ -2081,7 +2081,7 @@ class UserEntity < Grape::Entity rep = subject.value_for(:friends) expect(rep).to be_kind_of Array expect(rep.size).to eq 2 - expect(rep.all? { |r| r.is_a?(EntitySpec::UserEntity) }).to be true + expect(rep.all?(EntitySpec::UserEntity)).to be true end it 'class' do @@ -2092,7 +2092,7 @@ class UserEntity < Grape::Entity rep = subject.value_for(:friends) expect(rep).to be_kind_of Array expect(rep.size).to eq 2 - expect(rep.all? { |r| r.is_a?(EntitySpec::UserEntity) }).to be true + expect(rep.all?(EntitySpec::UserEntity)).to be true end end end From 8ba13a7a7191936e56dfd3ed81c2554494c4f4c3 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Thu, 9 Apr 2026 01:20:29 +0200 Subject: [PATCH 3/3] Add CHANGELOG entry for #394 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5112928..309a2a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Fixes +* [#394](https://github.com/ruby-grape/grape-entity/pull/394): Add `Entity.[]` for Grape >= 3.2 param type compatibility - [@numbata](https://github.com/numbata). * [#388](https://github.com/ruby-grape/grape-entity/pull/388): Drop ruby-head from test matrix - [@numbata](https://github.com/numbata). * [#384](https://github.com/ruby-grape/grape-entity/pull/384): Fix `inspect` to correctly handle `nil` values - [@fcce](https://github.com/fcce). * Your contribution here.