spec/grape_entity/entity_spec.rb in grape-entity-0.6.1 vs spec/grape_entity/entity_spec.rb in grape-entity-0.7.0

- old
+ new

@@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'spec_helper' require 'ostruct' describe Grape::Entity do let(:fresh_class) { Class.new(Grape::Entity) } @@ -25,11 +27,13 @@ expect { subject.expose :name, :email, as: :foo }.to raise_error ArgumentError expect { subject.expose :name, as: :foo }.not_to raise_error end it 'makes sure that :format_with as a proc cannot be used with a block' do - expect { subject.expose :name, format_with: proc {} {} }.to raise_error ArgumentError + # rubocop:disable 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 expect { subject.expose :name, unknown: nil }.to raise_error ArgumentError end @@ -62,10 +66,134 @@ expect(subject.represent(nested_hash).serializable_hash).to eq(special: { like_nested_hash: '12' }) end end end + context 'with :expose_nil option' do + let(:a) { nil } + let(:b) { nil } + let(:c) { 'value' } + + context 'when model is a PORO' do + let(:model) { Model.new(a, b, c) } + + before do + stub_const 'Model', Class.new + Model.class_eval do + attr_accessor :a, :b, :c + + def initialize(a, b, c) + @a = a + @b = b + @c = c + end + end + end + + context 'when expose_nil option is not provided' do + it 'exposes nil attributes' do + subject.expose(:a) + subject.expose(:b) + subject.expose(:c) + expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value') + end + end + + context 'when expose_nil option is true' do + it 'exposes nil attributes' do + subject.expose(:a, expose_nil: true) + subject.expose(:b, expose_nil: true) + subject.expose(:c) + expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value') + end + end + + context 'when expose_nil option is false' do + it 'does not expose nil attributes' do + subject.expose(:a, expose_nil: false) + subject.expose(:b, expose_nil: false) + subject.expose(:c) + expect(subject.represent(model).serializable_hash).to eq(c: 'value') + end + + it 'is only applied per attribute' do + subject.expose(:a, expose_nil: false) + subject.expose(:b) + subject.expose(:c) + expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value') + end + + it 'raises an error when applied to multiple attribute exposures' do + expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError + end + end + end + + context 'when model is a hash' do + let(:model) { { a: a, b: b, c: c } } + + context 'when expose_nil option is not provided' do + it 'exposes nil attributes' do + subject.expose(:a) + subject.expose(:b) + subject.expose(:c) + expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value') + end + end + + context 'when expose_nil option is true' do + it 'exposes nil attributes' do + subject.expose(:a, expose_nil: true) + subject.expose(:b, expose_nil: true) + subject.expose(:c) + expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value') + end + end + + context 'when expose_nil option is false' do + it 'does not expose nil attributes' do + subject.expose(:a, expose_nil: false) + subject.expose(:b, expose_nil: false) + subject.expose(:c) + expect(subject.represent(model).serializable_hash).to eq(c: 'value') + end + + it 'is only applied per attribute' do + subject.expose(:a, expose_nil: false) + subject.expose(:b) + subject.expose(:c) + expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value') + end + + it 'raises an error when applied to multiple attribute exposures' do + expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError + end + end + end + + context 'with nested structures' do + let(:model) { { a: a, b: b, c: { d: nil, e: nil, f: { g: nil, h: nil } } } } + + context 'when expose_nil option is false' do + it 'does not expose nil attributes' do + subject.expose(:a, expose_nil: false) + subject.expose(:b) + subject.expose(:c) do + subject.expose(:d, expose_nil: false) + subject.expose(:e) + subject.expose(:f) do + subject.expose(:g, expose_nil: false) + subject.expose(:h) + end + end + + expect(subject.represent(model).serializable_hash).to eq(b: nil, c: { e: nil, f: { h: nil } }) + end + end + end + end + context 'with a block' do it 'errors out if called with multiple attributes' do expect { subject.expose(:name, :email) { true } }.to raise_error ArgumentError end @@ -110,10 +238,34 @@ subject.expose(:size) { |_| self } expect(subject.represent({}).value_for(:size)).to be_an_instance_of fresh_class end end + context 'with block passed via &' do + it 'with does not pass options when block is passed via &' do + class SomeObject + def method_without_args + 'result' + end + end + + subject.expose :that_method_without_args do |object| + object.method_without_args + end + + subject.expose :that_method_without_args_again, &:method_without_args + + object = SomeObject.new + + value = subject.represent(object).value_for(:that_method_without_args) + expect(value).to eq('result') + + value2 = subject.represent(object).value_for(:that_method_without_args_again) + expect(value2).to eq('result') + end + end + context 'with no parameters passed to the block' do it 'adds a nested exposure' do subject.expose :awesome do subject.expose :nested do subject.expose :moar_nested, as: 'weee' @@ -129,11 +281,11 @@ expect(awesome).to be_nesting expect(nested).to_not be_nil expect(another_nested).to_not be_nil expect(another_nested.using_class_name).to eq('Awesome') expect(moar_nested).to_not be_nil - expect(moar_nested.key).to eq(:weee) + expect(moar_nested.key(subject)).to eq(:weee) end it 'represents the exposure as a hash of its nested.root_exposures' do subject.expose :awesome do subject.expose(:nested) { |_| 'value' } @@ -322,10 +474,19 @@ end expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'bar') expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'foo') end + + it 'overrides parent class exposure' do + subject.expose :name + child_class = Class.new(subject) + child_class.expose :name, as: :child_name + + expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar') + expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(child_name: 'bar') + end end context 'register formatters' do let(:date_formatter) { ->(date) { date.strftime('%m/%d/%Y') } } @@ -494,11 +655,11 @@ expose :awesome_thing, as: :extra_smooth end end exposure = subject.find_exposure(:awesome_thing) - expect(exposure.key).to eq :extra_smooth + expect(exposure.key(subject)).to eq :extra_smooth end it 'merges nested :if option' do match_proc = ->(_obj, _opts) { true } @@ -594,10 +755,38 @@ end exposure = subject.find_exposure(:awesome_thing) expect(exposure.documentation).to eq(desc: 'Other description.') end + + it 'propagates expose_nil option' do + subject.class_eval do + with_options(expose_nil: false) do + expose :awesome_thing + end + end + + exposure = subject.find_exposure(:awesome_thing) + expect(exposure.conditions[0].inversed?).to be true + expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true + end + + it 'overrides nested :expose_nil option' do + subject.class_eval do + with_options(expose_nil: true) do + expose :awesome_thing, expose_nil: false + expose :other_awesome_thing + end + end + + exposure = subject.find_exposure(:awesome_thing) + expect(exposure.conditions[0].inversed?).to be true + expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true + # Conditions are only added for exposures that do not expose nil + exposure = subject.find_exposure(:other_awesome_thing) + expect(exposure.conditions[0]).to be_nil + end end describe '.represent' do it 'returns a single entity if called with one object' do expect(subject.represent(Object.new)).to be_kind_of(subject) @@ -651,11 +840,11 @@ 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) + representation = subject.represent(OpenStruct.new, only: %i[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) @@ -664,11 +853,11 @@ 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], + only: %i[name phone], except: [:phone], serializable: true) expect(representation).to eq(name: nil) end context 'with strings or symbols passed to only and except' do @@ -734,11 +923,11 @@ 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) + representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, { user: %i[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) @@ -757,11 +946,11 @@ 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]], + only: [:id, :name, :phone, user: %i[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 @@ -769,21 +958,21 @@ 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) + representation = subject.represent(OpenStruct.new, condition: true, only: %i[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) + representation = subject.represent(OpenStruct.new, condition: true, except: %i[phone mobile_phone], serializable: true) expect(representation).to eq(id: nil, name: nil) end it 'choses proper exposure according to condition' do strategy1 = ->(_obj, _opts) { 'foo' } @@ -861,11 +1050,11 @@ 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) + representation = subject.represent(OpenStruct.new, condition: true, only: %i[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) @@ -888,11 +1077,11 @@ 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) + only: %i[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 @@ -1339,12 +1528,12 @@ context 'with projections passed in options' do it 'allows to pass different :only and :except params using the same instance' do fresh_class.expose :a, :b, :c presenter = fresh_class.new(a: 1, b: 2, c: 3) - expect(presenter.serializable_hash(only: [:a, :b])).to eq(a: 1, b: 2) - expect(presenter.serializable_hash(only: [:b, :c])).to eq(b: 2, c: 3) + expect(presenter.serializable_hash(only: %i[a b])).to eq(a: 1, b: 2) + expect(presenter.serializable_hash(only: %i[b c])).to eq(b: 2, c: 3) end end end describe '#inspect' do @@ -1760,43 +1949,9 @@ it 'instantiates with options if provided' do expect(instance.entity(awesome: true).options).to eq(awesome: true) end end - end - end - end - - describe Grape::Entity::Options do - module EntitySpec - class Crystalline - attr_accessor :prop1, :prop2 - - def initialize - @prop1 = 'value1' - @prop2 = 'value2' - end - end - - class CrystallineEntity < Grape::Entity - expose :prop1, if: ->(_, options) { options.fetch(:signal) } - expose :prop2, if: ->(_, options) { options.fetch(:beam, 'destructive') == 'destructive' } - end - end - - context '#fetch' do - it 'without passing in a required option raises KeyError' do - expect { EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new).as_json }.to raise_error KeyError - end - - it 'passing in a required option will expose the values' do - crystalline_entity = EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new, signal: true) - expect(crystalline_entity.as_json).to eq(prop1: 'value1', prop2: 'value2') - end - - it 'with an option that is not default will not expose that value' do - crystalline_entity = EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new, signal: true, beam: 'intermittent') - expect(crystalline_entity.as_json).to eq(prop1: 'value1') end end end end end