Skip to content
This repository
Browse code

Merge pull request #332 from haines/decorated_associations

Extract decorated associations
  • Loading branch information...
commit a9dedcdbae1f43b1e35e6d27fcbf39bc27b4ddb2 2 parents 5d95795 + 966d9cc
Steve Klabnik authored November 08, 2012
1  lib/draper.rb
@@ -8,6 +8,7 @@
8 8
 require 'draper/helper_proxy'
9 9
 require 'draper/lazy_helpers'
10 10
 require 'draper/decoratable'
  11
+require 'draper/decorated_association'
11 12
 require 'draper/security'
12 13
 require 'draper/helper_support'
13 14
 require 'draper/view_context'
55  lib/draper/decorated_association.rb
... ...
@@ -0,0 +1,55 @@
  1
+module Draper
  2
+  class DecoratedAssociation
  3
+
  4
+    attr_reader :source, :association, :options
  5
+
  6
+    def initialize(source, association, options)
  7
+      @source = source
  8
+      @association = association
  9
+      @options = options
  10
+    end
  11
+
  12
+    def call
  13
+      return undecorated if undecorated.nil? || undecorated == []
  14
+      decorate
  15
+    end
  16
+
  17
+    private
  18
+
  19
+    def undecorated
  20
+      @undecorated ||= begin
  21
+        associated = source.send(association)
  22
+        associated = associated.send(options[:scope]) if options[:scope]
  23
+        associated
  24
+      end
  25
+    end
  26
+
  27
+    def decorate
  28
+      @decorated ||= decorator_class.send(decorate_method, undecorated, options)
  29
+    end
  30
+
  31
+    def decorate_method
  32
+      if collection? && decorator_class.respond_to?(:decorate_collection)
  33
+        :decorate_collection
  34
+      else
  35
+        :decorate
  36
+      end
  37
+    end
  38
+
  39
+    def collection?
  40
+      undecorated.respond_to?(:first)
  41
+    end
  42
+
  43
+    def decorator_class
  44
+      return options[:with] if options[:with]
  45
+
  46
+      if collection?
  47
+        options[:with] = :infer
  48
+        Draper::CollectionDecorator
  49
+      else
  50
+        undecorated.decorator_class
  51
+      end
  52
+    end
  53
+
  54
+  end
  55
+end
56  lib/draper/decorator.rb
@@ -50,49 +50,25 @@ def self.has_finders(options = {})
50 50
     # Typically called within a decorator definition, this method causes
51 51
     # the assocation to be decorated when it is retrieved.
52 52
     #
53  
-    # @param [Symbol] association_symbol name of association to decorate, like `:products`
54  
-    # @option options [Hash] :with The decorator to decorate the association with
55  
-    #                        :scope The scope to apply to the association
56  
-    def self.decorates_association(association_symbol, options = {})
57  
-      define_method(association_symbol) do
58  
-        orig_association = source.send(association_symbol)
59  
-
60  
-        return orig_association if orig_association.nil? || orig_association == []
61  
-        return decorated_associations[association_symbol] if decorated_associations[association_symbol]
62  
-
63  
-        orig_association = orig_association.send(options[:scope]) if options[:scope]
64  
-
65  
-        return options[:with].decorate(orig_association) if options[:with]
66  
-
67  
-        collection = orig_association.respond_to?(:first)
68  
-
69  
-        klass = if options[:polymorphic]
70  
-                  orig_association.class
71  
-                elsif association_reflection = find_association_reflection(association_symbol)
72  
-                  association_reflection.klass
73  
-                elsif collection
74  
-                  orig_association.first.class
75  
-                else
76  
-                  orig_association.class
77  
-                end
78  
-
79  
-        decorator_class = "#{klass}Decorator".constantize
80  
-
81  
-        if collection
82  
-          decorated_associations[association_symbol] = decorator_class.decorate_collection(orig_association, options)
83  
-        else
84  
-          decorated_associations[association_symbol] = decorator_class.decorate(orig_association, options)
85  
-        end
  53
+    # @param [Symbol] association name of association to decorate, like `:products`
  54
+    # @option options [Class] :with the decorator to apply to the association
  55
+    # @option options [Symbol] :scope a scope to apply when fetching the association
  56
+    def self.decorates_association(association, options = {})
  57
+      define_method(association) do
  58
+        decorated_associations[association] ||= Draper::DecoratedAssociation.new(source, association, options)
  59
+        decorated_associations[association].call
86 60
       end
87 61
     end
88 62
 
89 63
     # A convenience method for decorating multiple associations. Calls
90 64
     # decorates_association on each of the given symbols.
91 65
     #
92  
-    # @param [Symbols*] association_symbols name of associations to decorate
93  
-    def self.decorates_associations(*association_symbols)
94  
-      options = association_symbols.extract_options!
95  
-      association_symbols.each{ |sym| decorates_association(sym, options) }
  66
+    # @param [Symbols*] associations name of associations to decorate
  67
+    def self.decorates_associations(*associations)
  68
+      options = associations.extract_options!
  69
+      associations.each do |association|
  70
+        decorates_association(association, options)
  71
+      end
96 72
     end
97 73
 
98 74
     # Specifies a black list of methods which may *not* be proxied to
@@ -227,12 +203,6 @@ def handle_multiple_decoration
227 203
       end
228 204
     end
229 205
 
230  
-    def find_association_reflection(association)
231  
-      if source.class.respond_to?(:reflect_on_association)
232  
-        source.class.reflect_on_association(association)
233  
-      end
234  
-    end
235  
-
236 206
     def decorated_associations
237 207
       @decorated_associations ||= {}
238 208
     end
2  lib/draper/rspec_integration.rb
... ...
@@ -1,2 +0,0 @@
1  
-warn 'DEPRECATION WARNING -- use `require "draper/test/rspec_integration"` instead of `require "draper/rspec_integration"`'
2  
-require 'draper/test/rspec_integration'
130  spec/draper/decorated_association_spec.rb
... ...
@@ -0,0 +1,130 @@
  1
+require 'spec_helper'
  2
+
  3
+describe Draper::DecoratedAssociation do
  4
+  let(:decorated_association) { Draper::DecoratedAssociation.new(source, association, options) }
  5
+  let(:source) { Product.new }
  6
+  let(:options) { {} }
  7
+
  8
+  describe "#call" do
  9
+    subject { decorated_association.call }
  10
+
  11
+    context "for an ActiveModel collection association" do
  12
+      let(:association) { :similar_products }
  13
+
  14
+      context "when the association is not empty" do
  15
+        it "decorates the collection" do
  16
+          subject.should be_a Draper::CollectionDecorator
  17
+        end
  18
+
  19
+        it "infers the decorator" do
  20
+          subject.decorator_class.should be :infer
  21
+        end
  22
+      end
  23
+
  24
+      context "when the association is empty" do
  25
+        it "doesn't decorate the collection" do
  26
+          source.stub(:similar_products).and_return([])
  27
+          subject.should_not be_a Draper::CollectionDecorator
  28
+          subject.should be_empty
  29
+        end
  30
+      end
  31
+    end
  32
+
  33
+    context "for non-ActiveModel collection associations" do
  34
+      let(:association) { :poro_similar_products }
  35
+
  36
+      context "when the association is not empty" do
  37
+        it "decorates the collection" do
  38
+          subject.should be_a Draper::CollectionDecorator
  39
+        end
  40
+
  41
+        it "infers the decorator" do
  42
+          subject.decorator_class.should be :infer
  43
+        end
  44
+      end
  45
+
  46
+      context "when the association is empty" do
  47
+        it "doesn't decorate the collection" do
  48
+          source.stub(:poro_similar_products).and_return([])
  49
+          subject.should_not be_a Draper::CollectionDecorator
  50
+          subject.should be_empty
  51
+        end
  52
+      end
  53
+    end
  54
+
  55
+    context "for an ActiveModel singular association" do
  56
+      let(:association) { :previous_version }
  57
+
  58
+      context "when the association is present" do
  59
+        it "decorates the association" do
  60
+          subject.should be_decorated_with ProductDecorator
  61
+        end
  62
+      end
  63
+
  64
+      context "when the association is absent" do
  65
+        it "doesn't decorate the association" do
  66
+          source.stub(:previous_version).and_return(nil)
  67
+          subject.should be_nil
  68
+        end
  69
+      end
  70
+    end
  71
+
  72
+    context "for a non-ActiveModel singular association" do
  73
+      let(:association) { :poro_previous_version }
  74
+
  75
+      context "when the association is present" do
  76
+        it "decorates the association" do
  77
+          subject.should be_decorated_with ProductDecorator
  78
+        end
  79
+      end
  80
+
  81
+      context "when the association is absent" do
  82
+        it "doesn't decorate the association" do
  83
+          source.stub(:poro_previous_version).and_return(nil)
  84
+          subject.should be_nil
  85
+        end
  86
+      end
  87
+    end
  88
+
  89
+    context "when a decorator is specified" do
  90
+      let(:options) { {with: SpecificProductDecorator} }
  91
+
  92
+      context "for a singular association" do
  93
+        let(:association) { :previous_version }
  94
+
  95
+        it "decorates with the specified decorator" do
  96
+          subject.should be_decorated_with SpecificProductDecorator
  97
+        end
  98
+      end
  99
+
  100
+      context "for a collection association" do
  101
+        let(:association) { :similar_products}
  102
+
  103
+        it "decorates with a collection of the specifed decorators" do
  104
+          subject.should be_a Draper::CollectionDecorator
  105
+          subject.decorator_class.should be SpecificProductDecorator
  106
+        end
  107
+      end
  108
+    end
  109
+
  110
+    context "when a collection decorator is specified" do
  111
+      let(:association) { :similar_products }
  112
+      let(:options) { {with: ProductsDecorator} }
  113
+
  114
+      it "decorates with the specified decorator" do
  115
+        subject.should be_a ProductsDecorator
  116
+      end
  117
+    end
  118
+
  119
+    context "with a scope" do
  120
+      let(:association) { :thing }
  121
+      let(:options) { {scope: :foo} }
  122
+
  123
+      it "applies the scope before decoration" do
  124
+        scoped = [SomeThing.new]
  125
+        SomeThing.any_instance.should_receive(:foo).and_return(scoped)
  126
+        subject.source.should be scoped
  127
+      end
  128
+    end
  129
+  end
  130
+end
102  spec/draper/decorator_spec.rb
@@ -126,100 +126,26 @@
126 126
   end
127 127
 
128 128
   describe ".decorates_association" do
129  
-    context "for ActiveModel collection associations" do
130  
-      before { subject.class.decorates_association :similar_products }
  129
+    before { subject.class.decorates_association :similar_products, with: ProductDecorator }
131 130
 
132  
-      context "when the association is not empty" do
133  
-        it "decorates the collection" do
134  
-          subject.similar_products.should be_a Draper::CollectionDecorator
135  
-          subject.similar_products.each {|item| item.should be_decorated_with ProductDecorator }
136  
-        end
137  
-      end
138  
-
139  
-      context "when the association is empty" do
140  
-        it "doesn't decorate the collection" do
141  
-          source.stub(:similar_products).and_return([])
142  
-          subject.similar_products.should_not be_a Draper::CollectionDecorator
143  
-          subject.similar_products.should be_empty
144  
-        end
145  
-      end
146  
-    end
147  
-
148  
-    context "for Plain Old Ruby Object collection associations" do
149  
-      before { subject.class.decorates_association :poro_similar_products }
150  
-
151  
-      context "when the association is not empty" do
152  
-        it "decorates the collection" do
153  
-          subject.poro_similar_products.should be_a Draper::CollectionDecorator
154  
-          subject.poro_similar_products.each {|item| item.should be_decorated_with ProductDecorator }
155  
-        end
156  
-      end
  131
+    describe "overridden association method" do
  132
+      let(:decorated_association) { ->{} }
157 133
 
158  
-      context "when the association is empty" do
159  
-        it "doesn't decorate the collection" do
160  
-          source.stub(:poro_similar_products).and_return([])
161  
-          subject.poro_similar_products.should_not be_a Draper::CollectionDecorator
162  
-          subject.poro_similar_products.should be_empty
163  
-        end
  134
+      it "creates a DecoratedAssociation" do
  135
+        Draper::DecoratedAssociation.should_receive(:new).with(source, :similar_products, {with: ProductDecorator}).and_return(decorated_association)
  136
+        subject.similar_products
164 137
       end
165  
-    end
166 138
 
167  
-    context "for an ActiveModel singular association" do
168  
-      before { subject.class.decorates_association :previous_version }
169  
-
170  
-      context "when the association is present" do
171  
-        it "decorates the association" do
172  
-          subject.previous_version.should be_decorated_with ProductDecorator
173  
-        end
  139
+      it "memoizes the DecoratedAssociation" do
  140
+        Draper::DecoratedAssociation.should_receive(:new).once.and_return(decorated_association)
  141
+        subject.similar_products
  142
+        subject.similar_products
174 143
       end
175 144
 
176  
-      context "when the association is absent" do
177  
-        it "doesn't decorate the association" do
178  
-          source.stub(:previous_version).and_return(nil)
179  
-          subject.previous_version.should be_nil
180  
-        end
181  
-      end
182  
-    end
183  
-
184  
-    context "for an ActiveModel singular association" do
185  
-      before { subject.class.decorates_association :poro_previous_version }
186  
-
187  
-      context "when the association is present" do
188  
-        it "decorates the association" do
189  
-          subject.poro_previous_version.should be_decorated_with ProductDecorator
190  
-        end
191  
-      end
192  
-
193  
-      context "when the association is absent" do
194  
-        it "doesn't decorate the association" do
195  
-          source.stub(:poro_previous_version).and_return(nil)
196  
-          subject.poro_previous_version.should be_nil
197  
-        end
198  
-      end
199  
-    end
200  
-
201  
-    context "when a decorator is specified" do
202  
-      before { subject.class.decorates_association :previous_version, with: SpecificProductDecorator }
203  
-
204  
-      it "decorates with the specified decorator" do
205  
-        subject.previous_version.should be_decorated_with SpecificProductDecorator
206  
-      end
207  
-    end
208  
-
209  
-    context "with a scope" do
210  
-      before { subject.class.decorates_association :thing, scope: :foo }
211  
-
212  
-      it "applies the scope before decoration" do
213  
-        SomeThing.any_instance.should_receive(:foo).and_return(:bar)
214  
-        subject.thing.model.should == :bar
215  
-      end
216  
-    end
217  
-
218  
-    context "for a polymorphic association" do
219  
-      before { subject.class.decorates_association :thing, polymorphic: true }
220  
-
221  
-      it "makes the association return the right decorator" do
222  
-        subject.thing.should be_decorated_with SomeThingDecorator
  145
+      it "calls the DecoratedAssociation" do
  146
+        Draper::DecoratedAssociation.stub(:new).and_return(decorated_association)
  147
+        decorated_association.should_receive(:call).and_return(:decorated)
  148
+        subject.similar_products.should be :decorated
223 149
       end
224 150
     end
225 151
   end
0  performance/active_record.rb → spec/performance/active_record.rb
File renamed without changes
0  performance/bechmark.rb → spec/performance/benchmark.rb
File renamed without changes
0  performance/decorators.rb → spec/performance/decorators.rb
File renamed without changes
0  performance/models.rb → spec/performance/models.rb
File renamed without changes

0 notes on commit a9dedcd

Please sign in to comment.
Something went wrong with that request. Please try again.