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

Allow associations to be defined with a block #106

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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you've seen my library 😂

{"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:
Expand Down
34 changes: 25 additions & 9 deletions lib/blueprinter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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: {method => 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],
Expand All @@ -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
Expand All @@ -127,15 +129,29 @@ 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 = {})
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing https://github.com/procore/blueprinter/blob/master/lib/blueprinter/extractor.rb#L11-L13
makes this break: https://github.com/procore/blueprinter/blob/master/spec/integrations/shared/base_render_examples.rb#L41

This may be a breaking change, so I'm not sure if we want to change this here. Maybe I should just apply the other changes you mentioned but leave self.extract?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize there were tests for that. OK let's leave that for now. Thanks!

end

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

# Generates a JSON formatted String.
Expand Down Expand Up @@ -220,7 +236,7 @@ def self.prepare(object, view_name:, local_options:)
# @return [Array<Symbol>] 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

Expand Down
3 changes: 0 additions & 3 deletions lib/blueprinter/extractor.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/blueprinter/extractors/association_extractor.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module Blueprinter
class AssociationExtractor < Extractor
def extract(association_name, object, local_options, options={})
value = object.public_send(association_name)
return (value || options[:default]) if value.nil?
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)
end
Expand Down
7 changes: 6 additions & 1 deletion lib/blueprinter/extractors/auto_extractor.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/blueprinter/extractors/block_extractor.rb
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions spec/integrations/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down