Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Expand Down
3 changes: 1 addition & 2 deletions lib/grape_entity/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 : {})

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/grape_entity/exposure/nesting_exposure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def clear
@exposures.clear
end

# rubocop:disable Style/DocumentDynamicEvalDefinition
# rubocop:disable-next Style/DocumentDynamicEvalDefinition
%i[
each
to_ary to_a
Expand All @@ -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)
Expand Down
28 changes: 7 additions & 21 deletions lib/grape_entity/exposure/nesting_exposure/output_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

output is public now, but it changes @output_collection every time it runs. If it's called twice when there's a merged array and a non-empty hash, @output_hash gets appended a second time.
As output = @output_collection doesn't copy the array. It makes a second name for the same array object, so everything done through output happens to @output_collection too. That's the same growing-result bug the old delegator had. One fix is to build the result without changing state:

 def output
    return @output_hash if @output_collection.empty?

    [*@output_collection, *(@output_hash unless @output_hash.empty?)].flatten
  end

The other option is to rename it to build!/finalize so it's clear it should only be called once. Renaming it would also stop the output local variable from shadowing the method.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@numbata
Thanks!
I fixed output so it no longer mutates the builder's state, by @output_collection.dup:

def output
  return @output_hash if @output_collection.empty?

  result = @output_collection.dup
  result << @output_hash unless @output_hash.empty?
  result.flatten
end

[*@output_collection, *(@output_hash unless @output_hash.empty?)].flatten would spread the hash into key/value pairs, so I could not use:

[*[%w[a b]], *{ name: 'John' }].flatten
# => ["a", "b", :name, "John"] (expected ["a", "b", { name: "John" }])

I also added the spec about this behaviour.

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
Expand Down
17 changes: 12 additions & 5 deletions spec/grape_entity/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1008,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

Expand Down Expand Up @@ -1702,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
Expand Down Expand Up @@ -2294,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
Expand Down
18 changes: 18 additions & 0 deletions spec/grape_entity/exposure/nesting_exposure/output_builder_spec.rb
Original file line number Diff line number Diff line change
@@ -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