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 transformers to be included across views #372

Merged
merged 14 commits into from
Jan 12, 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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,28 @@ class UserBlueprint < Blueprinter::Base
end
```

#### Transform across views

Transformers can be included across views:

```ruby
class UserBlueprint < Blueprinter::Base
transform DefaultTransformer

view :normal do
transform ViewTransformer
end

view :extended do
include_view :normal
end
end
```

Both the `normal` and `extended` views have `DefaultTransformer` and `ViewTransformer` applied.

Transformers are executed in a top-down order, so `DefaultTransformer` will be executed first, followed by `ViewTransformer`.

#### Global Transforms

You can also specify global default transformers. Create one or more transformer classes extending from `Blueprinter::Transformer` and set the `default_transformers` configuration
Expand Down
4 changes: 0 additions & 4 deletions lib/blueprinter/view.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ def initialize(name, fields: {}, included_view_names: [], excluded_view_names: [
@sort_by_definition = Blueprinter.configuration.sort_fields_by.eql?(:definition)
end

def transformers
view_transformers.empty? ? Blueprinter.configuration.default_transformers : view_transformers
end

Comment on lines -19 to -22
Copy link
Contributor Author

@njbbaer njbbaer Dec 27, 2023

Choose a reason for hiding this comment

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

This logic has been moved inside of ViewCollection since we need to aggregate the included transformers before deciding whether to apply the global default transformers. See #372 (comment) for details.

def track_definition_order(method, viewable: true)
return unless @sort_by_definition

Expand Down
13 changes: 12 additions & 1 deletion lib/blueprinter/view_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ def fields_for(view_name)
end

def transformers(view_name)
views[view_name].transformers
included_transformers = gather_transformers_from_included_views(view_name).reverse
all_transformers = views[:default].view_transformers.concat(included_transformers).uniq
all_transformers.empty? ? Blueprinter.configuration.default_transformers : all_transformers
end

def [](view_name)
Expand Down Expand Up @@ -89,5 +91,14 @@ def add_to_ordered_fields(ordered_fields, definition, fields, view_name_filter =
ordered_fields[definition.name] = fields[definition.name]
end
end

def gather_transformers_from_included_views(view_name)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Should we make this private?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Isn't it already?

current_view = views[view_name]
current_view.included_view_names.flat_map do |included_view_name|
next [] if view_name == included_view_name

gather_transformers_from_included_views(included_view_name)
end.concat(current_view.view_transformers)
end
end
end
3 changes: 3 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'blueprinter'
require 'json'

Dir[File.dirname(__FILE__) + '/support/**/*.rb'].each { |file| require file }

module SpecHelpers
def reset_blueprinter_config!
Expand Down
9 changes: 9 additions & 0 deletions spec/support/mock_field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class MockField
attr_reader :name, :method
def initialize(method, name = nil)
@method = method
@name = name || method
end
end
153 changes: 153 additions & 0 deletions spec/units/view_collection_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# frozen_string_literal: true

describe 'ViewCollection' do
subject(:view_collection) { Blueprinter::ViewCollection.new }

let!(:default_view) { view_collection[:default] }
let!(:view) { view_collection[:view] }
Comment on lines +6 to +7
Copy link

Choose a reason for hiding this comment

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

Why are we using let! here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When a view collection tries to fetch a view that doesn't exist it creates it, which is what we're doing here. We need that view to exist for our tests, but don't always call view directly, so it won't be initialized unless we use let!.


let(:default_field) { MockField.new(:default_field) }
let(:view_field) { MockField.new(:view_field) }
let(:new_field) { MockField.new(:new_field) }

let(:default_transformer) { Blueprinter::Transformer.new }

before do
default_view << default_field
view << view_field
end

describe '#initialize' do
it 'should create an identifier, view, and default view' do
expect(view_collection.views.keys).to eq([:identifier, :default, :view])
end
end

describe '#[]' do
it 'should return the view if it exists' do
expect(view_collection.views[:default]).to eq(default_view)
end

it 'should create the view if it does not exist' do
new_view = view_collection[:new_view]
expect(view_collection.views[:new_view]).to eq(new_view)
end
end

describe '#view?' do
it 'should return true if the view exists' do
expect(view_collection.view?(:default)).to eq(true)
end

it 'should return false if the view does not exist' do
expect(view_collection.view?(:missing_view)).to eq(false)
end
end

describe '#inherit' do
let(:parent_view_collection) { Blueprinter::ViewCollection.new }

before do
parent_view_collection[:view] << new_field
end

it 'should inherit the fields from the parent view collection' do
view_collection.inherit(parent_view_collection)
expect(view.fields).to include(parent_view_collection[:view].fields)
end
end

describe '#fields_for' do
it 'should return the fields for the view' do
expect(view_collection.fields_for(:view)).to eq([default_field, view_field])
end
end

describe '#transformers' do
let(:transformer) { Blueprinter::Transformer.new }

before do
view.add_transformer(transformer)
end

it 'should return the transformers for the view' do
expect(view_collection.transformers(:view)).to eq([transformer])
end

it 'should not return any transformers for another view' do
view_collection[:foo]
expect(view_collection.transformers(:foo)).to eq([])
end

context 'default view transformer' do
before do
default_view.add_transformer(default_transformer)
end

it 'should return the transformers for the default view' do
expect(view_collection.transformers(:default)).to eq([default_transformer])
end

it 'should return both the view transformer and default transformers for the view' do
expect(view_collection.transformers(:view)).to eq([default_transformer, transformer])
end
end

context 'include view transformer' do
let!(:includes_view) { view_collection[:includes_view] }
let!(:nested_view) { view_collection[:nested_view] }

before do
includes_view.include_view(:view)
nested_view.include_view(:includes_view)
end

it 'should return the transformers for the included view' do
expect(view_collection.transformers(:includes_view)).to include(transformer)
end

it 'should return the transformers for the nested included view' do
expect(view_collection.transformers(:nested_view)).to include(transformer)
end

it 'should only return unique transformers' do
includes_view.add_transformer(transformer)
transformers = view_collection.transformers(:nested_view)
expect(transformers.uniq.length == transformers.length).to eq(true)
end

it 'should return transformers in the correct order' do
includes_view_transformer = Blueprinter::Transformer.new
nested_view_transformer = Blueprinter::Transformer.new

default_view.add_transformer(default_transformer)
includes_view.add_transformer(includes_view_transformer)
nested_view.add_transformer(nested_view_transformer)

expect(view_collection.transformers(:nested_view)).to eq([
default_transformer, nested_view_transformer, includes_view_transformer, transformer
])
end
end

context 'global default transformers' do
before do
Blueprinter.configure { |config| config.default_transformers = [default_transformer] }
end

context 'with no view transformers' do
let!(:new_view) { view_collection[:new_view] }

it 'should return the global default transformers' do
expect(view_collection.transformers(:new_view)).to include(default_transformer)
end
end

context 'with view transformers' do
it 'should not return the global default transformers' do
expect(view_collection.transformers(:view)).to_not include(default_transformer)
end
end
end
end
end
18 changes: 0 additions & 18 deletions spec/units/view_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,23 +98,5 @@ class OverrideTransform < Blueprinter::Transformer; end
before do
Blueprinter.configure { |config| config.default_transformers = [default_transform] }
end

describe '#transformers' do
it 'should return the default transformers' do
expect(view_with_default_transform.transformers).to eq([default_transform])
end

it 'should allow for overriding the default transformers' do
expect(view_with_override_transform.transformers).to eq([override_transform])
end
end
end
end

class MockField
attr_reader :name, :method
def initialize(method, name = nil)
@method = method
@name = name || method
end
end