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 @@ -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.


Expand Down
8 changes: 8 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -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`.
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
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
11 changes: 10 additions & 1 deletion spec/grape_entity/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
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