Skip to content

Improve performance for serialization of entities #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 11, 2014
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ Next Release
============

* Your contribution here.
* [#77](https://github.com/intridea/grape-entity/pull/77): Fix compatibility with Rspec 3 - [@justfalter](https://github.com/justfalter).
* [#76](https://github.com/intridea/grape-entity/pull/76): Improve performance of entity serialization - [@justfalter](https://github.com/justfalter)

0.4.2 (2014-04-03)
==================
Expand Down
98 changes: 98 additions & 0 deletions bench/serializing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
require 'grape-entity'
require 'benchmark'

module Models
class School
attr_reader :classrooms
def initialize
@classrooms = []
end
end

class ClassRoom
attr_reader :students
attr_accessor :teacher
def initialize(opts = {})
@teacher = opts[:teacher]
@students = []
end
end

class Person
attr_accessor :name
def initialize(opts = {})
@name = opts[:name]
end
end

class Teacher < Models::Person
attr_accessor :tenure
def initialize(opts = {})
super(opts)
@tenure = opts[:tenure]
end
end

class Student < Models::Person
attr_reader :grade
def initialize(opts = {})
super(opts)
@grade = opts[:grade]
end
end
end

module Entities
class School < Grape::Entity
expose :classrooms, using: 'Entities::ClassRoom'
end

class ClassRoom < Grape::Entity
expose :teacher, using: 'Entities::Teacher'
expose :students, using: 'Entities::Student'
expose :size do |model, _opts|
model.students.count
end
end

class Person < Grape::Entity
expose :name
end

class Student < Entities::Person
expose :grade
expose :failing do |model, _opts|
model.grade == 'F'
end
end

class Teacher < Entities::Person
expose :tenure
end
end

teacher1 = Models::Teacher.new(name: 'John Smith', tenure: 2)
classroom1 = Models::ClassRoom.new(teacher: teacher1)
classroom1.students << Models::Student.new(name: 'Bobby', grade: 'A')
classroom1.students << Models::Student.new(name: 'Billy', grade: 'B')

teacher2 = Models::Teacher.new(name: 'Lisa Barns')
classroom2 = Models::ClassRoom.new(teacher: teacher2, tenure: 15)
classroom2.students << Models::Student.new(name: 'Eric', grade: 'A')
classroom2.students << Models::Student.new(name: 'Eddie', grade: 'C')
classroom2.students << Models::Student.new(name: 'Arnie', grade: 'C')
classroom2.students << Models::Student.new(name: 'Alvin', grade: 'F')
school = Models::School.new
school.classrooms << classroom1
school.classrooms << classroom2

iters = 5000

Benchmark.bm do |bm|
bm.report('serializing') do
iters.times do
Entities::School.represent(school, serializable: true)
end
end
end
44 changes: 37 additions & 7 deletions lib/grape_entity/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ def self.expose(*args, &block)

args.each do |attribute|
unless @nested_attributes.empty?
orig_attribute = attribute.to_sym
attribute = "#{@nested_attributes.last}__#{attribute}"
nested_attribute_names_hash[attribute.to_sym] = orig_attribute
options[:nested] = true
nested_exposures_hash[@nested_attributes.last.to_sym] ||= {}
nested_exposures_hash[@nested_attributes.last.to_sym][attribute.to_sym] = options
Expand Down Expand Up @@ -175,7 +177,9 @@ def self.with_options(options)
# are symbolized references to methods on the containing object, the values are
# the options that were passed into expose.
def self.exposures
@exposures ||= {}
return @exposures unless @exposures.nil?

@exposures = {}

if superclass.respond_to? :exposures
@exposures = superclass.exposures.merge(@exposures)
Expand All @@ -185,20 +189,39 @@ def self.exposures
end

class << self
attr_accessor :_nested_attribute_names_hash
attr_accessor :_nested_exposures_hash

def nested_attribute_names_hash
self._nested_attribute_names_hash ||= {}
end

def nested_exposures_hash
self._nested_exposures_hash ||= {}
end

def nested_attribute_names
return @nested_attribute_names unless @nested_attribute_names.nil?

@nested_attribute_names = {}.merge(nested_attribute_names_hash)

if superclass.respond_to? :nested_attribute_names
@nested_attribute_names = superclass.nested_attribute_names.deep_merge(@nested_attribute_names)
end

@nested_attribute_names
end

def nested_exposures
value = nested_exposures_hash
return @nested_exposures unless @nested_exposures.nil?

@nested_exposures = {}.merge(nested_exposures_hash)

if superclass.respond_to? :nested_exposures
value = superclass.nested_exposures.deep_merge(value)
@nested_exposures = superclass.nested_exposures.deep_merge(@nested_exposures)
end

value
@nested_exposures
end
end

Expand Down Expand Up @@ -404,7 +427,8 @@ def to_xml(options = {})
protected

def self.name_for(attribute)
attribute.to_s.split('__').last.to_sym
attribute = attribute.to_sym
nested_attribute_names[attribute] || attribute
end

def self.key_for(attribute)
Expand Down Expand Up @@ -475,7 +499,10 @@ def valid_exposure?(attribute, exposure_options)
end

def conditions_met?(exposure_options, options)
if_conditions = (exposure_options[:if_extras] || []).dup
if_conditions = []
unless exposure_options[:if_extras].nil?
if_conditions.concat(exposure_options[:if_extras])
end
if_conditions << exposure_options[:if] unless exposure_options[:if].nil?

if_conditions.each do |if_condition|
Expand All @@ -486,7 +513,10 @@ def conditions_met?(exposure_options, options)
end
end

unless_conditions = (exposure_options[:unless_extras] || []).dup
unless_conditions = []
unless exposure_options[:unless_extras].nil?
unless_conditions.concat(exposure_options[:unless_extras])
end
unless_conditions << exposure_options[:unless] unless exposure_options[:unless].nil?

unless_conditions.each do |unless_condition|
Expand Down
52 changes: 26 additions & 26 deletions spec/grape_entity/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ class Parent < Person

it 'adds the collection: true option if called with a collection' do
representation = subject.represent(4.times.map { Object.new })
representation.each { |r| r.options[:collection].should be_true }
representation.each { |r| r.options[:collection].should be true }
end

it 'returns a serialized hash of a single object if serializable: true' do
Expand Down Expand Up @@ -909,7 +909,7 @@ class UserEntity < Grape::Entity
rep = subject.send(:value_for, :friends)
rep.should be_kind_of Array
rep.size.should == 2
rep.all? { |r| r.is_a?(EntitySpec::UserEntity) }.should be_true
rep.all? { |r| r.is_a?(EntitySpec::UserEntity) }.should be true
end

it 'class' do
Expand All @@ -920,7 +920,7 @@ class UserEntity < Grape::Entity
rep = subject.send(:value_for, :friends)
rep.should be_kind_of Array
rep.size.should == 2
rep.all? { |r| r.is_a?(EntitySpec::UserEntity) }.should be_true
rep.all? { |r| r.is_a?(EntitySpec::UserEntity) }.should be true
end
end
end
Expand Down Expand Up @@ -972,54 +972,54 @@ class UserEntity < Grape::Entity
it 'only passes through hash :if exposure if all attributes match' do
exposure_options = { if: { condition1: true, condition2: true } }

subject.send(:conditions_met?, exposure_options, {}).should be_false
subject.send(:conditions_met?, exposure_options, condition1: true).should be_false
subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true).should be_true
subject.send(:conditions_met?, exposure_options, condition1: false, condition2: true).should be_false
subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true, other: true).should be_true
subject.send(:conditions_met?, exposure_options, {}).should be false
subject.send(:conditions_met?, exposure_options, condition1: true).should be false
subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true).should be true
subject.send(:conditions_met?, exposure_options, condition1: false, condition2: true).should be false
subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true, other: true).should be true
end

it 'looks for presence/truthiness if a symbol is passed' do
exposure_options = { if: :condition1 }

subject.send(:conditions_met?, exposure_options, {}).should be_false
subject.send(:conditions_met?, exposure_options, condition1: true).should be_true
subject.send(:conditions_met?, exposure_options, condition1: false).should be_false
subject.send(:conditions_met?, exposure_options, condition1: nil).should be_false
subject.send(:conditions_met?, exposure_options, {}).should be false
subject.send(:conditions_met?, exposure_options, condition1: true).should be true
subject.send(:conditions_met?, exposure_options, condition1: false).should be false
subject.send(:conditions_met?, exposure_options, condition1: nil).should be false
end

it 'looks for absence/falsiness if a symbol is passed' do
exposure_options = { unless: :condition1 }

subject.send(:conditions_met?, exposure_options, {}).should be_true
subject.send(:conditions_met?, exposure_options, condition1: true).should be_false
subject.send(:conditions_met?, exposure_options, condition1: false).should be_true
subject.send(:conditions_met?, exposure_options, condition1: nil).should be_true
subject.send(:conditions_met?, exposure_options, {}).should be true
subject.send(:conditions_met?, exposure_options, condition1: true).should be false
subject.send(:conditions_met?, exposure_options, condition1: false).should be true
subject.send(:conditions_met?, exposure_options, condition1: nil).should be true
end

it 'only passes through proc :if exposure if it returns truthy value' do
exposure_options = { if: lambda { |_, opts| opts[:true] } }

subject.send(:conditions_met?, exposure_options, true: false).should be_false
subject.send(:conditions_met?, exposure_options, true: true).should be_true
subject.send(:conditions_met?, exposure_options, true: false).should be false
subject.send(:conditions_met?, exposure_options, true: true).should be true
end

it 'only passes through hash :unless exposure if any attributes do not match' do
exposure_options = { unless: { condition1: true, condition2: true } }

subject.send(:conditions_met?, exposure_options, {}).should be_true
subject.send(:conditions_met?, exposure_options, condition1: true).should be_false
subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true).should be_false
subject.send(:conditions_met?, exposure_options, condition1: false, condition2: true).should be_false
subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true, other: true).should be_false
subject.send(:conditions_met?, exposure_options, condition1: false, condition2: false).should be_true
subject.send(:conditions_met?, exposure_options, {}).should be true
subject.send(:conditions_met?, exposure_options, condition1: true).should be false
subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true).should be false
subject.send(:conditions_met?, exposure_options, condition1: false, condition2: true).should be false
subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true, other: true).should be false
subject.send(:conditions_met?, exposure_options, condition1: false, condition2: false).should be true
end

it 'only passes through proc :unless exposure if it returns falsy value' do
exposure_options = { unless: lambda { |_, options| options[:true] == true } }

subject.send(:conditions_met?, exposure_options, true: false).should be_true
subject.send(:conditions_met?, exposure_options, true: true).should be_false
subject.send(:conditions_met?, exposure_options, true: false).should be true
subject.send(:conditions_met?, exposure_options, true: true).should be false
end
end

Expand Down