Skip to content

Commit

Permalink
feat: Include ORM associations in CollectionDecorator (#845)
Browse files Browse the repository at this point in the history
## Description
Include all QueryMethods from the ORM in CollectionDecorator. The default adapter is :active_record

* Why was this change required?
It was necessary to delegate or define a method of the ORM which you are using in your decorator to make an instance of CollectionDecorator able to call it.
* Is there something you aren't happy with or that needs extra attention?
In order to support other ORM associations, we'll need to write a method `allowed?` for each strategy at `lib/draper/query_methods/load_strategy.rb`

## Testing
1. Create a decorator for the model and its association
```ruby
class OrderHistoryDecorator < Draper::Decorator
  delegate_all
end

class OrderDecorator < Draper::Decorator
  delegate_all

  decorates_association :order_histories, with: OrderHistoryDecorator
end
```

2. Call any query method in the decorated instance
```ruby
pry(main)> Order.last.decorate.order_histories.includes(:user)
```

## References
* Issue #702 
* Issue #812
  • Loading branch information
brunohkbx authored and codebycliff committed Feb 25, 2019
1 parent b1974a8 commit 6b3e9bc
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ source "https://rubygems.org"
gemspec

platforms :ruby do
gem "sqlite3"
gem 'sqlite3', '~> 1.3.6'
end

platforms :jruby do
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,17 @@ your `ArticleDecorator` and they'll return decorated objects:
@article = ArticleDecorator.find(params[:id])
```

### Decorated Query Methods
By default, Draper will decorate all [QueryMethods](https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html)
of ActiveRecord.
If you're using another ORM, in order to support it, you can tell Draper to use a custom strategy:

```ruby
Draper.configure do |config|
config.default_query_methods_strategy = :mongoid
end
```

### When to Decorate Objects

Decorators are supposed to behave very much like the models they decorate, and
Expand Down
1 change: 1 addition & 0 deletions lib/draper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
require 'draper/decorated_association'
require 'draper/helper_support'
require 'draper/view_context'
require 'draper/query_methods'
require 'draper/collection_decorator'
require 'draper/undecorate'
require 'draper/decorates_assigned'
Expand Down
1 change: 1 addition & 0 deletions lib/draper/collection_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Draper
class CollectionDecorator
include Enumerable
include Draper::ViewHelpers
include Draper::QueryMethods
extend Draper::Delegation

# @return the collection being decorated.
Expand Down
8 changes: 8 additions & 0 deletions lib/draper/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,13 @@ def default_controller
def default_controller=(controller)
@@default_controller = controller
end

def default_query_methods_strategy
@@default_query_methods_strategy ||= :active_record
end

def default_query_methods_strategy=(strategy)
@@default_query_methods_strategy = strategy
end
end
end
19 changes: 19 additions & 0 deletions lib/draper/query_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require_relative 'query_methods/load_strategy'

module Draper
module QueryMethods
# Proxies missing query methods to the source class if the strategy allows.
def method_missing(method, *args, &block)
return super unless strategy.allowed? method

object.send(method, *args, &block).decorate
end

private

# Configures the strategy used to proxy the query methods, which defaults to `:active_record`.
def strategy
@strategy ||= LoadStrategy.new(Draper.default_query_methods_strategy)
end
end
end
21 changes: 21 additions & 0 deletions lib/draper/query_methods/load_strategy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Draper
module QueryMethods
module LoadStrategy
def self.new(name)
const_get(name.to_s.camelize).new
end

class ActiveRecord
def allowed?(method)
::ActiveRecord::Relation::VALUE_METHODS.include? method
end
end

class Mongoid
def allowed?(method)
raise NotImplementedError
end
end
end
end
end
40 changes: 32 additions & 8 deletions spec/draper/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,44 @@ module Draper
Draper.configure { |config| expect(config).to be Draper }
end

it 'defaults default_controller to ApplicationController' do
expect(Draper.default_controller).to be ApplicationController
describe '#default_controller' do
it 'defaults default_controller to ApplicationController' do
expect(Draper.default_controller).to be ApplicationController
end

it 'allows customizing default_controller through configure' do
default = Draper.default_controller

Draper.configure do |config|
config.default_controller = CustomController
end

expect(Draper.default_controller).to be CustomController

Draper.default_controller = default
end
end

it 'allows customizing default_controller through configure' do
default = Draper.default_controller
describe '#default_query_methods_strategy' do
let!(:default) { Draper.default_query_methods_strategy }

subject { Draper.default_query_methods_strategy }

Draper.configure do |config|
config.default_controller = CustomController
context 'when there is no custom strategy' do
it { is_expected.to eq(:active_record) }
end

expect(Draper.default_controller).to be CustomController
context 'when using a custom strategy' do
before do
Draper.configure do |config|
config.default_query_methods_strategy = :mongoid
end
end

Draper.default_controller = default
after { Draper.default_query_methods_strategy = default }

it { is_expected.to eq(:mongoid) }
end
end
end
end
26 changes: 26 additions & 0 deletions spec/draper/query_methods/load_strategy_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'spec_helper'
require 'active_record'

module Draper
module QueryMethods
describe LoadStrategy do
describe '#new' do
subject { described_class.new(:active_record) }

it { is_expected.to be_an_instance_of(LoadStrategy::ActiveRecord) }
end
end

describe LoadStrategy::ActiveRecord do
describe '#allowed?' do
it 'checks whether or not ActiveRecord::Relation::VALUE_METHODS has the given method' do
allow(::ActiveRecord::Relation::VALUE_METHODS).to receive(:include?)

described_class.new.allowed? :foo

expect(::ActiveRecord::Relation::VALUE_METHODS).to have_received(:include?).with(:foo)
end
end
end
end
end
39 changes: 39 additions & 0 deletions spec/draper/query_methods_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require 'spec_helper'
require_relative '../dummy/app/decorators/post_decorator'

Post = Struct.new(:id) { }

module Draper
describe QueryMethods do
describe '#method_missing' do
let(:collection) { [ Post.new, Post.new ] }
let(:collection_decorator) { PostDecorator.decorate_collection(collection) }
let(:fake_strategy) { instance_double(QueryMethods::LoadStrategy::ActiveRecord) }

before { allow(QueryMethods::LoadStrategy).to receive(:new).and_return(fake_strategy) }

context 'when strategy allows collection to call the method' do
let(:results) { spy(:results) }

before do
allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(true)
allow(collection).to receive(:send).with(:some_query_method).and_return(results)
end

it 'calls the method on the collection and decorate it results' do
collection_decorator.some_query_method

expect(results).to have_received(:decorate)
end
end

context 'when strategy does not allow collection to call the method' do
before { allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(false) }

it 'raises NoMethodError' do
expect { collection_decorator.some_query_method }.to raise_exception(NoMethodError)
end
end
end
end
end

0 comments on commit 6b3e9bc

Please sign in to comment.