spec/grape_entity/entity_spec.rb in grape-entity-0.4.5 vs spec/grape_entity/entity_spec.rb in grape-entity-0.4.6

- old
+ new

@@ -68,18 +68,18 @@ expect(prop1).to eq 'MODIFIED 2' end context 'with parameters passed to the block' do it 'sets the :proc option in the exposure options' do - block = lambda { |_| true } + block = ->(_) { true } subject.expose :name, using: 'Awesome', &block expect(subject.exposures[:name]).to eq(proc: block, using: 'Awesome') end it 'references an instance of the entity without any options' do subject.expose(:size) { |_| self } - expect(subject.represent(Hash.new).send(:value_for, :size)).to be_an_instance_of fresh_class + expect(subject.represent({}).send(:value_for, :size)).to be_an_instance_of fresh_class end end context 'with no parameters passed to the block' do it 'adds a nested exposure' do @@ -89,33 +89,33 @@ end subject.expose :another_nested, using: 'Awesome' end expect(subject.exposures).to eq( - awesome: {}, - awesome__nested: { nested: true }, - awesome__nested__moar_nested: { as: 'weee', nested: true }, - awesome__another_nested: { using: 'Awesome', nested: true } + awesome: {}, + awesome__nested: { nested: true }, + awesome__nested__moar_nested: { as: 'weee', nested: true }, + awesome__another_nested: { using: 'Awesome', nested: true } ) end it 'represents the exposure as a hash of its nested exposures' do subject.expose :awesome do subject.expose(:nested) { |_| 'value' } subject.expose(:another_nested) { |_| 'value' } end expect(subject.represent({}).send(:value_for, :awesome)).to eq( - nested: 'value', - another_nested: 'value' + nested: 'value', + another_nested: 'value' ) end it 'does not represent nested exposures whose conditions are not met' do subject.expose :awesome do - subject.expose(:condition_met, if: lambda { |_, _| true }) { |_| 'value' } - subject.expose(:condition_not_met, if: lambda { |_, _| false }) { |_| 'value' } + subject.expose(:condition_met, if: ->(_, _) { true }) { |_| 'value' } + subject.expose(:condition_not_met, if: ->(_, _) { false }) { |_| 'value' } end expect(subject.represent({}).send(:value_for, :awesome)).to eq(condition_met: 'value') end @@ -127,17 +127,17 @@ subject.expose(:deeply_exposed_attr) { |_| 'value' } end end expect(subject.represent({}).serializable_hash).to eq( - awesome: { - nested: 'value', - another_nested: 'value', - second_level_nested: { - deeply_exposed_attr: 'value' - } - } + awesome: { + nested: 'value', + another_nested: 'value', + second_level_nested: { + deeply_exposed_attr: 'value' + } + } ) end it 'complex nested attributes' do class ClassRoom < Grape::Entity @@ -160,26 +160,26 @@ class Parent < Person expose(:children, using: 'Student') { |_| [{}, {}] } end expect(ClassRoom.represent({}).serializable_hash).to eq( - parents: [ - { - user: { in_first: 'value' }, - children: [ - { user: { in_first: 'value', user_id: 'value', display_id: 'value' } }, - { user: { in_first: 'value', user_id: 'value', display_id: 'value' } } - ] - }, - { - user: { in_first: 'value' }, - children: [ - { user: { in_first: 'value', user_id: 'value', display_id: 'value' } }, - { user: { in_first: 'value', user_id: 'value', display_id: 'value' } } - ] - } - ] + parents: [ + { + user: { in_first: 'value' }, + children: [ + { user: { in_first: 'value', user_id: 'value', display_id: 'value' } }, + { user: { in_first: 'value', user_id: 'value', display_id: 'value' } } + ] + }, + { + user: { in_first: 'value' }, + children: [ + { user: { in_first: 'value', user_id: 'value', display_id: 'value' } }, + { user: { in_first: 'value', user_id: 'value', display_id: 'value' } } + ] + } + ] ) end it 'is safe if its nested exposures are safe' do subject.with_options safe: true do @@ -188,15 +188,18 @@ end subject.expose :not_awesome do subject.expose :nested end end - - valid_keys = subject.represent({}).valid_exposures.keys - - expect(valid_keys.include?(:awesome)).to be true - expect(valid_keys.include?(:not_awesome)).to be false + expect(subject.represent({}, serializable: true)).to eq( + awesome: { + nested: 'value' + }, + not_awesome: { + nested: nil + } + ) end end end context 'inherited exposures' do @@ -226,11 +229,11 @@ expect(child_class.exposures[:name]).to have_key :proc end end context 'register formatters' do - let(:date_formatter) { lambda { |date| date.strftime('%m/%d/%Y') } } + let(:date_formatter) { ->(date) { date.strftime('%m/%d/%Y') } } it 'registers a formatter' do subject.format_with :timestamp, &date_formatter expect(subject.formatters[:timestamp]).not_to be_nil @@ -257,22 +260,22 @@ model = { birthday: Time.gm(2012, 2, 27) } expect(subject.new(double(model)).as_json[:birthday]).to eq '02/27/2012' end it 'formats an exposure with a :format_with lambda that returns a value from the entity instance' do - object = Hash.new + object = {} - subject.expose(:size, format_with: lambda { |_value| self.object.class.to_s }) + subject.expose(:size, format_with: ->(_value) { self.object.class.to_s }) expect(subject.represent(object).send(:value_for, :size)).to eq object.class.to_s end it 'formats an exposure with a :format_with symbol that returns a value from the entity instance' do subject.format_with :size_formatter do |_date| self.object.class.to_s end - object = Hash.new + object = {} subject.expose(:size, format_with: :size_formatter) expect(subject.represent(object).send(:value_for, :size)).to eq object.class.to_s end end @@ -294,28 +297,17 @@ expect(child_class.exposures).to eq(name: {}) expect(subject.exposures).to eq(name: {}, email: {}) end - # the following 2 behaviors are testing because it is not most intuitive and could be confusing - context 'when called from the parent class' do - it 'remove from parent and all child classes that have not locked down their attributes with an .exposures call' do + context 'when called from the parent class' do + it 'remove from parent and do not remove from child classes' do subject.expose :name, :email child_class = Class.new(subject) subject.unexpose :email expect(subject.exposures).to eq(name: {}) - expect(child_class.exposures).to eq(name: {}) - end - - it 'remove from parent and do not remove from child classes that have locked down their attributes with an .exposures call' do - subject.expose :name, :email - child_class = Class.new(subject) - child_class.exposures - subject.unexpose :email - - expect(subject.exposures).to eq(name: {}) expect(child_class.exposures).to eq(name: {}, email: {}) end end end end @@ -362,11 +354,11 @@ expect(subject.exposures[:awesome_thing]).to eq(as: :extra_smooth) end it 'merges nested :if option' do - match_proc = lambda { |_obj, _opts| true } + match_proc = ->(_obj, _opts) { true } subject.class_eval do # Symbol with_options(if: :awesome) do # Hash @@ -381,17 +373,17 @@ end end end expect(subject.exposures[:awesome_thing]).to eq( - if: { awesome: false, less_awesome: true }, - if_extras: [:awesome, match_proc] + if: { awesome: false, less_awesome: true }, + if_extras: [:awesome, match_proc] ) end it 'merges nested :unless option' do - match_proc = lambda { |_, _| true } + match_proc = ->(_, _) { true } subject.class_eval do # Symbol with_options(unless: :awesome) do # Hash @@ -406,12 +398,12 @@ end end end expect(subject.exposures[:awesome_thing]).to eq( - unless: { awesome: false, less_awesome: true }, - unless_extras: [:awesome, match_proc] + unless: { awesome: false, less_awesome: true }, + unless_extras: [:awesome, match_proc] ) end it 'overrides nested :using option' do subject.class_eval do @@ -431,14 +423,14 @@ end expect(subject.exposures[:awesome_thing]).to eq(using: 'SomethingElse') end it 'overrides nested :proc option' do - match_proc = lambda { |_obj, _opts| 'more awesomer' } + match_proc = ->(_obj, _opts) { 'more awesomer' } subject.class_eval do - with_options(proc: lambda { |_obj, _opts| 'awesome' }) do + with_options(proc: ->(_obj, _opts) { 'awesome' }) do expose :awesome_thing, proc: match_proc end end expect(subject.exposures[:awesome_thing]).to eq(proc: match_proc) @@ -459,11 +451,11 @@ it 'returns a single entity if called with one object' do expect(subject.represent(Object.new)).to be_kind_of(subject) end it 'returns a single entity if called with a hash' do - expect(subject.represent(Hash.new)).to be_kind_of(subject) + expect(subject.represent({})).to be_kind_of(subject) end it 'returns multiple entities if called with a collection' do representation = subject.represent(4.times.map { Object.new }) expect(representation).to be_kind_of Array @@ -504,10 +496,168 @@ subject.expose(:awesome) expect do subject.represent(Object.new, serializable: true) end.to raise_error(NoMethodError, /missing attribute `awesome'/) end + + context 'with specified fields' do + it 'returns only specified fields with only option' do + subject.expose(:id, :name, :phone) + representation = subject.represent(OpenStruct.new, only: [:id, :name], serializable: true) + expect(representation).to eq(id: nil, name: nil) + end + + it 'returns all fields except the ones specified in the except option' do + subject.expose(:id, :name, :phone) + representation = subject.represent(OpenStruct.new, except: [:phone], serializable: true) + expect(representation).to eq(id: nil, name: nil) + end + + it 'returns only fields specified in the only option and not specified in the except option' do + subject.expose(:id, :name, :phone) + representation = subject.represent(OpenStruct.new, + only: [:name, :phone], + except: [:phone], serializable: true) + expect(representation).to eq(name: nil) + end + + context 'with strings or symbols passed to only and except' do + let(:object) { OpenStruct.new(user: {}) } + + before do + user_entity = Class.new(Grape::Entity) + user_entity.expose(:id, :name, :email) + + subject.expose(:id, :name, :phone, :address) + subject.expose(:user, using: user_entity) + end + + it 'can specify "only" option attributes as strings' do + representation = subject.represent(object, only: ['id', 'name', { 'user' => ['email'] }], serializable: true) + expect(representation).to eq(id: nil, name: nil, user: { email: nil }) + end + + it 'can specify "except" option attributes as strings' do + representation = subject.represent(object, except: ['id', 'name', { 'user' => ['email'] }], serializable: true) + expect(representation).to eq(phone: nil, address: nil, user: { id: nil, name: nil }) + end + + it 'can specify "only" option attributes as symbols' do + representation = subject.represent(object, only: [:name, :phone, { user: [:name] }], serializable: true) + expect(representation).to eq(name: nil, phone: nil, user: { name: nil }) + end + + it 'can specify "except" option attributes as symbols' do + representation = subject.represent(object, except: [:name, :phone, { user: [:name] }], serializable: true) + expect(representation).to eq(id: nil, address: nil, user: { id: nil, email: nil }) + end + + it 'can specify "only" attributes as strings and symbols' do + representation = subject.represent(object, only: [:id, 'address', { user: [:id, 'name'] }], serializable: true) + expect(representation).to eq(id: nil, address: nil, user: { id: nil, name: nil }) + end + + it 'can specify "except" attributes as strings and symbols' do + representation = subject.represent(object, except: [:id, 'address', { user: [:id, 'name'] }], serializable: true) + expect(representation).to eq(name: nil, phone: nil, user: { email: nil }) + end + end + + it 'can specify children attributes with only' do + user_entity = Class.new(Grape::Entity) + user_entity.expose(:id, :name, :email) + + subject.expose(:id, :name, :phone) + subject.expose(:user, using: user_entity) + + representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, { user: [:name, :email] }], serializable: true) + expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil }) + end + + it 'can specify children attributes with except' do + user_entity = Class.new(Grape::Entity) + user_entity.expose(:id, :name, :email) + + subject.expose(:id, :name, :phone) + subject.expose(:user, using: user_entity) + + representation = subject.represent(OpenStruct.new(user: {}), except: [:phone, { user: [:id] }], serializable: true) + expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil }) + end + + it 'can specify children attributes with mixed only and except' do + user_entity = Class.new(Grape::Entity) + user_entity.expose(:id, :name, :email, :address) + + subject.expose(:id, :name, :phone, :mobile_phone) + subject.expose(:user, using: user_entity) + + representation = subject.represent(OpenStruct.new(user: {}), + only: [:id, :name, :phone, user: [:id, :name, :email]], + except: [:phone, { user: [:id] }], serializable: true) + expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil }) + end + + context 'specify attribute with exposure condition' do + it 'returns only specified fields' do + subject.expose(:id) + subject.with_options(if: { condition: true }) do + subject.expose(:name) + end + + representation = subject.represent(OpenStruct.new, condition: true, only: [:id, :name], serializable: true) + expect(representation).to eq(id: nil, name: nil) + end + + it 'does not return fields specified in the except option' do + subject.expose(:id, :phone) + subject.with_options(if: { condition: true }) do + subject.expose(:name, :mobile_phone) + end + + representation = subject.represent(OpenStruct.new, condition: true, except: [:phone, :mobile_phone], serializable: true) + expect(representation).to eq(id: nil, name: nil) + end + end + + context 'attribute with alias' do + it 'returns only specified fields' do + subject.expose(:id) + subject.expose(:name, as: :title) + + representation = subject.represent(OpenStruct.new, condition: true, only: [:id, :title], serializable: true) + expect(representation).to eq(id: nil, title: nil) + end + + it 'does not return fields specified in the except option' do + subject.expose(:id) + subject.expose(:name, as: :title) + subject.expose(:phone, as: :phone_number) + + representation = subject.represent(OpenStruct.new, condition: true, except: [:phone_number], serializable: true) + expect(representation).to eq(id: nil, title: nil) + end + end + + context 'attribute that is an entity itself' do + it 'returns correctly the children entity attributes' do + user_entity = Class.new(Grape::Entity) + user_entity.expose(:id, :name, :email) + + nephew_entity = Class.new(Grape::Entity) + nephew_entity.expose(:id, :name, :email) + + subject.expose(:id, :name, :phone) + subject.expose(:user, using: user_entity) + subject.expose(:nephew, using: nephew_entity) + + representation = subject.represent(OpenStruct.new(user: {}), + only: [:id, :name, :user], except: [:nephew], serializable: true) + expect(representation).to eq(id: nil, name: nil, user: { id: nil, name: nil, email: nil }) + end + end + end end describe '.present_collection' do it 'make the objects accessible' do subject.present_collection true @@ -621,10 +771,34 @@ expect(representation['things'].size).to eq 4 expect(representation['things'].reject { |r| r.is_a?(subject) }).to be_empty end end end + + context 'inheriting from parent entity' do + before(:each) do + subject.root 'things', 'thing' + end + + it 'inherits single root' do + child_class = Class.new(subject) + representation = child_class.represent(Object.new) + expect(representation).to be_kind_of Hash + expect(representation).to have_key 'thing' + expect(representation['thing']).to be_kind_of(child_class) + end + + it 'inherits array root root' do + child_class = Class.new(subject) + representation = child_class.represent(4.times.map { Object.new }) + expect(representation).to be_kind_of Hash + expect(representation).to have_key('things') + expect(representation['things']).to be_kind_of Array + expect(representation['things'].size).to eq 4 + expect(representation['things'].reject { |r| r.is_a?(child_class) }).to be_empty + end + end end describe '#initialize' do it 'takes an object and an optional options hash' do expect { subject.new(Object.new) }.not_to raise_error @@ -675,35 +849,46 @@ it 'does not throw an exception when an attribute is not found on the object' do fresh_class.expose :name, :nonexistent_attribute, safe: true expect { fresh_class.new(model).serializable_hash }.not_to raise_error end - it "does not expose attributes that don't exist on the object" do + it 'exposes values of private method calls' do + some_class = Class.new do + define_method :name do + true + end + private :name + end + fresh_class.expose :name, safe: true + expect(fresh_class.new(some_class.new).serializable_hash).to eq(name: true) + end + + it "does expose attributes that don't exist on the object as nil" do fresh_class.expose :email, :nonexistent_attribute, :name, safe: true res = fresh_class.new(model).serializable_hash expect(res).to have_key :email - expect(res).not_to have_key :nonexistent_attribute + expect(res).to have_key :nonexistent_attribute expect(res).to have_key :name end it 'does expose attributes marked as safe if model is a hash object' do fresh_class.expose :name, safe: true res = fresh_class.new(name: 'myname').serializable_hash expect(res).to have_key :name end - it "does not expose attributes that don't exist on the object, even with criteria" do + it "does expose attributes that don't exist on the object as nil if criteria is true" do fresh_class.expose :email - fresh_class.expose :nonexistent_attribute, safe: true, if: lambda { false } - fresh_class.expose :nonexistent_attribute2, safe: true, if: lambda { true } + fresh_class.expose :nonexistent_attribute, safe: true, if: ->(_obj, _opts) { false } + fresh_class.expose :nonexistent_attribute2, safe: true, if: ->(_obj, _opts) { true } res = fresh_class.new(model).serializable_hash expect(res).to have_key :email expect(res).not_to have_key :nonexistent_attribute - expect(res).not_to have_key :nonexistent_attribute2 + expect(res).to have_key :nonexistent_attribute2 end end context 'without safe option' do it 'throws an exception when an attribute is not found on the object' do @@ -718,13 +903,13 @@ res = fresh_class.new(model).serializable_hash expect(res).to have_key :nonexistent_attribute end it 'does not expose attributes that are generated by a block but have not passed criteria' do - fresh_class.expose :nonexistent_attribute, proc: lambda { |_model, _opts| - 'I exist, but it is not yet my time to shine' - }, if: lambda { |_model, _opts| false } + fresh_class.expose :nonexistent_attribute, + proc: ->(_model, _opts) { 'I exist, but it is not yet my time to shine' }, + if: ->(_model, _opts) { false } res = fresh_class.new(model).serializable_hash expect(res).not_to have_key :nonexistent_attribute end end @@ -740,13 +925,13 @@ res = fresh_class.new(model).serializable_hash expect(res).to have_key :nonexistent_attribute end it 'does not expose attributes that are generated by a block but have not passed criteria' do - fresh_class.expose :nonexistent_attribute, proc: lambda { |_, _| - 'I exist, but it is not yet my time to shine' - }, if: lambda { |_, _| false } + fresh_class.expose :nonexistent_attribute, + proc: ->(_, _) { 'I exist, but it is not yet my time to shine' }, + if: ->(_, _) { false } res = fresh_class.new(model).serializable_hash expect(res).not_to have_key :nonexistent_attribute end context '#serializable_hash' do @@ -821,11 +1006,11 @@ def timestamp(date) date.strftime('%m/%d/%Y') end - expose :fantasies, format_with: lambda { |f| f.reverse } + expose :fantasies, format_with: ->(f) { f.reverse } end end it 'passes through bare expose attributes' do expect(subject.send(:value_for, :name)).to eq attributes[:name] @@ -1069,10 +1254,50 @@ fresh_class.expose :email, documentation: doc fresh_class.expose :birthday expect(subject.documentation).to eq(label: doc, email: doc) end + + context 'inherited documentation' do + it 'returns documentation from ancestor' do + doc = { type: 'foo', desc: 'bar' } + fresh_class.expose :name, documentation: doc + child_class = Class.new(fresh_class) + child_class.expose :email, documentation: doc + + expect(fresh_class.documentation).to eq(name: doc) + expect(child_class.documentation).to eq(name: doc, email: doc) + end + + it 'obeys unexposed attributes in subclass' do + doc = { type: 'foo', desc: 'bar' } + fresh_class.expose :name, documentation: doc + fresh_class.expose :email, documentation: doc + child_class = Class.new(fresh_class) + child_class.unexpose :email + + expect(fresh_class.documentation).to eq(name: doc, email: doc) + expect(child_class.documentation).to eq(name: doc) + end + + it 'obeys re-exposed attributes in subclass' do + doc = { type: 'foo', desc: 'bar' } + fresh_class.expose :name, documentation: doc + fresh_class.expose :email, documentation: doc + + child_class = Class.new(fresh_class) + child_class.unexpose :email + + nephew_class = Class.new(child_class) + new_doc = { type: 'todler', descr: '???' } + nephew_class.expose :email, documentation: new_doc + + expect(fresh_class.documentation).to eq(name: doc, email: doc) + expect(child_class.documentation).to eq(name: doc) + expect(nephew_class.documentation).to eq(name: doc, email: new_doc) + end + end end describe '#key_for' do it 'returns the attribute if no :as is set' do fresh_class.expose :name @@ -1118,11 +1343,11 @@ expect(subject.send(:conditions_met?, exposure_options, condition1: false)).to be true expect(subject.send(:conditions_met?, exposure_options, condition1: nil)).to be true end it 'only passes through proc :if exposure if it returns truthy value' do - exposure_options = { if: lambda { |_, opts| opts[:true] } } + exposure_options = { if: ->(_, opts) { opts[:true] } } expect(subject.send(:conditions_met?, exposure_options, true: false)).to be false expect(subject.send(:conditions_met?, exposure_options, true: true)).to be true end @@ -1136,10 +1361,10 @@ expect(subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true, other: true)).to be false expect(subject.send(:conditions_met?, exposure_options, condition1: false, condition2: false)).to be true end it 'only passes through proc :unless exposure if it returns falsy value' do - exposure_options = { unless: lambda { |_, options| options[:true] == true } } + exposure_options = { unless: ->(_, opts) { opts[:true] == true } } expect(subject.send(:conditions_met?, exposure_options, true: false)).to be true expect(subject.send(:conditions_met?, exposure_options, true: true)).to be false end end