From b82417a23a49e0bf4b1392a813a302d5d5cd0c91 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 8442405c..10ae3e21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,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 446b30e3..467b5a0f 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -1007,6 +1007,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 @@ -1701,7 +1711,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 9fdf361573a975eee92ad0a303ddd736c6672f49 Mon Sep 17 00:00:00 2001 From: a5-stable Date: Thu, 29 Oct 2026 10:05:21 +0900 Subject: [PATCH 2/2] Add UPGRADING note --- UPGRADING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/UPGRADING.md b/UPGRADING.md index 1c81381a..b9b9c5e0 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,5 +1,13 @@ # Upgrading Grape Entity + +### Upgrading to >= 1.2.0 + +`serializable_hash` and `represent(..., serializable: true)` now return a plain `Hash` or `Array` instead of `Grape::Entity::Exposure::NestingExposure::OutputBuilder`. +Checks such as `result.is_a?(Grape::Entity::Exposure::NestingExposure::OutputBuilder)` no longer match, and `result.__getobj__` raises `NoMethodError`. Use the result directly as a `Hash` or `Array` instead. + +See [#408](https://github.com/ruby-grape/grape-entity/pull/408) for more information. + ### Upgrading to >= 1.1.0 `multi_json` gem is no longer a runtime dependency. Applications that relied on a configured `MultiJson` adapter should add it to their own `Gemfile`.