Skip to content

Commit

Permalink
Merge pull request #464 from procore-oss/js/association-if-condition-fix
Browse files Browse the repository at this point in the history
Fix Bug when using `if: Symbol` on Association
  • Loading branch information
lessthanjacob authored Sep 23, 2024
2 parents 30e355d + 853b362 commit 7cc97b4
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 13 deletions.
5 changes: 3 additions & 2 deletions lib/blueprinter/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions lib/blueprinter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
52 changes: 47 additions & 5 deletions spec/integrations/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions spec/integrations/shared/base_render_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
21 changes: 15 additions & 6 deletions spec/units/association_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7cc97b4

Please sign in to comment.