From 1df653c07d887f32187429dda4ef32c139e44a65 Mon Sep 17 00:00:00 2001 From: Jacob Sheehy Date: Mon, 16 Sep 2024 12:30:05 -0400 Subject: [PATCH] [fix] Fixes an issue when using a symbol if condition with association Signed-off-by: Jacob Sheehy --- lib/blueprinter/association.rb | 5 +- lib/blueprinter/base.rb | 1 + spec/integrations/base_spec.rb | 52 +++++++++++++++++-- .../shared/base_render_examples.rb | 30 +++++++++++ spec/units/association_spec.rb | 21 +++++--- 5 files changed, 96 insertions(+), 13 deletions(-) diff --git a/lib/blueprinter/association.rb b/lib/blueprinter/association.rb index a8d563e6..eebaf586 100644 --- a/lib/blueprinter/association.rb +++ b/lib/blueprinter/association.rb @@ -11,18 +11,19 @@ class Association < Field # @param name [Symbol] The name of the association as it will appear when rendered # @param blueprint [Blueprinter::Base] The blueprint to use for rendering the association # @param view [Symbol] The view to use in conjunction with the blueprint + # @param parent_blueprint [Blueprinter::Base] The blueprint that this association is being defined within # @param extractor [Blueprinter::Extractor] The extractor to use when retrieving the associated data # @param options [Hash] # # @return [Blueprinter::Association] - def initialize(method:, name:, blueprint:, view:, extractor: AssociationExtractor.new, options: {}) + def initialize(method:, name:, blueprint:, view:, parent_blueprint:, extractor: AssociationExtractor.new, options: {}) BlueprintValidator.validate!(blueprint) super( method, name, extractor, - blueprint, + parent_blueprint, options.merge( blueprint: blueprint, view: view, diff --git a/lib/blueprinter/base.rb b/lib/blueprinter/base.rb index fcf7438a..b40f16ba 100644 --- a/lib/blueprinter/base.rb +++ b/lib/blueprinter/base.rb @@ -156,6 +156,7 @@ def self.association(method, options = {}, &block) name: options.fetch(:name) { method }, extractor: options.fetch(:extractor) { AssociationExtractor.new }, blueprint: options.fetch(:blueprint), + parent_blueprint: self, view: options.fetch(:view, :default), options: options.except( :name, diff --git a/spec/integrations/base_spec.rb b/spec/integrations/base_spec.rb index a731706d..699d8e72 100644 --- a/spec/integrations/base_spec.rb +++ b/spec/integrations/base_spec.rb @@ -199,7 +199,6 @@ def blueprint end end end - context "Given default_if option is Blueprinter::EMPTY_COLLECTION" do before { vehicle.update(user: nil) } after { vehicle.update(user: obj) } @@ -260,7 +259,6 @@ def blueprint to raise_error(ArgumentError, /:blueprint must be provided when defining an association/) end end - context 'Given an association :options option' do let(:result) { '{"id":' + obj_id + ',"vehicles":[{"make":"Super Car Enhanced"}]}' } let(:blueprint) do @@ -277,7 +275,6 @@ def blueprint end it('returns json using the association options') { should eq(result) } end - context 'Given an association :extractor option' do let(:result) { '{"id":' + obj_id + ',"vehicles":[{"make":"SUPER CAR"}]}' } let(:blueprint) do @@ -296,7 +293,6 @@ def extract(association_name, object, _local_options, _options={}) end it('returns json derived from a custom extractor') { should eq(result) } end - context 'when a view is specified' do let(:vehicle) { create(:vehicle, :with_model) } let(:blueprint) do @@ -319,7 +315,6 @@ def extract(association_name, object, _local_options, _options={}) expect(blueprint.render(obj)).to eq(result) end end - context 'Given included view with re-defined association' do let(:blueprint) do vehicle_blueprint = Class.new(Blueprinter::Base) do @@ -364,6 +359,53 @@ def extract(association_name, object, _local_options, _options={}) expect(blueprint.render(obj, view: :with_height)).to eq(result_with_height) end end + context 'when if option is provided' do + let(:vehicle) { create(:vehicle, make: 'Super Car') } + let(:user_without_cars) { create(:user, vehicles: []) } + let(:user_with_cars) { create(:user, vehicles: [vehicle]) } + + let(:blueprint) do + vehicle_blueprint = Class.new(Blueprinter::Base) do + fields :make + end + Class.new(Blueprinter::Base) do + identifier :id + association :vehicles, blueprint: vehicle_blueprint, if: ->(_field_name, object, _local_opts) { object.vehicles.present? } + end + end + it 'does not render the association if the if condition is not met' do + expect(blueprint.render(user_without_cars)).to eq("{\"id\":#{user_without_cars.id}}") + end + it 'renders the association if the if condition is met' do + expect(blueprint.render(user_with_cars)).to eq("{\"id\":#{user_with_cars.id},\"vehicles\":[{\"make\":\"Super Car\"}]}") + end + + context 'and if option is a symbol' do + let(:blueprint) do + vehicle_blueprint = Class.new(Blueprinter::Base) do + fields :make + end + Class.new(Blueprinter::Base) do + identifier :id + association :vehicles, blueprint: vehicle_blueprint, if: :has_vehicles? + association :vehicles, name: :cars, blueprint: vehicle_blueprint, if: :has_cars? + + def self.has_vehicles?(_field_name, object, local_options) + false + end + + def self.has_cars?(_field_name, object, local_options) + true + end + end + end + + it 'renders the association based on evaluating the symbol as a method on the blueprint' do + expect(blueprint.render(user_with_cars)). + to eq("{\"id\":#{user_with_cars.id},\"cars\":[{\"make\":\"Super Car\"}]}") + end + end + end end context "Given association is nil" do diff --git a/spec/integrations/shared/base_render_examples.rb b/spec/integrations/shared/base_render_examples.rb index 6113ffcc..172a3ffa 100644 --- a/spec/integrations/shared/base_render_examples.rb +++ b/spec/integrations/shared/base_render_examples.rb @@ -420,6 +420,36 @@ def self.unless_method(_field_name, _object, _options) end end + context 'Given blueprint has fields with if conditional' do + let(:result) { '{"id":' + obj_id + '}' } + let(:blueprint) do + Class.new(Blueprinter::Base) do + identifier :id + field :first_name, if: ->(_field_name, _object, _local_opts) { false } + end + end + it 'does not render the field if condition is false' do + expect(blueprint.render(obj)).to eq(result) + end + + context 'when if value is a symbol' do + let(:result) { '{"id":' + obj_id + '}' } + let(:blueprint) do + Class.new(Blueprinter::Base) do + identifier :id + field :first_name, if: :if_method + + def self.if_method(_field_name, _object, _local_opts) + false + end + end + end + it 'does not render the field if the result of sending symbol to Blueprint is false' do + should eq(result) + end + end + end + context 'Given blueprint has :meta without :root' do let(:blueprint) { blueprint_with_block } it('raises a BlueprinterError') { diff --git a/spec/units/association_spec.rb b/spec/units/association_spec.rb index 01e8c148..b8355732 100644 --- a/spec/units/association_spec.rb +++ b/spec/units/association_spec.rb @@ -5,33 +5,42 @@ describe Blueprinter::Association do describe '#initialize' do let(:blueprint) { Class.new(Blueprinter::Base) } + let(:parent_blueprint) { Class.new(Blueprinter::Base) } + let(:if_condition) { -> { true } } let(:args) do { method: :method, name: :name, extractor: :extractor, blueprint: blueprint, + parent_blueprint: parent_blueprint, view: :view, - options: { if: -> { true } } + options: { if: if_condition } } end - it 'returns an instance of Blueprinter::Association' do - expect(Blueprinter::Association.new(**args)). - to be_instance_of(Blueprinter::Association) + it 'returns an instance of Blueprinter::Association with expected values', aggregate_failures: true do + association = described_class.new(**args) + expect(association).to be_instance_of(described_class) + expect(association.method).to eq(:method) + expect(association.name).to eq(:name) + expect(association.extractor).to eq(:extractor) + expect(association.blueprint).to eq(parent_blueprint) + expect(association.options).to eq({ if: if_condition, blueprint: blueprint, view: :view, association: true }) end + context 'when provided :blueprint is invalid' do let(:blueprint) { Class.new } it 'raises a Blueprinter::InvalidBlueprintError' do - expect { Blueprinter::Association.new(**args) }. + expect { described_class.new(**args) }. to raise_error(Blueprinter::Errors::InvalidBlueprint) end end context 'when an extractor is not provided' do it 'defaults to using AssociationExtractor' do - expect(Blueprinter::Association.new(**args.except(:extractor)).extractor). + expect(described_class.new(**args.except(:extractor)).extractor). to be_an_instance_of(Blueprinter::AssociationExtractor) end end