Skip to content
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

Fix Bug when using if: Symbol on Association #464

Merged
merged 3 commits into from
Sep 23, 2024
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
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