Skip to content

Commit

Permalink
Allow identifiers to be defined with a block
Browse files Browse the repository at this point in the history
Now that `::association` and `::field` receive an optional block,
`::identifier` should receive it as well.

Since all three methods receive an optional block, the block extraction
mechanism was moved to `AutoExtractor`. Removing this mechanism from
each individual method made them very similar. `::association` now uses
`::field` instead of directly adding the field to the current view.
  • Loading branch information
hugopeixoto committed Oct 25, 2018
1 parent f849049 commit 22b04ff
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 31 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,32 @@ Output:
}
```

#### Defining an identifier directly in the Blueprint

You can also pass a block to an identifier:

```ruby
class UserBlueprint < Blueprinter::Base
identifier :uuid do |user, options|
options[:current_user].anonymize(user.uuid)
end
end
```

Usage:

```ruby
puts UserBlueprint.render(user, current_user: current_user)
```

Output:

```json
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
}
```

#### Defining an association directly in the Blueprint

You can also pass a block to an association:
Expand Down
63 changes: 35 additions & 28 deletions lib/blueprinter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class Base
# @param name [Symbol] to rename the identifier key in the JSON
# output. Defaults to method given.
# @param extractor [AssociationExtractor,AutoExtractor,BlockExtractor,HashExtractor,PublicSendExtractor]
# @yield [object, options] The object and the options passed to render are
# also yielded to the block.
#
# Kind of extractor to use.
# Either define your own or use Blueprinter's premade extractors.
# Defaults to AutoExtractor
Expand All @@ -36,13 +39,22 @@ class Base
# # other code
# end
#
# @example Passing a block to be evaluated as the value.
# class UserBlueprint < Blueprinter::Base
# identifier :uuid do |user, options|
# options[:current_user].anonymize(user.uuid)
# end
# end
#
# @return [Field] A Field object
def self.identifier(method, name: method, extractor: AutoExtractor.new)
view_collection[:identifier] << Field.new(method, name, extractor, self)
end

def self.inherited(subclass)
subclass.send(:view_collection).inherit(view_collection)
def self.identifier(method, name: method, extractor: AutoExtractor.new, &block)
view_collection[:identifier] << Field.new(
method,
name,
extractor,
self,
block: block,
)
end

# Specify a field or method name to be included for serialization.
Expand Down Expand Up @@ -98,16 +110,13 @@ def self.inherited(subclass)
#
# @return [Field] A Field object
def self.field(method, options = {}, &block)
options = if block_given?
{name: method, extractor: BlockExtractor.new, block: block}
else
{name: method, extractor: AutoExtractor.new}
end.merge(options)
current_view << Field.new(method,
options[:name],
options[:extractor],
self,
options)
current_view << Field.new(
method,
options.fetch(:name) { method },
options.fetch(:extractor) { AutoExtractor.new },
self,
options.merge(block: block),
)
end

# Specify an associated object to be included for serialization.
Expand Down Expand Up @@ -141,19 +150,12 @@ def self.field(method, options = {}, &block)
# @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.new, block: block)
else
options.merge(extractor: AutoExtractor.new)
end

current_view << Field.new(method,
name,
AssociationExtractor.new,
self,
options.merge(association: true))
field(
method,
options.merge(association: true, extractor: AssociationExtractor.new),
&block
)
end

# Generates a JSON formatted String.
Expand Down Expand Up @@ -313,6 +315,11 @@ def self.view(view_name)

private

def self.inherited(subclass)
subclass.send(:view_collection).inherit(view_collection)
end
private_class_method :inherited

def self.object_to_hash(object, view_name:, local_options:)
view_collection.fields_for(view_name).each_with_object({}) do |field, hash|
next if field.skip?(object, local_options)
Expand Down
6 changes: 5 additions & 1 deletion lib/blueprinter/extractors/association_extractor.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
module Blueprinter
class AssociationExtractor < Extractor
def initialize
@extractor = AutoExtractor.new
end

def extract(association_name, object, local_options, options={})
value = options[:extractor].extract(association_name, object, local_options, options)
value = @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)
Expand Down
14 changes: 12 additions & 2 deletions lib/blueprinter/extractors/auto_extractor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@ class AutoExtractor < Extractor
def initialize
@hash_extractor = HashExtractor.new
@public_send_extractor = PublicSendExtractor.new
@block_extractor = BlockExtractor.new
end

def extract(field_name, object, local_options, options = {})
extractor = object.is_a?(Hash) ? @hash_extractor : @public_send_extractor
extraction = extractor.extract(field_name, object, local_options, options)
extraction = extractor(object, options).extract(field_name, object, local_options, options)
options.key?(:datetime_format) ? format_datetime(extraction, options[:datetime_format]) : extraction
end

private

def extractor(object, options)
if options[:block]
@block_extractor
elsif object.is_a?(Hash)
@hash_extractor
else
@public_send_extractor
end
end

def format_datetime(datetime, format)
datetime.strftime(format)
rescue NoMethodError
Expand Down
28 changes: 28 additions & 0 deletions spec/integrations/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,34 @@
end
end

describe 'identifier' do
let(:rendered) do
blueprint.render_as_hash(OpenStruct.new(uid: 42))
end

let(:blueprint) do
Class.new(Blueprinter::Base) do
identifier :uid
end
end

it "renders identifier" do
expect(rendered).to eq(uid: 42)
end

describe 'Given a block is passed' do
let(:blueprint) do
Class.new(Blueprinter::Base) do
identifier(:id) { |object, _| object.uid * 2 }
end
end

it "renders result of block" do
expect(rendered).to eq(id: 84)
end
end
end

describe 'Using the ApplicationBlueprint pattern' do
let(:obj) { OpenStruct.new(id: 1, name: 'Meg', age: 32) }
let(:application_blueprint) do
Expand Down

0 comments on commit 22b04ff

Please sign in to comment.