From 125882aa13a74f6d9ab2525ca998d988ca09d395 Mon Sep 17 00:00:00 2001 From: Hugo Peixoto Date: Mon, 15 Oct 2018 23:53:08 +0100 Subject: [PATCH 1/2] Allow associations to be defined with a block --- lib/blueprinter/base.rb | 19 +++++++++++++------ .../extractors/association_extractor.rb | 8 ++++++-- lib/blueprinter/extractors/block_extractor.rb | 2 +- spec/integrations/base_spec.rb | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/lib/blueprinter/base.rb b/lib/blueprinter/base.rb index 02299aa4..1ed0b699 100644 --- a/lib/blueprinter/base.rb +++ b/lib/blueprinter/base.rb @@ -97,7 +97,7 @@ def self.inherited(subclass) # @return [Field] A Field object def self.field(method, options = {}, &block) options = if block_given? - {name: method, extractor: BlockExtractor, block: {method => block}} + {name: method, extractor: BlockExtractor, block: block} else {name: method, extractor: AutoExtractor} end.merge(options) @@ -128,14 +128,21 @@ def self.field(method, options = {}, &block) # end # # @return [Field] A Field object - def self.association(method, options = {}) + def self.association(method, options = {}, &block) raise BlueprinterError, 'blueprint required' unless options[:blueprint] name = options.delete(:name) || method + + options = if block_given? + options.merge(extractor: BlockExtractor, block: block) + else + options.merge(extractor: AutoExtractor) + end + current_view << Field.new(method, - name, - AssociationExtractor, - self, - options.merge(association: true)) + name, + AssociationExtractor.new(options[:extractor]), + self, + options.merge(association: true)) end # Generates a JSON formatted String. diff --git a/lib/blueprinter/extractors/association_extractor.rb b/lib/blueprinter/extractors/association_extractor.rb index 2c4c9679..43147f49 100644 --- a/lib/blueprinter/extractors/association_extractor.rb +++ b/lib/blueprinter/extractors/association_extractor.rb @@ -1,8 +1,12 @@ module Blueprinter class AssociationExtractor < Extractor + def initialize(field_extractor) + @field_extractor = field_extractor + end + def extract(association_name, object, local_options, options={}) - value = object.public_send(association_name) - return (value || options[:default]) if value.nil? + value = @field_extractor.extract(association_name, object, local_options, options) + return options[:default] if value.nil? view = options[:view] || :default options[:blueprint].prepare(value, view_name: view, local_options: local_options) end diff --git a/lib/blueprinter/extractors/block_extractor.rb b/lib/blueprinter/extractors/block_extractor.rb index f031ffc5..d9eb2183 100644 --- a/lib/blueprinter/extractors/block_extractor.rb +++ b/lib/blueprinter/extractors/block_extractor.rb @@ -1,7 +1,7 @@ module Blueprinter class BlockExtractor < Extractor def extract(field_name, object, local_options, options = {}) - options[:block][field_name].call(object, local_options) + options[:block].call(object, local_options) end end end diff --git a/spec/integrations/base_spec.rb b/spec/integrations/base_spec.rb index 0b6b695b..827a5963 100644 --- a/spec/integrations/base_spec.rb +++ b/spec/integrations/base_spec.rb @@ -84,6 +84,22 @@ end it('returns json with association') { should eq(result) } end + context 'Given block is passed' do + let(:blueprint) do + vehicle_blueprint = Class.new(Blueprinter::Base) do + fields :make + end + + Class.new(Blueprinter::Base) do + identifier :id + association(:automobiles, blueprint: vehicle_blueprint) { |o| o.vehicles } + end + end + let(:result) do + '{"id":' + obj_id + ',"automobiles":[{"make":"Super Car"}]}' + end + it('returns json with aliased association') { should eq(result) } + end context 'Given no associated blueprint is given' do let(:blueprint) do Class.new(Blueprinter::Base) do From 507395340dd87d31a71ef52b9258bd32d023be10 Mon Sep 17 00:00:00 2001 From: Hugo Peixoto Date: Mon, 15 Oct 2018 23:58:32 +0100 Subject: [PATCH 2/2] Optimize extractor object creation --- README.md | 38 +++++++++++++++++++ lib/blueprinter/base.rb | 23 +++++++---- lib/blueprinter/extractor.rb | 3 -- .../extractors/association_extractor.rb | 6 +-- lib/blueprinter/extractors/auto_extractor.rb | 7 +++- 5 files changed, 61 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 800457e5..7976ed20 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,44 @@ Output: } ``` +#### Defining an association directly in the Blueprint + +You can also pass a block to an association: + +```ruby +class ProjectBlueprint < Blueprinter::Base + identifier :uuid + field :name +end + +class UserBlueprint < Blueprinter::Base + identifier :uuid + + association :projects, blueprint: ProjectBlueprint do |user| + user.projects + user.company.projects + end +end +``` + +Usage: + +```ruby +puts UserBlueprint.render(user) +``` + +Output: + +```json +{ + "uuid": "733f0758-8f21-4719-875f-262c3ec743af", + "projects": [ + {"uuid": "b426a1e6-ac41-45ab-bfef-970b9a0b4289", "name": "query-console"}, + {"uuid": "5bd84d6c-4fd2-4e36-ae31-c137e39be542", "name": "blueprinter"}, + {"uuid": "785f5cd4-7d8d-4779-a6dd-ec5eab440eff", "name": "uncontrollable"} + ] +} +``` + ### Passing additional properties to `render` `render` takes an options hash which you can pass additional properties, allowing you to utilize those additional properties in the `field` block. For example: diff --git a/lib/blueprinter/base.rb b/lib/blueprinter/base.rb index 1ed0b699..a9acf94f 100644 --- a/lib/blueprinter/base.rb +++ b/lib/blueprinter/base.rb @@ -37,7 +37,7 @@ class Base # end # # @return [Field] A Field object - def self.identifier(method, name: method, extractor: AutoExtractor) + def self.identifier(method, name: method, extractor: AutoExtractor.new) view_collection[:identifier] << Field.new(method, name, extractor, self) end @@ -97,9 +97,9 @@ def self.inherited(subclass) # @return [Field] A Field object def self.field(method, options = {}, &block) options = if block_given? - {name: method, extractor: BlockExtractor, block: block} + {name: method, extractor: BlockExtractor.new, block: block} else - {name: method, extractor: AutoExtractor} + {name: method, extractor: AutoExtractor.new} end.merge(options) current_view << Field.new(method, options[:name], @@ -119,6 +119,8 @@ def self.field(method, options = {}, &block) # JSON output. # @option options [Symbol] :view Specify the view to use or fall back to # to the :default view. + # @yield [Object] The object passed to `render` is also passed to the + # block. # # @example Specifying an association # class UserBlueprint < Blueprinter::Base @@ -127,20 +129,27 @@ def self.field(method, options = {}, &block) # # code # end # + # @example Passing a block to be evaluated as the value. + # class UserBlueprint < Blueprinter::Base + # association :vehicles, blueprint: VehiclesBlueprint do |user| + # user.vehicles + user.company.vehicles + # end + # end + # # @return [Field] A Field object def self.association(method, options = {}, &block) raise BlueprinterError, 'blueprint required' unless options[:blueprint] name = options.delete(:name) || method options = if block_given? - options.merge(extractor: BlockExtractor, block: block) + options.merge(extractor: BlockExtractor.new, block: block) else - options.merge(extractor: AutoExtractor) + options.merge(extractor: AutoExtractor.new) end current_view << Field.new(method, name, - AssociationExtractor.new(options[:extractor]), + AssociationExtractor.new, self, options.merge(association: true)) end @@ -227,7 +236,7 @@ def self.prepare(object, view_name:, local_options:) # @return [Array] an array of field names def self.fields(*field_names) field_names.each do |field_name| - current_view << Field.new(field_name, field_name, AutoExtractor, self) + current_view << Field.new(field_name, field_name, AutoExtractor.new, self) end end diff --git a/lib/blueprinter/extractor.rb b/lib/blueprinter/extractor.rb index e4bf559b..c1b8d3d5 100644 --- a/lib/blueprinter/extractor.rb +++ b/lib/blueprinter/extractor.rb @@ -1,9 +1,6 @@ # @api private module Blueprinter class Extractor - def initialize - end - def extract(field_name, object, local_options, options={}) fail NotImplementedError, "An Extractor must implement #extract" end diff --git a/lib/blueprinter/extractors/association_extractor.rb b/lib/blueprinter/extractors/association_extractor.rb index 43147f49..25b6c76c 100644 --- a/lib/blueprinter/extractors/association_extractor.rb +++ b/lib/blueprinter/extractors/association_extractor.rb @@ -1,11 +1,7 @@ module Blueprinter class AssociationExtractor < Extractor - def initialize(field_extractor) - @field_extractor = field_extractor - end - def extract(association_name, object, local_options, options={}) - value = @field_extractor.extract(association_name, object, local_options, options) + value = options[:extractor].extract(association_name, object, local_options, options) return options[:default] if value.nil? view = options[:view] || :default options[:blueprint].prepare(value, view_name: view, local_options: local_options) diff --git a/lib/blueprinter/extractors/auto_extractor.rb b/lib/blueprinter/extractors/auto_extractor.rb index c82f757f..ef712055 100644 --- a/lib/blueprinter/extractors/auto_extractor.rb +++ b/lib/blueprinter/extractors/auto_extractor.rb @@ -1,7 +1,12 @@ module Blueprinter class AutoExtractor < Extractor + def initialize + @hash_extractor = HashExtractor.new + @public_send_extractor = PublicSendExtractor.new + end + def extract(field_name, object, local_options, options = {}) - extractor = object.is_a?(Hash) ? HashExtractor : PublicSendExtractor + extractor = object.is_a?(Hash) ? @hash_extractor : @public_send_extractor extraction = extractor.extract(field_name, object, local_options, options) options.key?(:datetime_format) ? format_datetime(extraction, options[:datetime_format]) : extraction end