From 3cfa4345565539d76b65a608926345897653ecce Mon Sep 17 00:00:00 2001 From: a5-stable Date: Sun, 25 Oct 2026 13:34:42 +0900 Subject: [PATCH 1/2] Return a plain Hash from OutputBuilder for faster serialization --- CHANGELOG.md | 1 + lib/grape_entity/exposure/nesting_exposure.rb | 2 +- .../nesting_exposure/output_builder.rb | 28 +++++-------------- spec/grape_entity/entity_spec.rb | 11 +++++++- .../nesting_exposure/output_builder_spec.rb | 18 ++++++++++++ 5 files changed, 37 insertions(+), 23 deletions(-) create mode 100644 spec/grape_entity/exposure/nesting_exposure/output_builder_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index d997acc4..10332305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Features +* [#408](https://github.com/ruby-grape/grape-entity/pull/408): Return a plain `Hash`/`Array` from `serializable_hash` and `represent(..., serializable: true)` instead of an `OutputBuilder` for faster serialization. `Hash === result`, `Array === result` and `case`/`when` against the result now match - [@a5-stable](https://github.com/a5-stable). * Your contribution here. diff --git a/lib/grape_entity/exposure/nesting_exposure.rb b/lib/grape_entity/exposure/nesting_exposure.rb index 8d07d4be..fda89d80 100644 --- a/lib/grape_entity/exposure/nesting_exposure.rb +++ b/lib/grape_entity/exposure/nesting_exposure.rb @@ -122,7 +122,7 @@ def map_entity_exposures(entity, options) result = yield(exposure, new_options) out.add(exposure, result) end - end + end.output end end end diff --git a/lib/grape_entity/exposure/nesting_exposure/output_builder.rb b/lib/grape_entity/exposure/nesting_exposure/output_builder.rb index 93afe3c1..be350e0c 100644 --- a/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +++ b/lib/grape_entity/exposure/nesting_exposure/output_builder.rb @@ -4,13 +4,11 @@ module Grape class Entity module Exposure class NestingExposure - class OutputBuilder < SimpleDelegator + class OutputBuilder def initialize(entity) @entity = entity @output_hash = {} @output_collection = [] - - super end def add(exposure, result) @@ -28,29 +26,17 @@ def add(exposure, result) end end - def kind_of?(klass) - klass == output.class || super - end - alias is_a? kind_of? + # If output_collection contains at least one element we have to represent the output as a collection + def output + return @output_hash if @output_collection.empty? - def __getobj__ - output + result = @output_collection.dup + result << @output_hash unless @output_hash.empty? + result.flatten end private - # If output_collection contains at least one element we have to represent the output as a collection - def output - if @output_collection.empty? - output = @output_hash - else - output = @output_collection - output << @output_hash unless @output_hash.empty? - output.flatten! - end - output - end - # In case if we want to solve collisions providing lambda to :merge option def merge_strategy(for_merge) if for_merge.respond_to? :call diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index aa497857..e1949de7 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -1008,6 +1008,16 @@ class Parent < Person awesome: { just_a_key: 'value', just_another_key: 'value' } ) end + + it 'returns the same result when serializable_hash is called repeatedly' do + subject.expose(:tags, merge: true) { |_| %w[a b] } + subject.expose(:name) + + entity = subject.represent(name: 'John') + expected = ['a', 'b', { name: 'John' }] + + 2.times { expect(entity.serializable_hash).to eq(expected) } + end end end @@ -1702,7 +1712,6 @@ class Parent < Person subject.expose :my_items representation = subject.represent(Array.new(4) { Object.new }, serializable: true) - expect(representation).to be_kind_of(Grape::Entity::Exposure::NestingExposure::OutputBuilder) expect(representation).to be_kind_of(Hash) expect(representation).to have_key :my_items expect(representation[:my_items]).to be_kind_of Array diff --git a/spec/grape_entity/exposure/nesting_exposure/output_builder_spec.rb b/spec/grape_entity/exposure/nesting_exposure/output_builder_spec.rb new file mode 100644 index 00000000..216f431e --- /dev/null +++ b/spec/grape_entity/exposure/nesting_exposure/output_builder_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Grape::Entity::Exposure::NestingExposure::OutputBuilder do + subject(:builder) { described_class.new(Object.new) } + + let(:merged_array) { double(for_merge: true) } + let(:plain) { double(for_merge: false, key: :name) } + + it 'returns the same result when #output is called repeatedly' do + builder.add(merged_array, %w[a b]) + builder.add(plain, 'John') + + expected = ['a', 'b', { name: 'John' }] + 2.times { expect(builder.output).to eq(expected) } + end +end From e0be0eabe11dbf6dc153d6ac546e999e46bbc6c8 Mon Sep 17 00:00:00 2001 From: a5-stable Date: Tue, 27 Oct 2026 19:58:42 +0900 Subject: [PATCH 2/2] Fix Style/DirectiveScope offenses from RuboCop 1.90 --- lib/grape_entity/entity.rb | 3 +-- .../exposure/nesting_exposure/nested_exposures.rb | 3 +-- spec/grape_entity/entity_spec.rb | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index 4b409c13..93c87c08 100644 --- a/lib/grape_entity/entity.rb +++ b/lib/grape_entity/entity.rb @@ -190,7 +190,7 @@ def self.inherited(subclass) # field, typically the value is a hash with two fields, type and desc. # @option options :merge This option allows you to merge an exposed field to the root # - # rubocop:disable Layout/LineLength + # rubocop:disable-next Layout/LineLength def self.expose(*args, &block) options = merge_options(args.last.is_a?(Hash) ? args.pop : {}) @@ -217,7 +217,6 @@ def self.expose(*args, &block) @nesting_stack ||= [] args.each { |attribute| build_exposure_for_attribute(attribute, @nesting_stack, options, block) } end - # rubocop:enable Layout/LineLength def self.build_exposure_for_attribute(attribute, nesting_stack, options, block) exposure_list = nesting_stack.empty? ? root_exposures : nesting_stack.last.nested_exposures diff --git a/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb b/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb index 80076ed8..72752377 100644 --- a/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +++ b/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb @@ -36,7 +36,7 @@ def clear @exposures.clear end - # rubocop:disable Style/DocumentDynamicEvalDefinition + # rubocop:disable-next Style/DocumentDynamicEvalDefinition %i[ each to_ary to_a @@ -56,7 +56,6 @@ def #{name}(*args, &block) end RUBY end - # rubocop:enable Style/DocumentDynamicEvalDefinition # Determine if we have any nesting exposures with the same name. def deep_complex_nesting?(entity) diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index e1949de7..467b5a0f 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -33,13 +33,12 @@ end it 'makes sure that :format_with as a proc cannot be used with a block' do - # rubocop:disable Style/BlockDelimiters + # rubocop:disable-next Style/BlockDelimiters expect { subject.expose :name, format_with: proc {} do p 'hi' end }.to raise_error ArgumentError - # rubocop:enable Style/BlockDelimiters end it 'makes sure unknown options are not silently ignored' do @@ -2303,12 +2302,11 @@ class FriendEntity < Grape::Entity end end - # rubocop:disable Lint/EmptyBlock + # rubocop:disable-next Lint/EmptyBlock fresh_class.class_eval do expose :first_friend, using: EntitySpec::FriendEntity do |_user, _opts| end end - # rubocop:enable Lint/EmptyBlock rep = subject.value_for(:first_friend) expect(rep).to be_kind_of EntitySpec::FriendEntity