draper 0.4.1.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Readme.markdown +135 -67
- data/lib/draper.rb +6 -1
- data/lib/draper/all_helpers.rb +43 -0
- data/lib/draper/base.rb +29 -20
- data/lib/draper/lazy_helpers.rb +11 -0
- data/lib/draper/system.rb +7 -0
- data/lib/draper/version.rb +1 -1
- data/lib/generators/draper/model/templates/model.rb +14 -10
- data/spec/base_spec.rb +85 -49
- data/spec/samples/active_record.rb +7 -0
- data/spec/samples/application_controller.rb +10 -0
- data/spec/samples/application_helper.rb +1 -1
- data/spec/samples/decorator_with_application_helper.rb +21 -0
- data/spec/samples/decorator_with_denies.rb +1 -13
- data/spec/samples/product.rb +21 -0
- data/spec/samples/product_decorator.rb +3 -0
- metadata +23 -18
- data/spec/samples/decorator_application_helper.rb +0 -5
data/Readme.markdown
CHANGED
@@ -1,70 +1,114 @@
|
|
1
|
-
|
1
|
+
# Draper: View Models for Rails
|
2
2
|
|
3
|
-
|
3
|
+
## Quick Start
|
4
4
|
|
5
|
-
|
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
|
-
|
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
|
-
|
25
|
+
### 1. Object Oriented Helpers
|
18
26
|
|
19
|
-
In
|
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
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
content_tag
|
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
|
-
|
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
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
91
|
+
A better approach is to define a whitelist using `allows`:
|
60
92
|
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
##
|
109
|
+
## Up an Running
|
66
110
|
|
67
|
-
|
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
|
-
|
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
|
-
|
133
|
+
### Writing Methods
|
88
134
|
|
89
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
148
|
+
### Using Existing Helpers
|
100
149
|
|
101
|
-
|
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
|
-
|
109
|
-
|
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
|
-
|
164
|
+
### In the Controller
|
114
165
|
|
115
|
-
|
166
|
+
When writing your controller actions, you have three options:
|
116
167
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
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
|
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
|
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.
|
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.
|
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
|
-
|
187
|
-
|
188
|
-
|
189
|
-
content_tag
|
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
|
-
*
|
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
|
|
data/lib/draper.rb
CHANGED
@@ -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
|
data/lib/draper/base.rb
CHANGED
@@ -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, :
|
9
|
-
attr_accessor
|
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(
|
15
|
-
|
16
|
-
self.class.
|
17
|
-
|
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
|
-
|
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
|
-
|
50
|
+
|
51
|
+
def to_model
|
52
|
+
@model
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
46
56
|
def select_methods
|
47
|
-
self.allowed || (
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
data/lib/draper/version.rb
CHANGED
@@ -1,24 +1,28 @@
|
|
1
1
|
class <%= singular_name.camelize %>Decorator < Draper::Base
|
2
|
+
decorates :<%= singular_name.to_sym %>
|
2
3
|
|
3
|
-
# Rails
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
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
|
18
|
+
# To allow only the listed methods (whitelist):
|
15
19
|
# allows :method1, :method2
|
16
20
|
#
|
17
|
-
# To allow everything
|
21
|
+
# To allow everything except the listed methods (blacklist):
|
18
22
|
# denies :method1, :method2
|
19
23
|
|
20
24
|
# Presentation Methods
|
21
|
-
# Define
|
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
|
data/spec/base_spec.rb
CHANGED
@@ -3,19 +3,67 @@ require 'draper'
|
|
3
3
|
|
4
4
|
describe Draper::Base do
|
5
5
|
subject{ Draper::Base.new(source) }
|
6
|
-
let(:source){
|
7
|
-
|
8
|
-
|
9
|
-
|
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.
|
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 ".
|
64
|
+
context ".decorate" do
|
17
65
|
it "should return a collection of wrapped objects when given a collection of source objects" do
|
18
|
-
sources = [
|
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(:
|
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(:
|
91
|
+
subject.should respond_to(:hello_world)
|
55
92
|
end
|
56
93
|
|
57
|
-
it "should
|
58
|
-
subject_with_denies.
|
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 :
|
103
|
-
denies :
|
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 :
|
110
|
-
allows :
|
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
|
-
|
133
|
-
|
134
|
-
|
135
|
-
decorator.
|
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,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 :
|
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
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: draper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.4.
|
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-
|
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
|