draper 0.4.1.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,70 +1,114 @@
1
- ## Heads Up!
1
+ # Draper: View Models for Rails
2
2
 
3
- This gem is not yet production ready. The API is sure to change quickly and there could be side-effects I haven't discovered yet.
3
+ ## Quick Start
4
4
 
5
- Please only use it if you're playing around and helping me experiment! Thanks :)
5
+ 1. Add `gem 'draper'` to your `Gemfile` and `bundle`
6
+ 2. Run `rails g draper:model YourModel`
7
+ 3. Edit `app/decorators/your_model_decorator.rb` using:
8
+ 1. `h` to proxy to Rails/application helpers like `h.current_user`
9
+ 2. `model` to access the wrapped object like `model.created_at`
10
+ 4. Wrap models in your controller with the decorator using:
11
+ 1. `.find` automatic lookup & wrap: `ArticleDecorator.find(1)`
12
+ 2. `.decorate` method with single object or collection: `ArticleDecorator.decorate(Article.all)`
13
+ 3. `.new` method with single object: `ArticleDecorator.new(Article.first)`
14
+ 5. Call and output the methods in your view templates as normal
6
15
 
7
- # Draper
16
+ ## Goals
8
17
 
9
- This gem makes it easy to apply the decorator pattern to the models in a Rails application. This gives you three wins:
18
+ This gem makes it easy to apply the decorator pattern to the data models in a Rails application. This gives you three wins:
10
19
 
11
20
  1. Replace most helpers with an object-oriented approach
12
21
  2. Filter data at the presentation level
13
22
  3. Enforce an interface between your controllers and view templates.
14
23
 
15
- ## 1. Object Oriented Helpers
16
24
 
17
- Why hate helpers? In Ruby/Rails we approach everything from an Object-Oriented perspective, then with helpers we get procedural.The job of a helper is to take in data and output a presentation-ready string. We can do that job in an OO style with a decorator.
25
+ ### 1. Object Oriented Helpers
18
26
 
19
- In general, a decorator wraps an object with presentation-related accessor methods. For instance, if you had an `Article` object, then a decorator might add instance methods like `.formatted_published_at` or `.formatted_title` that output actual HTML.
27
+ Why hate helpers? In Ruby/Rails we approach everything from an Object-Oriented perspective, then with helpers we get procedural.The job of a helper is to take in data and output a presentation-ready string. We can do that with a decorator.
20
28
 
21
- For example:
29
+ A decorator wraps an object with presentation-related accessor methods. For instance, if you had an `Article` object, then the decorator could override `.published_at` to use formatted output like this:
22
30
 
23
31
  ```ruby
24
32
  class ArticleDecorator < Draper::Base
25
- def formatted_published_at
26
- date = content_tag(:span, published_at.strftime("%A, %B %e").squeeze(" "), :class => 'date')
27
- time = content_tag(:span, published_at.strftime("%l:%M%p"), :class => 'time').delete(" ")
28
- content_tag :span, date + time, :class => 'created_at'
33
+ decorates :article
34
+ def published_at
35
+ date = h.content_tag(:span, model.published_at.strftime("%A, %B %e").squeeze(" "), :class => 'date')
36
+ time = h.content_tag(:span, model.published_at.strftime("%l:%M%p"), :class => 'time').delete(" ")
37
+ h.content_tag :span, date + time, :class => 'created_at'
29
38
  end
30
39
  end
31
40
  ```
32
41
 
33
- ## 2. View-Layer Data Filtering
42
+ ### 2. View-Layer Data Filtering
34
43
 
35
- Have you ever written a `to_xml` or `to_json` method in your model? Did it feel weird to put what is essentially view logic in your model?
44
+ Have you ever written a `to_xml` or `to_json` method in your model? Did it feel weird to put presentation logic in your model?
36
45
 
37
46
  Or, in the course of formatting this data, did you wish you could access `current_user` down in the model? Maybe for guests your `to_json` is only going to show three attributes, but if the user is an admin they get to see them all.
38
47
 
39
48
  How would you handle this in the model layer? You'd probably pass the `current_user` or some role/flag down to `to_json`. That should still feel slimy.
40
49
 
41
- When you use a decorator you have the power of a Ruby object but it's a part of the view layer. This is where your `to_xml` belongs. It has access to the core data from the model, but it also knows about `current_user` because it can see the `ApplicationHelper` where that method is typically defined.
42
-
43
- For example:
50
+ When you use a decorator you have the power of a Ruby object but it's a part of the view layer. This is where your `to_xml` belongs. You can access your `current_user` helper method using the `h` proxy available in the decorator:
44
51
 
45
52
  ```ruby
46
53
  class ArticleDecorator < Draper::Base
54
+ decorates :article
47
55
  ADMIN_VISIBLE_ATTRIBUTES = [:title, :body, :author, :status]
48
56
  PUBLIC_VISIBLE_ATTRIBUTES = [:title, :body]
49
57
 
50
58
  def to_xml
51
- attr_set = current_user.admin? ? ADMIN_VISIBLE_ATTRIBUTES : PUBLIC_VISIBLE_ATTRIBUTES
52
- self.subject.to_xml(:only => attr_set)
59
+ attr_set = h.current_user.admin? ? ADMIN_VISIBLE_ATTRIBUTES : PUBLIC_VISIBLE_ATTRIBUTES
60
+ model.to_xml(:only => attr_set)
53
61
  end
54
62
  end
55
63
  ```
56
64
 
57
- ## How is it implemented?
65
+ ### 3. Enforcing an Interface
66
+
67
+ Want to strictly control what methods are proxied to the original object? Use `denies` or `allows`.
68
+
69
+ #### Using `denies`
70
+
71
+ The `denies` method takes a blacklist approach. For instance:
72
+
73
+ ```ruby
74
+ class ArticleDecorator < Draper::Base
75
+ decorates :article
76
+ denies :title
77
+ end
78
+ ```
79
+
80
+ Then, to test it:
81
+
82
+ ```irb
83
+ ruby-1.9.2-p290 :001 > ad = ArticleDecorator.find(1)
84
+ => #<ArticleDecorator:0x000001020d7728 @model=#<Article id: 1, title: "Hello, World">>
85
+ ruby-1.9.2-p290 :002 > ad.title
86
+ NoMethodError: undefined method `title' for #<ArticleDecorator:0x000001020d7728>
87
+ ```
88
+
89
+ #### Using `allows`
58
90
 
59
- To implement the pattern in Rails we can:
91
+ A better approach is to define a whitelist using `allows`:
60
92
 
61
- 1. Write a wrapper class with the decoration methods
62
- 2. Wrap the data object
63
- 3. Utilize those methods within our view layer
93
+ ```ruby
94
+ class ArticleDecorator < Draper::Base
95
+ decorates :article
96
+ allows :title, :description
97
+ end
98
+ ```
99
+
100
+ ```irb
101
+ ruby-1.9.2-p290 :001 > ad = ArticleDecorator.find(1)
102
+ => #<ArticleDecorator:0x000001020d7728 @model=#<Article id: 1, title: "Hello, World">>
103
+ ruby-1.9.2-p290 :002 > ad.title
104
+ => "Hello, World"
105
+ ruby-1.9.2-p290 :003 > ad.created_at
106
+ NoMethodError: undefined method `created_at' for #<ArticleDecorator:0x000001020d7728>
107
+ ```
64
108
 
65
- ## How do you utilize this gem in your application?
109
+ ## Up an Running
66
110
 
67
- Here are the steps to utilizing this gem:
111
+ ### Setup
68
112
 
69
113
  Add the dependency to your `Gemfile`:
70
114
 
@@ -78,49 +122,73 @@ Run bundle:
78
122
  bundle
79
123
  ```
80
124
 
81
- Create a decorator for your model (ex: `Article`)
125
+ ### Generate the Decorator
126
+
127
+ To decorate a model named `Article`:
82
128
 
83
129
  ```
84
130
  rails generate draper:model Article
85
131
  ```
86
132
 
87
- Open the decorator model (ex: `app/decorators/article_decorator.rb`)
133
+ ### Writing Methods
88
134
 
89
- Add your new formatting methods as normal instance or class methods. You have access to the Rails helpers from the following classes:
135
+ Open the decorator model (ex: `app/decorators/article_decorator.rb`) and add normal instance methods. To access the wrapped source object, use the `model` method:
90
136
 
91
- ```
92
- ActionView::Helpers::TagHelper
93
- ActionView::Helpers::UrlHelper
94
- ActionView::Helpers::TextHelper
137
+ ```ruby
138
+ class Article < Draper::Base
139
+ decorates :article
140
+
141
+ def author_name
142
+ model.author.first_name + " " + model.author.last_name
143
+ end
144
+ end
95
145
  ```
96
146
 
97
- Use the new methods in your views like any other model method (ex: `@article.formatted_published_at`)
98
147
 
99
- ## An Interface with Allows/Denies
148
+ ### Using Existing Helpers
100
149
 
101
- A proper interface defines a contract between two objects. One purpose of the decorator pattern is to define an interface between your data model and the view template.
102
-
103
- You are provided class methods `allows` and `denies` to control exactly which of the subject's methods are available. By default, *all* of the subject's methods can be accessed.
104
-
105
- For example, say you want to prevent access to the `:title` method. You'd use `denies` like this:
150
+ You probably want to make use of Rails helpers and those defined in your application. Use the `helpers` or `h` method proxy:
106
151
 
107
152
  ```ruby
108
- class ArticleDecorator < Draper::Base
109
- denies :title
153
+ class Article < Draper::Base
154
+ decorates :article
155
+
156
+ def published_at
157
+ date = h.content_tag(:span, model.published_at.strftime("%A, %B %e").squeeze(" "), :class => 'date')
158
+ time = h.content_tag(:span, model.published_at.strftime("%l:%M%p"), :class => 'time').delete(" ")
159
+ h.content_tag :span, date + time, :class => 'created_at'
110
160
  end
161
+ end
111
162
  ```
112
163
 
113
- `denies` uses a blacklist approach. Note that, as of the current version, denying `:title` does not affect related methods like `:title=`, `:title?`, etc.
164
+ ### In the Controller
114
165
 
115
- A better idea is a whitelist approach using `allows`:
166
+ When writing your controller actions, you have three options:
116
167
 
117
- ```ruby
118
- class ArticleDecorator < Draper::Base
119
- allows :title, :body, :author
120
- end
121
- ```
168
+ * Call `.new` and pass in the object to be wrapped
169
+
170
+ ```ruby
171
+ ArticleDecorator.new(Article.find(params[:id]))`
172
+ ```
173
+
174
+ * Call `.decorate` and pass in an object or collection of objects to be wrapped:
175
+ ```ruby
176
+ ArticleDecorator.decorate(Article.first) # Returns one instance of ArticleDecorator
177
+ ArticleDecorator.decorate(Article.all) # Returns an array of ArticleDecorator instances
178
+ ```
179
+
180
+ * Call `.find` to do automatically do a lookup on the `decorates` class:
181
+ ```ruby
182
+ ArticleDecorator.find(1)
183
+ ```
184
+
185
+ ### In Your Views
122
186
 
123
- Now only those methods and any defined in the decorator class itself can be accessed directly.
187
+ Use the new methods in your views like any other model method (ex: `@article.published_at`):
188
+
189
+ ```erb
190
+ <h1><%= @article.title %> <%= @article.published_at %></h1>
191
+ ```
124
192
 
125
193
  ## Possible Decoration Methods
126
194
 
@@ -132,6 +200,8 @@ Here are some ideas of what you might do in decorator methods:
132
200
 
133
201
  ## Example Using a Decorator
134
202
 
203
+ For a brief tutorial with sample project, check this out: http://tutorials.jumpstartlab.com/rails/topics/decorators.html
204
+
135
205
  Say I have a publishing system with `Article` resources. My designer decides that whenever we print the `published_at` timestamp, it should be constructed like this:
136
206
 
137
207
  ```html
@@ -143,7 +213,7 @@ Say I have a publishing system with `Article` resources. My designer decides tha
143
213
 
144
214
  Could we build that using a partial? Yes. A helper? Uh-huh. But the point of the decorator is to encapsulate logic just like we would a method in our models. Here's how to implement it.
145
215
 
146
- First, follow the steps above to add the dependency, update your bundle, then run the `rails generate decorator:setup` to prepare your app.
216
+ First, follow the steps above to add the dependency and update your bundle.
147
217
 
148
218
  Since we're talking about the `Article` model we'll create an `ArticleDecorator` class. You could do it by hand, but use the provided generator:
149
219
 
@@ -154,21 +224,19 @@ rails generate draper:model Article
154
224
  Now open up the created `app/decorators/article_decorator.rb` and you'll find an `ArticleDecorator` class. Add this method:
155
225
 
156
226
  ```ruby
157
- def formatted_published_at
158
- date = content_tag(:span, published_at.strftime("%A, %B %e").squeeze(" "), :class => 'date')
159
- time = content_tag(:span, published_at.strftime("%l:%M%p").delete(" "), :class => 'time')
160
- content_tag :span, date + time, :class => 'published_at'
227
+ def published_at
228
+ date = h.content_tag(:span, model.published_at.strftime("%A, %B %e").squeeze(" "), :class => 'date')
229
+ time = h.content_tag(:span, model.published_at.strftime("%l:%M%p").delete(" "), :class => 'time')
230
+ h.content_tag :span, date + time, :class => 'published_at'
161
231
  end
162
232
  ```
163
233
 
164
- *ASIDE*: Unfortunately, due to the current implementation of `content_tag`, you can't use the style of sending the content is as a block or you'll get an error about `undefined method 'output_buffer='`. Passing in the content as the second argument, as above, works fine.
165
-
166
234
  Then you need to perform the wrapping in your controller. Here's the simplest method:
167
235
 
168
236
  ```ruby
169
237
  class ArticlesController < ApplicationController
170
238
  def show
171
- @article = ArticleDecorator.new( Article.find params[:id] )
239
+ @article = ArticleDecorator.find params[:id]
172
240
  end
173
241
  end
174
242
  ```
@@ -176,17 +244,19 @@ end
176
244
  Then within your views you can utilize both the normal data methods and your new presentation methods:
177
245
 
178
246
  ```ruby
179
- <%= @article.formatted_published_at %>
247
+ <%= @article.published_at %>
180
248
  ```
181
249
 
182
250
  Ta-da! Object-oriented data formatting for your view layer. Below is the complete decorator with extra comments removed:
183
251
 
184
252
  ```ruby
185
253
  class ArticleDecorator < Draper::Base
186
- def formatted_published_at
187
- date = content_tag(:span, published_at.strftime("%A, %B %e").squeeze(" "), :class => 'date')
188
- time = content_tag(:span, published_at.strftime("%l:%M%p"), :class => 'time').delete(" ")
189
- content_tag :span, date + time, :class => 'created_at'
254
+ decorates :article
255
+
256
+ def published_at
257
+ date = h.content_tag(:span, published_at.strftime("%A, %B %e").squeeze(" "), :class => 'date')
258
+ time = h.content_tag(:span, published_at.strftime("%l:%M%p"), :class => 'time').delete(" ")
259
+ h.content_tag :span, date + time, :class => 'created_at'
190
260
  end
191
261
  end
192
262
  ```
@@ -194,10 +264,8 @@ end
194
264
  ## Issues / Pending
195
265
 
196
266
  * Test coverage for generators
197
- * Ability to decorate multiple objects at once, ex: `ArticleDecorator.decorate(Article.all)`
198
- * Revise readme to better explain interface pattern
267
+ * Keep revising Readme for better organization/clarity
199
268
  * Build sample Rails application
200
- * Consider: `ArticleDecorator.new(1)` does the equivalent of `ArticleDecorator.new(Article.find(1))`
201
269
 
202
270
  ## License
203
271
 
@@ -1,2 +1,7 @@
1
1
  require "draper/version"
2
- require 'draper/base'
2
+ require 'draper/system'
3
+ require 'draper/all_helpers'
4
+ require 'draper/base'
5
+ require 'draper/lazy_helpers'
6
+
7
+ Draper::System.setup
@@ -0,0 +1,43 @@
1
+ module Draper
2
+ module AllHelpers
3
+ # Provide access to helper methods from outside controllers and views,
4
+ # such as in Presenter objects. Rails provides ActionController::Base.helpers,
5
+ # but this does not include any of our application helpers.
6
+ def all_helpers
7
+ @all_helpers_proxy ||= begin
8
+ # Start with just the rails helpers. This is the same method used
9
+ # by ActionController::Base.helpers
10
+ # proxy = ActionView::Base.new.extend(_helpers)
11
+ proxy = ActionController::Base.helpers
12
+
13
+ # url_for depends on _routes method being defined
14
+ proxy.instance_eval do
15
+ def _routes
16
+ Rails.application.routes
17
+ end
18
+ end
19
+
20
+ # Import all named path methods
21
+ proxy.extend(Rails.application.routes.named_routes.module)
22
+
23
+ # Load all our application helpers to extend
24
+ modules_for_helpers([:all]).each do |mod|
25
+ proxy.extend(mod)
26
+ end
27
+
28
+ proxy.instance_eval do
29
+ # A hack since this proxy doesn't pick up default_url_options from anywhere
30
+ def url_for(*args)
31
+ if args.last.is_a?(Hash) && !args.last[:only_path]
32
+ args = args.dup
33
+ args << args.pop.merge(host: ActionMailer::Base.default_url_options[:host])
34
+ end
35
+ super(*args)
36
+ end
37
+ end
38
+
39
+ proxy
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,23 +1,28 @@
1
1
  module Draper
2
- class Base
3
- include ActionView::Helpers::TagHelper
4
- include ActionView::Helpers::UrlHelper
5
- include ActionView::Helpers::TextHelper
6
-
2
+ class Base
7
3
  require 'active_support/core_ext/class/attribute'
8
- class_attribute :denied, :allowed, :source_class
9
- attr_accessor :source
4
+ class_attribute :denied, :allowed, :model_class
5
+ attr_accessor :model
10
6
 
11
7
  DEFAULT_DENIED = Object.new.methods << :method_missing
8
+ FORCED_PROXY = [:to_param]
12
9
  self.denied = DEFAULT_DENIED
13
10
 
14
- def initialize(subject)
15
- subject.inspect
16
- self.class.source_class = subject.class
17
- self.source = subject
11
+ def initialize(input)
12
+ input.inspect
13
+ self.class.model_class = input.class if model_class.nil?
14
+ @model = input
18
15
  build_methods
19
16
  end
20
17
 
18
+ def self.find(input)
19
+ self.new(model_class.find(input))
20
+ end
21
+
22
+ def self.decorates(input)
23
+ self.model_class = input.to_s.classify.constantize
24
+ end
25
+
21
26
  def self.denies(*input_denied)
22
27
  raise ArgumentError, "Specify at least one method (as a symbol) to exclude when using denies" if input_denied.empty?
23
28
  raise ArgumentError, "Use either 'allows' or 'denies', but not both." if self.allowed?
@@ -35,25 +40,29 @@ module Draper
35
40
  end
36
41
 
37
42
  def helpers
38
- ActionController::Base.helpers
43
+ @helpers ||= ApplicationController::all_helpers
39
44
  end
45
+ alias :h :helpers
40
46
 
41
47
  def self.model_name
42
48
  ActiveModel::Name.new(source_class)
43
49
  end
44
-
45
- private
50
+
51
+ def to_model
52
+ @model
53
+ end
54
+
55
+ private
46
56
  def select_methods
47
- self.allowed || (source.public_methods - denied)
57
+ specified = self.allowed || (model.public_methods - denied)
58
+ (specified - self.public_methods) + FORCED_PROXY
48
59
  end
49
60
 
50
61
  def build_methods
51
62
  select_methods.each do |method|
52
- unless self.respond_to?(method)
53
- (class << self; self; end).class_eval do
54
- define_method method do |*args, &block|
55
- source.send method, *args, &block
56
- end
63
+ (class << self; self; end).class_eval do
64
+ define_method method do |*args, &block|
65
+ model.send method, *args, &block
57
66
  end
58
67
  end
59
68
  end
@@ -0,0 +1,11 @@
1
+ module Draper
2
+ module LazyHelpers
3
+ def method_missing(method_name, *args, &block)
4
+ begin
5
+ helpers.send method_name, *args, &block
6
+ rescue NoMethodError
7
+ super
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module Draper
2
+ class System
3
+ def self.setup
4
+ ActionController::Base.send(:extend, Draper::AllHelpers) if defined?(ActionController::Base)
5
+ end
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Draper
2
- VERSION = "0.4.1.1"
2
+ VERSION = "0.4.2"
3
3
  end
@@ -1,24 +1,28 @@
1
1
  class <%= singular_name.camelize %>Decorator < Draper::Base
2
+ decorates :<%= singular_name.to_sym %>
2
3
 
3
- # Rails Helpers
4
- # Rails helpers like content_tag, link_to, and pluralize are already
5
- # available to you. If you need access to other helpers, include them
6
- # like this:
7
- # include ActionView::Helpers::TextHelper
8
- # Or pull in the whole kitchen sink:
9
- # include ActionView::Helpers
4
+ # Helpers from Rails an Your Application
5
+ # You can access any helper via a proxy to ApplicationController
6
+ #
7
+ # Normal Usage: helpers.number_to_currency(2)
8
+ # Abbreviated : h.number_to_currency(2)
9
+ #
10
+ # You can optionally enable "lazy helpers" by including this module:
11
+ # include Draper::LazyHelpers
12
+ # Then use the helpers with no prefix:
13
+ # number_to_currency(2)
10
14
 
11
15
  # Wrapper Methods
12
16
  # Control access to the wrapped subject's methods using one of the following:
13
17
  #
14
- # To allow _only_ the listed methods:
18
+ # To allow only the listed methods (whitelist):
15
19
  # allows :method1, :method2
16
20
  #
17
- # To allow everything _except_ the listed methods:
21
+ # To allow everything except the listed methods (blacklist):
18
22
  # denies :method1, :method2
19
23
 
20
24
  # Presentation Methods
21
- # Define presentation-related instance methods. Ex:
25
+ # Define your own instance methods. Ex:
22
26
  # def formatted_created_at
23
27
  # content_tag :span, created_at.strftime("%A")
24
28
  # end
@@ -3,19 +3,67 @@ require 'draper'
3
3
 
4
4
  describe Draper::Base do
5
5
  subject{ Draper::Base.new(source) }
6
- let(:source){ "Sample String" }
7
-
8
- it "should return the wrapped object when asked for source" do
9
- subject.source.should == source
6
+ let(:source){ Product.new }
7
+
8
+ context(".decorates") do
9
+ it "sets the model class for the decorator" do
10
+ ProductDecorator.new(source).model_class == Product
11
+ end
10
12
  end
11
13
 
14
+ context(".model / .to_model") do
15
+ it "should return the wrapped object" do
16
+ subject.to_model.should == source
17
+ subject.model.should == source
18
+ end
19
+ end
20
+
21
+ context("selecting methods") do
22
+ it "echos the methods of the wrapped class except default exclusions" do
23
+ source.methods.each do |method|
24
+ unless Draper::Base::DEFAULT_DENIED.include?(method)
25
+ subject.should respond_to(method)
26
+ end
27
+ end
28
+ end
29
+
30
+ it "should not override a defined method with a source method" do
31
+ DecoratorWithApplicationHelper.new(source).length.should == "overridden"
32
+ end
33
+
34
+ it "should always proxy to_param" do
35
+ source.send :class_eval, "def to_param; 1; end"
36
+ Draper::Base.new(source).to_param.should == 1
37
+ end
38
+
39
+ it "should not copy the .class, .inspect, or other existing methods" do
40
+ source.class.should_not == subject.class
41
+ source.inspect.should_not == subject.inspect
42
+ source.to_s.should_not == subject.to_s
43
+ end
44
+ end
45
+
12
46
  it "should wrap source methods so they still accept blocks" do
13
- subject.gsub("Sample"){|match| "Super"}.should == "Super String"
47
+ subject.block{"marker"}.should == "marker"
48
+ end
49
+
50
+ context ".find" do
51
+ it "should lookup the associated model when passed an integer" do
52
+ pd = ProductDecorator.find(1)
53
+ pd.should be_instance_of(ProductDecorator)
54
+ pd.model.should be_instance_of(Product)
55
+ end
56
+
57
+ it "should lookup the associated model when passed a string" do
58
+ pd = ProductDecorator.find("1")
59
+ pd.should be_instance_of(ProductDecorator)
60
+ pd.model.should be_instance_of(Product)
61
+ end
14
62
  end
15
63
 
16
- context ".draper" do
64
+ context ".decorate" do
17
65
  it "should return a collection of wrapped objects when given a collection of source objects" do
18
- sources = ["one", "two", "three"]
66
+ sources = [Product.new, Product.new]
19
67
  output = Draper::Base.decorate(sources)
20
68
  output.should respond_to(:each)
21
69
  output.size.should == sources.size
@@ -27,49 +75,25 @@ describe Draper::Base do
27
75
  output.should be_instance_of(Draper::Base)
28
76
  end
29
77
  end
30
-
31
- it "echos the methods of the wrapped class" do
32
- source.methods.each do |method|
33
- subject.should respond_to(method)
34
- end
35
- end
36
-
37
- it "should not copy the .class, .inspect, or other existing methods" do
38
- source.class.should_not == subject.class
39
- source.inspect.should_not == subject.inspect
40
- source.to_s.should_not == subject.to_s
41
- end
42
-
43
- describe "a sample usage with denies" do
44
- before(:all) do
45
- end
46
78
 
79
+ describe "a sample usage with denies" do
47
80
  let(:subject_with_denies){ DecoratorWithDenies.new(source) }
48
81
 
82
+ it "should proxy methods not listed in denies" do
83
+ subject_with_denies.should respond_to(:hello_world)
84
+ end
85
+
49
86
  it "should not echo methods specified with denies" do
50
- subject_with_denies.should_not respond_to(:upcase)
87
+ subject_with_denies.should_not respond_to(:goodnight_moon)
51
88
  end
52
89
 
53
90
  it "should not clobber other decorators' methods" do
54
- subject.should respond_to(:upcase)
91
+ subject.should respond_to(:hello_world)
55
92
  end
56
93
 
57
- it "should be able to use the content_tag helper" do
58
- subject_with_denies.sample_content.to_s.should == "<span>Hello, World!</span>"
59
- end
60
-
61
- it "should be able to use the link_to helper" do
62
- subject_with_denies.sample_link.should == "<a href=\"/World\">Hello</a>"
63
- end
64
-
65
- it "should be able to use the pluralize helper" do
66
- pending("Figure out odd interaction when the wrapped source object already has the text_helper methods (ie: a String)")
67
- subject_with_denies.sample_truncate.should == "Once..."
68
- end
69
-
70
- it "should nullify method_missing to prevent AR from being cute" do
71
- pending("How to test this without AR? Ugh.")
72
- end
94
+ it "should not allow method_missing to circumvent a deny" do
95
+ expect{subject_with_denies.title}.to raise_error(NoMethodError)
96
+ end
73
97
  end
74
98
 
75
99
  describe "a sample usage with allows" do
@@ -99,15 +123,15 @@ describe Draper::Base do
99
123
 
100
124
  let(:using_allows_then_denies){
101
125
  class DecoratorWithAllowsAndDenies < Draper::Base
102
- allows :upcase
103
- denies :downcase
126
+ allows :hello_world
127
+ denies :goodnight_moon
104
128
  end
105
129
  }
106
130
 
107
131
  let(:using_denies_then_allows){
108
132
  class DecoratorWithDeniesAndAllows < Draper::Base
109
- denies :downcase
110
- allows :upcase
133
+ denies :goodnight_moon
134
+ allows :hello_world
111
135
  end
112
136
  }
113
137
 
@@ -129,10 +153,22 @@ describe Draper::Base do
129
153
  end
130
154
 
131
155
  context "in a Rails application" do
132
- it "should include ApplicationHelper if one exists" do
133
- pending
134
- decorator = DecoratorApplicationHelper.decorate(Object.new)
135
- decorator.uses_hello == "Hello, World!"
156
+ let(:decorator){ DecoratorWithApplicationHelper.decorate(Object.new) }
157
+
158
+ it "should have access to ApplicationHelper helpers" do
159
+ decorator.uses_hello_world == "Hello, World!"
160
+ end
161
+
162
+ it "should be able to use the content_tag helper" do
163
+ decorator.sample_content.to_s.should == "<span>Hello, World!</span>"
164
+ end
165
+
166
+ it "should be able to use the link_to helper" do
167
+ decorator.sample_link.should == "<a href=\"/World\">Hello</a>"
168
+ end
169
+
170
+ it "should be able to use the pluralize helper" do
171
+ decorator.sample_truncate.should == "Once..."
136
172
  end
137
173
  end
138
174
  end
@@ -0,0 +1,7 @@
1
+ module ActiveRecord
2
+ class Base
3
+ def method_missing(name, *args)
4
+ name
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ class ApplicationController
2
+ extend ActionView::Helpers
3
+ extend ActionView::Helpers::TagHelper
4
+ extend ActionView::Helpers::UrlHelper
5
+ extend ApplicationHelper
6
+
7
+ def self.all_helpers
8
+ self
9
+ end
10
+ end
@@ -1,5 +1,5 @@
1
1
  module ApplicationHelper
2
- def hello
2
+ def hello_world
3
3
  "Hello, World!"
4
4
  end
5
5
  end
@@ -0,0 +1,21 @@
1
+ class DecoratorWithApplicationHelper < Draper::Base
2
+ def uses_hello_world
3
+ h.hello_world
4
+ end
5
+
6
+ def sample_content
7
+ h.content_tag :span, "Hello, World!"
8
+ end
9
+
10
+ def sample_link
11
+ h.link_to "Hello", "/World"
12
+ end
13
+
14
+ def sample_truncate
15
+ h.truncate("Once upon a time", :length => 7)
16
+ end
17
+
18
+ def length
19
+ "overridden"
20
+ end
21
+ end
@@ -1,15 +1,3 @@
1
1
  class DecoratorWithDenies < Draper::Base
2
- denies :upcase
3
-
4
- def sample_content
5
- content_tag :span, "Hello, World!"
6
- end
7
-
8
- def sample_link
9
- link_to "Hello", "/World"
10
- end
11
-
12
- def sample_truncate
13
- ActionView::Helpers::TextHelper.truncate("Once upon a time", :length => 7)
14
- end
2
+ denies :goodnight_moon, :title
15
3
  end
@@ -0,0 +1,21 @@
1
+ class Product < ActiveRecord::Base
2
+ def self.find(id)
3
+ return Product.new
4
+ end
5
+
6
+ def hello_world
7
+ "Hello, World"
8
+ end
9
+
10
+ def goodnight_moon
11
+ "Goodnight, Moon"
12
+ end
13
+
14
+ def title
15
+ "Sample Title"
16
+ end
17
+
18
+ def block
19
+ yield
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ class ProductDecorator < Draper::Base
2
+ decorates :product
3
+ end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: draper
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.4.1.1
5
+ version: 0.4.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - Jeff Casimir
@@ -10,11 +10,12 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-07-12 00:00:00 -04:00
13
+ date: 2011-07-26 00:00:00 -05:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rake
18
+ prerelease: false
18
19
  requirement: &id001 !ruby/object:Gem::Requirement
19
20
  none: false
20
21
  requirements:
@@ -22,10 +23,10 @@ dependencies:
22
23
  - !ruby/object:Gem::Version
23
24
  version: 0.8.7
24
25
  type: :development
25
- prerelease: false
26
26
  version_requirements: *id001
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec
29
+ prerelease: false
29
30
  requirement: &id002 !ruby/object:Gem::Requirement
30
31
  none: false
31
32
  requirements:
@@ -33,10 +34,10 @@ dependencies:
33
34
  - !ruby/object:Gem::Version
34
35
  version: 2.0.1
35
36
  type: :development
36
- prerelease: false
37
37
  version_requirements: *id002
38
38
  - !ruby/object:Gem::Dependency
39
39
  name: activesupport
40
+ prerelease: false
40
41
  requirement: &id003 !ruby/object:Gem::Requirement
41
42
  none: false
42
43
  requirements:
@@ -44,10 +45,10 @@ dependencies:
44
45
  - !ruby/object:Gem::Version
45
46
  version: 3.0.9
46
47
  type: :development
47
- prerelease: false
48
48
  version_requirements: *id003
49
49
  - !ruby/object:Gem::Dependency
50
50
  name: actionpack
51
+ prerelease: false
51
52
  requirement: &id004 !ruby/object:Gem::Requirement
52
53
  none: false
53
54
  requirements:
@@ -55,10 +56,10 @@ dependencies:
55
56
  - !ruby/object:Gem::Version
56
57
  version: 3.0.9
57
58
  type: :development
58
- prerelease: false
59
59
  version_requirements: *id004
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: ruby-debug19
62
+ prerelease: false
62
63
  requirement: &id005 !ruby/object:Gem::Requirement
63
64
  none: false
64
65
  requirements:
@@ -66,10 +67,10 @@ dependencies:
66
67
  - !ruby/object:Gem::Version
67
68
  version: "0"
68
69
  type: :development
69
- prerelease: false
70
70
  version_requirements: *id005
71
71
  - !ruby/object:Gem::Dependency
72
72
  name: guard
73
+ prerelease: false
73
74
  requirement: &id006 !ruby/object:Gem::Requirement
74
75
  none: false
75
76
  requirements:
@@ -77,10 +78,10 @@ dependencies:
77
78
  - !ruby/object:Gem::Version
78
79
  version: "0"
79
80
  type: :development
80
- prerelease: false
81
81
  version_requirements: *id006
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: guard-rspec
84
+ prerelease: false
84
85
  requirement: &id007 !ruby/object:Gem::Requirement
85
86
  none: false
86
87
  requirements:
@@ -88,10 +89,10 @@ dependencies:
88
89
  - !ruby/object:Gem::Version
89
90
  version: "0"
90
91
  type: :development
91
- prerelease: false
92
92
  version_requirements: *id007
93
93
  - !ruby/object:Gem::Dependency
94
94
  name: rb-fsevent
95
+ prerelease: false
95
96
  requirement: &id008 !ruby/object:Gem::Requirement
96
97
  none: false
97
98
  requirements:
@@ -99,7 +100,6 @@ dependencies:
99
100
  - !ruby/object:Gem::Version
100
101
  version: "0"
101
102
  type: :development
102
- prerelease: false
103
103
  version_requirements: *id008
104
104
  description: Draper reimagines the role of helpers in the view layer of a Rails application, allowing an object-oriented approach rather than procedural.
105
105
  email:
@@ -118,16 +118,23 @@ files:
118
118
  - Readme.markdown
119
119
  - draper.gemspec
120
120
  - lib/draper.rb
121
+ - lib/draper/all_helpers.rb
121
122
  - lib/draper/base.rb
123
+ - lib/draper/lazy_helpers.rb
124
+ - lib/draper/system.rb
122
125
  - lib/draper/version.rb
123
126
  - lib/generators/draper/model/USAGE
124
127
  - lib/generators/draper/model/model_generator.rb
125
128
  - lib/generators/draper/model/templates/model.rb
126
129
  - spec/base_spec.rb
130
+ - spec/samples/active_record.rb
131
+ - spec/samples/application_controller.rb
127
132
  - spec/samples/application_helper.rb
128
- - spec/samples/decorator_application_helper.rb
129
133
  - spec/samples/decorator_with_allows.rb
134
+ - spec/samples/decorator_with_application_helper.rb
130
135
  - spec/samples/decorator_with_denies.rb
136
+ - spec/samples/product.rb
137
+ - spec/samples/product_decorator.rb
131
138
  - spec/spec_helper.rb
132
139
  has_rdoc: true
133
140
  homepage: http://github.com/jcasimir/draper
@@ -143,18 +150,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
143
150
  requirements:
144
151
  - - ">="
145
152
  - !ruby/object:Gem::Version
146
- hash: 4442743917446124071
147
- segments:
148
- - 0
149
153
  version: "0"
150
154
  required_rubygems_version: !ruby/object:Gem::Requirement
151
155
  none: false
152
156
  requirements:
153
157
  - - ">="
154
158
  - !ruby/object:Gem::Version
155
- hash: 4442743917446124071
156
- segments:
157
- - 0
158
159
  version: "0"
159
160
  requirements: []
160
161
 
@@ -165,8 +166,12 @@ specification_version: 3
165
166
  summary: Decorator pattern implmentation for Rails.
166
167
  test_files:
167
168
  - spec/base_spec.rb
169
+ - spec/samples/active_record.rb
170
+ - spec/samples/application_controller.rb
168
171
  - spec/samples/application_helper.rb
169
- - spec/samples/decorator_application_helper.rb
170
172
  - spec/samples/decorator_with_allows.rb
173
+ - spec/samples/decorator_with_application_helper.rb
171
174
  - spec/samples/decorator_with_denies.rb
175
+ - spec/samples/product.rb
176
+ - spec/samples/product_decorator.rb
172
177
  - spec/spec_helper.rb
@@ -1,5 +0,0 @@
1
- class DecoratorApplicationHelper < Draper::Base
2
- def uses_hello
3
- self.hello
4
- end
5
- end