diff --git a/lib/draper.rb b/lib/draper.rb index 4f8b667d..20ce8a84 100644 --- a/lib/draper.rb +++ b/lib/draper.rb @@ -23,7 +23,7 @@ require 'draper/decorated_association' require 'draper/helper_support' require 'draper/view_context' -require 'draper/query_methods' +require 'draper/load_strategy' require 'draper/collection_decorator' require 'draper/undecorate' require 'draper/decorates_assigned' diff --git a/lib/draper/collection_decorator.rb b/lib/draper/collection_decorator.rb index 35e0b1b4..9d7b8b6d 100644 --- a/lib/draper/collection_decorator.rb +++ b/lib/draper/collection_decorator.rb @@ -2,7 +2,6 @@ module Draper class CollectionDecorator include Enumerable include Draper::ViewHelpers - include Draper::QueryMethods extend Draper::Delegation # @return the collection being decorated. @@ -72,6 +71,17 @@ def replace(other) self end + # 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(with: decorator_class, context: context) + end + + def respond_to_missing?(method, include_private = false) + strategy.allowed?(method) || super + end + protected # Decorates the given item. @@ -88,5 +98,10 @@ def item_decorator ->(item, options) { item.decorate(options) } end end + + # 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 diff --git a/lib/draper/load_strategy.rb b/lib/draper/load_strategy.rb new file mode 100644 index 00000000..59fe9b91 --- /dev/null +++ b/lib/draper/load_strategy.rb @@ -0,0 +1,19 @@ +module Draper + 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 diff --git a/lib/draper/query_methods.rb b/lib/draper/query_methods.rb deleted file mode 100644 index 59c42bb8..00000000 --- a/lib/draper/query_methods.rb +++ /dev/null @@ -1,23 +0,0 @@ -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(with: decorator_class, context: context) - end - - def respond_to_missing?(method, include_private = false) - strategy.allowed?(method) || super - 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 diff --git a/lib/draper/query_methods/load_strategy.rb b/lib/draper/query_methods/load_strategy.rb deleted file mode 100644 index 878468e6..00000000 --- a/lib/draper/query_methods/load_strategy.rb +++ /dev/null @@ -1,21 +0,0 @@ -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 diff --git a/spec/draper/collection_decorator_spec.rb b/spec/draper/collection_decorator_spec.rb index d329e861..5897d79e 100644 --- a/spec/draper/collection_decorator_spec.rb +++ b/spec/draper/collection_decorator_spec.rb @@ -1,6 +1,10 @@ require 'spec_helper' require 'support/shared_examples/view_helpers' +require_relative '../dummy/app/decorators/post_decorator' + +Post = Struct.new(:id) { } + module Draper describe CollectionDecorator do it_behaves_like "view helpers", CollectionDecorator.new([]) @@ -286,5 +290,75 @@ module Draper expect(decorator.replace([:foo, :bar])).to be decorator end end + + describe '#method_missing' do + let(:collection) { [ Post.new, Post.new ] } + let(:collection_context) { { user: 'foo' } } + let(:fake_strategy) { instance_double(LoadStrategy::ActiveRecord) } + + let(:collection_decorator) do + PostDecorator.decorate_collection( + collection, + context: collection_context + ) + end + + before { allow(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 + + it 'calls the method on the collection and keeps the decoration options' do + collection_decorator.some_query_method + + expect(results).to have_received(:decorate).with({ context: collection_context, with: PostDecorator }) + 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 + + describe "#respond_to?" do + let(:collection) { [ Post.new, Post.new ] } + let(:collection_decorator) { PostDecorator.decorate_collection(collection) } + let(:fake_strategy) { instance_double(LoadStrategy::ActiveRecord) } + + subject { collection_decorator.respond_to?(:some_query_method) } + + before { allow(LoadStrategy).to receive(:new).and_return(fake_strategy) } + + context 'when strategy allows collection to call the method' do + before do + allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(true) + end + + it { is_expected.to eq(true) } + end + + context 'when strategy does not allow collection to call the method' do + before do + allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(false) + end + + it { is_expected.to eq(false) } + end + end end end diff --git a/spec/draper/query_methods/load_strategy_spec.rb b/spec/draper/load_strategy_spec.rb similarity index 100% rename from spec/draper/query_methods/load_strategy_spec.rb rename to spec/draper/load_strategy_spec.rb diff --git a/spec/draper/query_methods_spec.rb b/spec/draper/query_methods_spec.rb deleted file mode 100644 index 7849654d..00000000 --- a/spec/draper/query_methods_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'spec_helper' -require_relative '../dummy/app/decorators/post_decorator' - -Post = Struct.new(:id) { } - -module Draper - describe QueryMethods do - let(:fake_strategy) { instance_double(QueryMethods::LoadStrategy::ActiveRecord) } - - before { allow(QueryMethods::LoadStrategy).to receive(:new).and_return(fake_strategy) } - - describe '#method_missing' do - let(:collection) { [ Post.new, Post.new ] } - let(:collection_context) { { user: 'foo' } } - let(:collection_decorator) { PostDecorator.decorate_collection(collection, context: collection_context) } - - 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 - - it 'calls the method on the collection and keeps the decoration options' do - collection_decorator.some_query_method - - expect(results).to have_received(:decorate).with({ context: collection_context, with: PostDecorator }) - 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 - - describe "#respond_to?" do - let(:collection) { [ Post.new, Post.new ] } - let(:collection_decorator) { PostDecorator.decorate_collection(collection) } - - subject { collection_decorator.respond_to?(:some_query_method) } - - context 'when strategy allows collection to call the method' do - before do - allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(true) - end - - it { is_expected.to eq(true) } - end - - context 'when strategy does not allow collection to call the method' do - before do - allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(false) - end - - it { is_expected.to eq(false) } - end - end - end -end