decent_exposure 1.0.2 → 2.0.0.rc1
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.md +349 -156
- data/lib/decent_exposure.rb +3 -32
- data/lib/decent_exposure/active_record_strategy.rb +68 -0
- data/lib/decent_exposure/active_record_with_eager_attributes_strategy.rb +30 -0
- data/lib/decent_exposure/configuration.rb +19 -0
- data/lib/decent_exposure/expose.rb +60 -0
- data/lib/decent_exposure/exposure.rb +17 -0
- data/lib/decent_exposure/inflector.rb +34 -0
- data/lib/decent_exposure/strategizer.rb +60 -0
- data/lib/decent_exposure/strategy.rb +37 -0
- data/lib/decent_exposure/version.rb +1 -1
- metadata +28 -35
- data/COPYING +0 -12
- data/init.rb +0 -2
- data/lib/decent_exposure/default_exposure.rb +0 -28
- data/lib/decent_exposure/railtie.rb +0 -20
- data/rails/init.rb +0 -1
data/README.md
CHANGED
@@ -1,163 +1,356 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
views.
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
In `Gemfile`:
|
24
|
-
|
25
|
-
gem 'decent_exposure'
|
26
|
-
|
27
|
-
|
28
|
-
Examples
|
29
|
-
--------
|
30
|
-
|
31
|
-
### Railscast
|
32
|
-
|
33
|
-
Ryan Bates has a Railscasts episode covering `decent_exposure`. If you're just
|
34
|
-
getting started or just enjoy screencasts (Ryan's are always great), you can
|
35
|
-
check it out here: [Railscasts - Decent Exposure][railscast].
|
36
|
-
|
37
|
-
### A full example
|
38
|
-
|
39
|
-
The wiki has a full example of [converting a classic-style Rails
|
40
|
-
controller][converting].
|
41
|
-
|
42
|
-
### In your controllers
|
43
|
-
|
44
|
-
When no block is given, `expose` attempts to determine which resource you want
|
45
|
-
to acquire. When `params` contains `:category_id` or `:id`, a call to:
|
46
|
-
|
47
|
-
expose(:category)
|
48
|
-
|
49
|
-
Would result in the following `ActiveRecord#find`:
|
50
|
-
|
51
|
-
Category.find(params[:category_id]||params[:id])
|
52
|
-
|
53
|
-
As the example shows, the symbol passed is used to guess the class name of the
|
54
|
-
object (and potentially the `params` key to find it with) you want an instance
|
55
|
-
of.
|
56
|
-
|
57
|
-
Should `params` not contain an identifiable `id`, a call to:
|
58
|
-
|
59
|
-
expose(:category)
|
60
|
-
|
61
|
-
Will instead attempt to build a new instance of the object like so:
|
62
|
-
|
63
|
-
Category.new(params[:category])
|
64
|
-
|
65
|
-
If you define a collection with a pluralized name of the singular resource,
|
66
|
-
`decent_exposure` will attempt to use it to scope its calls from. Let's take the
|
67
|
-
following scenario:
|
68
|
-
|
69
|
-
class ProductsController < ApplicationController
|
70
|
-
expose(:category)
|
71
|
-
expose(:products) { category.products }
|
72
|
-
expose(:product)
|
1
|
+
## Mad Decent
|
2
|
+
|
3
|
+
Rails controllers are the sweaty armpit of every rails app. This is due, in
|
4
|
+
large part, to the fact that they expose their instance variables directly to
|
5
|
+
their views. This means that your instance variables are your interface... and
|
6
|
+
that you've broken encapsulation. Instance variables are meant to be private,
|
7
|
+
for Science's sake!
|
8
|
+
|
9
|
+
What `decent_exposure` proposes is that you go from this:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class Controller
|
13
|
+
def new
|
14
|
+
@person = Person.new(params[:person])
|
15
|
+
end
|
16
|
+
|
17
|
+
def create
|
18
|
+
@person = Person.new(params[:person])
|
19
|
+
if @person.save
|
20
|
+
redirect_to(@person)
|
21
|
+
else
|
22
|
+
render :new
|
73
23
|
end
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
actions in a typicall Rails-style RESTful controller.
|
87
|
-
|
88
|
-
#### A Note on Style
|
89
|
-
|
90
|
-
When the code has become complex enough to surpass a single line (and is not
|
91
|
-
appropriate to extract into a model method), use the `do...end` style of block:
|
92
|
-
|
93
|
-
expose(:associated_products) do
|
94
|
-
product.associated.tap do |associated_products|
|
95
|
-
present(associated_products, :with => AssociatedProductPresenter)
|
96
|
-
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def edit
|
27
|
+
@person = Person.find(params[:id])
|
28
|
+
end
|
29
|
+
|
30
|
+
def update
|
31
|
+
@person = Person.find(params[:id])
|
32
|
+
if @person.update_attributes(params[:person])
|
33
|
+
redirect_to(@person)
|
34
|
+
else
|
35
|
+
render :edit
|
97
36
|
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
98
40
|
|
99
|
-
|
100
|
-
|
101
|
-
Use the product of those assignments like you would an instance variable or any
|
102
|
-
other method you might normally have access to:
|
103
|
-
|
104
|
-
= render bread_crumbs_for(category)
|
105
|
-
%h3#product_title= product.title
|
106
|
-
= render product
|
107
|
-
%h3 Associated Products
|
108
|
-
%ul
|
109
|
-
- associated_products.each do |associated_product|
|
110
|
-
%li= link_to(associated_product.title,product_path(associated_product))
|
111
|
-
|
112
|
-
### Custom defaults
|
41
|
+
To something like this:
|
113
42
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
inside of a controller. The argument will be the string or symbol passed in to
|
118
|
-
the `expose` call.
|
43
|
+
```ruby
|
44
|
+
class Controller
|
45
|
+
expose(:person)
|
119
46
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
47
|
+
def create
|
48
|
+
if person.save
|
49
|
+
redirect_to(person)
|
50
|
+
else
|
51
|
+
render :new
|
124
52
|
end
|
53
|
+
end
|
125
54
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
55
|
+
def update
|
56
|
+
if person.save
|
57
|
+
redirect_to(person)
|
58
|
+
else
|
59
|
+
render :edit
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
`decent_exposure` makes it easy to define named methods that are made available
|
66
|
+
to your views and which memoize the resultant values. It also tucks away the
|
67
|
+
details of the common fetching, initializing and updating of resources and
|
68
|
+
their parameters.
|
69
|
+
|
70
|
+
That's neat and all, but the real advantage comes when it's time to refactor
|
71
|
+
(because you've encapsulated now). What happens when you need to scope your
|
72
|
+
`Person` resource from a `Company`? Which implementation isolates those changes
|
73
|
+
better? In that particular example, `decent_exposure` goes one step farther and
|
74
|
+
will handle the scoping for you (with a smidge of configuration) while still
|
75
|
+
handling all that repetitive initialization, as we'll see next.
|
76
|
+
|
77
|
+
Even if you decide not to use `decent_exposure`, do yourself a favor and stop
|
78
|
+
using instance variables in your views. Your code will be cleaner and easier to
|
79
|
+
refactor as a result. If you want to learn more about his approach, I've
|
80
|
+
expanded on my thoughts in the article [A Diatribe on Maintaining State][1].
|
81
|
+
|
82
|
+
## Environmental Awareness
|
83
|
+
|
84
|
+
Well, no it won't lessen your carbon footprint, but it does take a lot of
|
85
|
+
cues from what's going on around it...
|
86
|
+
|
87
|
+
`decent_exposure` will build the requested object in one of a couple of ways
|
88
|
+
depending on what the `params` make available to it. At its simplest, when an
|
89
|
+
`id` is present in the `params` hash, `decent_exposure` will attempt to find a
|
90
|
+
record. In absence of `params[:id]` `decent_exposure` will try to build a new
|
91
|
+
record.
|
92
|
+
|
93
|
+
Once the object has been obtained, it attempts to set the attributes of the
|
94
|
+
resulting object. Thus, a newly minted `person` instance will get any
|
95
|
+
attributes set that've been passed along in `params[:person]`. When you
|
96
|
+
interact with `person` in your create action, just call save on it and handle
|
97
|
+
the valid/invalid branch. Let's revisit our previous example:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class Controller
|
101
|
+
expose(:person)
|
102
|
+
|
103
|
+
def create
|
104
|
+
if person.save
|
105
|
+
redirect_to(person)
|
106
|
+
else
|
107
|
+
render :new
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
Behind the scenes, `decent_exposure` has essentially done this:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
person.attributes = params[:person]
|
117
|
+
```
|
118
|
+
|
119
|
+
In Rails, this assignment is actually a merge with the current attributes and
|
120
|
+
it marks attributes as dirty as you would expect. This is why you're simply
|
121
|
+
able to call `save` on the `person` instance in the create action, rather than
|
122
|
+
the typical `update_attributes(params[:person])`.
|
123
|
+
|
124
|
+
**An Aside**
|
125
|
+
|
126
|
+
Did you notice there's no `new` action? Yeah, that's because we don't need it.
|
127
|
+
More often than not actions that respond to `GET` requests are just setting up
|
128
|
+
state. Since we've declared an interface to our state and made it available to
|
129
|
+
the view (a.k.a. the place where we actually want to access it), we just let
|
130
|
+
Rails do it's magic and render the `new` view, lazily evaluating `person` when
|
131
|
+
we actually need it.
|
132
|
+
|
133
|
+
**A Caveat**
|
134
|
+
|
135
|
+
Rails conveniently responds with a 404 if you get a record not found in the
|
136
|
+
controller. Since we don't find the object until we're in the view in this
|
137
|
+
paradigm, we get an ugly `ActionView::TemplateError` instead. If this is
|
138
|
+
problematic for you, consider using the `expose!` method to circumvent lazy
|
139
|
+
evaluation and eagerly evaluate whilst still in the controller.
|
140
|
+
|
141
|
+
## Usage
|
142
|
+
|
143
|
+
In an effort to make the examples below a bit less magical, we'll offer a
|
144
|
+
simplified explanation for how the exposed resource would be queried for
|
145
|
+
(assuming you are using `ActiveRecord`).
|
146
|
+
|
147
|
+
### Obtaining an instance of an object:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
expose(:person)
|
151
|
+
```
|
152
|
+
|
153
|
+
**Query Explanation**
|
154
|
+
|
155
|
+
<table>
|
156
|
+
<tr>
|
157
|
+
<td><code>id</code> present?</td>
|
158
|
+
<td>Query</td>
|
159
|
+
</tr>
|
160
|
+
<tr>
|
161
|
+
<td><code>true</code></td>
|
162
|
+
<td><code>Person.find(params[:id])</code></td>
|
163
|
+
</tr>
|
164
|
+
<tr>
|
165
|
+
<td><code>false</code></td>
|
166
|
+
<td><code>Person.new(params[:person])</code></td>
|
167
|
+
</tr>
|
168
|
+
</table>
|
169
|
+
|
170
|
+
### Obtaining a collection of objects
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
expose(:people)
|
174
|
+
```
|
175
|
+
|
176
|
+
**Query Explanation**
|
177
|
+
|
178
|
+
<table>
|
179
|
+
<tr>
|
180
|
+
<td>Query</td>
|
181
|
+
</tr>
|
182
|
+
<tr>
|
183
|
+
<td><code>Person.scoped</code></td>
|
184
|
+
</tr>
|
185
|
+
</table>
|
186
|
+
|
187
|
+
### Scoping your object queries
|
188
|
+
|
189
|
+
Want to scope your queries to ensure object hierarchy? `decent_exposure`
|
190
|
+
automatically scopes singular forms of a resource from a plural form where
|
191
|
+
they're defined:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
expose(:people)
|
195
|
+
expose(:person)
|
196
|
+
```
|
197
|
+
|
198
|
+
**Query Explanation**
|
199
|
+
|
200
|
+
<table>
|
201
|
+
<tr>
|
202
|
+
<td><code>id</code> present?</td>
|
203
|
+
<td>Query</td>
|
204
|
+
</tr>
|
205
|
+
<tr>
|
206
|
+
<td><code>true</code></td>
|
207
|
+
<td><code>Person.scoped.find(params[:id])</code></td>
|
208
|
+
</tr>
|
209
|
+
<tr>
|
210
|
+
<td><code>false</code></td>
|
211
|
+
<td><code>Person.scoped.new(params[:person])</code></td>
|
212
|
+
</tr>
|
213
|
+
</table>
|
214
|
+
|
215
|
+
How about a more realistic scenario where the object hierarchy specifies
|
216
|
+
something useful, like only finding people in a given company:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
expose(:company)
|
220
|
+
expose(:people, ancestor: :company)
|
221
|
+
expose(:person)
|
222
|
+
```
|
223
|
+
|
224
|
+
**Query Explanation**
|
225
|
+
|
226
|
+
<table>
|
227
|
+
<tr>
|
228
|
+
<td>person <code>id</code> present?</td>
|
229
|
+
<td>Query</td>
|
230
|
+
</tr>
|
231
|
+
<tr>
|
232
|
+
<td><code>true</code></td>
|
233
|
+
<td><code>Company.find(params[:company_id]).people.find(params[:id])</code></td>
|
234
|
+
</tr>
|
235
|
+
<tr>
|
236
|
+
<td><code>false</code></td>
|
237
|
+
<td><code>Company.find(params[:company_id]).people.new(params[:person])</code></td>
|
238
|
+
</tr>
|
239
|
+
</table>
|
240
|
+
|
241
|
+
### Further configuration
|
242
|
+
|
243
|
+
`decent_exposure` is a configurable beast. Let's take a look at some of the
|
244
|
+
things you can do:
|
245
|
+
|
246
|
+
**Specify the model name:**
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
expose(:company, model: :enterprisey_company)
|
250
|
+
```
|
251
|
+
|
252
|
+
**Specify the parameter accessor method:**
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
expose(:company, params: :company_params)
|
256
|
+
```
|
257
|
+
|
258
|
+
**Specify the finder method:**
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
expose(:article, finder: :find_by_slug)
|
262
|
+
```
|
263
|
+
|
264
|
+
**Specify the parameter key to use to fetch the object:**
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
expose(:article, finder_parameter: :slug)
|
268
|
+
```
|
269
|
+
|
270
|
+
### Getting your hands dirty
|
271
|
+
|
272
|
+
While we try to make things as easy for you as possible, sometimes you just
|
273
|
+
need to go off the beaten path. For those times, `expose` takes a block which
|
274
|
+
it lazily evaluates and returns the result of when called. So for instance:
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
expose(:environment) { Rails.env }
|
278
|
+
```
|
279
|
+
|
280
|
+
This block is evaluated and the memoized result is returned whenever you call
|
281
|
+
`environment`.
|
282
|
+
|
283
|
+
### Custom strategies
|
284
|
+
|
285
|
+
For the times when custom behavior is needed for resource finding,
|
286
|
+
`decent_exposure` provides a base class for extending. For example, if
|
287
|
+
scoping a resource from `current_user` is not and option, but you'd like
|
288
|
+
to verify a resource's relationship to the `current_user`, you can use a
|
289
|
+
custom strategy like the following:
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
class VerifiableStrategy < DecentExposure::Strategy
|
293
|
+
delegate :current_user, :to => :controller
|
294
|
+
|
295
|
+
def resource
|
296
|
+
instance = model.find(params[:id])
|
297
|
+
if current_user != instance.user
|
298
|
+
raise ActiveRecord::RecordNotFound
|
299
|
+
end
|
300
|
+
instance
|
301
|
+
end
|
302
|
+
end
|
303
|
+
```
|
304
|
+
|
305
|
+
You would then use your custom strategy in your controller:
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
expose(:post, strategy: VerifiableStrategy)
|
309
|
+
```
|
310
|
+
|
311
|
+
The API only necessitates you to define `resource`, but provides some
|
312
|
+
common helpers to access common things, such as the `params` hash. For
|
313
|
+
everything else, you can delegate to `controller`, which is the same as
|
314
|
+
`self` in the context of a normal controller action.
|
315
|
+
|
316
|
+
### Customizing your exposures
|
317
|
+
|
318
|
+
For most things, you'll be able to pass a few configuration options and get
|
319
|
+
the desired behavior. For changes you want to affect every call to `expose` in
|
320
|
+
a controller or controllers inheriting from it (e.g. `ApplicationController`,
|
321
|
+
if you need to change the behavior for all your controllers), you can define
|
322
|
+
an `decent_configuration` block:
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
class ApplicationController < ActionController::Base
|
326
|
+
decent_configuration do
|
327
|
+
strategy MongoidStrategy
|
328
|
+
end
|
329
|
+
end
|
330
|
+
```
|
331
|
+
|
332
|
+
A `decent_configuration` block without a `:name` argument is considered the
|
333
|
+
"default" configuration for that controller (and it's ancestors). All things
|
334
|
+
considered, you probably only want to change the strategy in a default.
|
335
|
+
Nonetheless, you can pass any configuration option you can to an individual
|
336
|
+
exposure to the `decent_configuration` block.
|
337
|
+
|
338
|
+
If you don't want a specific configuration to affect every exposure in the
|
339
|
+
given controller, you can give it a name like so:
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
class ArticleController < ApplicationController
|
343
|
+
decent_configuration(:sluggable) do
|
344
|
+
finder :find_by_slug
|
345
|
+
finder_parameter :slug
|
346
|
+
end
|
347
|
+
end
|
348
|
+
```
|
349
|
+
|
350
|
+
And opt into it like so:
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
expose(:article, config: :sluggable)
|
354
|
+
```
|
355
|
+
|
356
|
+
[1]: http://blog.voxdolo.me/a-diatribe-on-maintaining-state.html
|
data/lib/decent_exposure.rb
CHANGED
@@ -1,34 +1,5 @@
|
|
1
|
-
require 'decent_exposure/
|
1
|
+
require 'decent_exposure/expose'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
closured_exposure = default_exposure
|
6
|
-
klass.class_eval do
|
7
|
-
default_exposure(&closured_exposure)
|
8
|
-
end
|
9
|
-
super
|
10
|
-
end
|
11
|
-
|
12
|
-
attr_accessor :_default_exposure
|
13
|
-
|
14
|
-
def default_exposure(&block)
|
15
|
-
self._default_exposure = block if block_given?
|
16
|
-
_default_exposure
|
17
|
-
end
|
18
|
-
|
19
|
-
def expose(name, &block)
|
20
|
-
closured_exposure = default_exposure
|
21
|
-
define_method name do
|
22
|
-
@_resources ||= {}
|
23
|
-
@_resources.fetch(name) do
|
24
|
-
@_resources[name] = if block_given?
|
25
|
-
instance_eval(&block)
|
26
|
-
else
|
27
|
-
instance_exec(name, &closured_exposure)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
helper_method name
|
32
|
-
hide_action name
|
33
|
-
end
|
3
|
+
ActiveSupport.on_load(:action_controller) do
|
4
|
+
extend DecentExposure::Expose
|
34
5
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'decent_exposure/strategy'
|
2
|
+
require 'active_support/core_ext/module/delegation'
|
3
|
+
|
4
|
+
module DecentExposure
|
5
|
+
class ActiveRecordStrategy < Strategy
|
6
|
+
delegate :plural?, :parameter, :to => :inflector
|
7
|
+
|
8
|
+
def collection
|
9
|
+
inflector.plural.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
def scope
|
13
|
+
if options[:ancestor]
|
14
|
+
ancestor_scope
|
15
|
+
else
|
16
|
+
default_scope
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def ancestor_scope
|
21
|
+
if plural?
|
22
|
+
controller.send(options[:ancestor]).send(inflector.plural)
|
23
|
+
else
|
24
|
+
controller.send(options[:ancestor])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_scope
|
29
|
+
if controller.respond_to?(collection) && !plural?
|
30
|
+
controller.send(collection)
|
31
|
+
else
|
32
|
+
model
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def finder
|
37
|
+
options[:finder] || :find
|
38
|
+
end
|
39
|
+
|
40
|
+
def collection_resource
|
41
|
+
scope.scoped
|
42
|
+
end
|
43
|
+
|
44
|
+
def id
|
45
|
+
params[parameter] || params[finder_parameter]
|
46
|
+
end
|
47
|
+
|
48
|
+
def finder_parameter
|
49
|
+
options[:finder_parameter] || :id
|
50
|
+
end
|
51
|
+
|
52
|
+
def singular_resource
|
53
|
+
if id
|
54
|
+
scope.send(finder, id)
|
55
|
+
else
|
56
|
+
scope.new
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def resource
|
61
|
+
if plural?
|
62
|
+
collection_resource
|
63
|
+
else
|
64
|
+
singular_resource
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'decent_exposure/active_record_strategy'
|
2
|
+
|
3
|
+
module DecentExposure
|
4
|
+
class ActiveRecordWithEagerAttributesStrategy < ActiveRecordStrategy
|
5
|
+
delegate :get?, :to => :request
|
6
|
+
|
7
|
+
def singular?
|
8
|
+
!plural?
|
9
|
+
end
|
10
|
+
|
11
|
+
def attributes
|
12
|
+
params[inflector.singular]
|
13
|
+
end
|
14
|
+
|
15
|
+
def assign_attributes?
|
16
|
+
return false unless attributes && singular?
|
17
|
+
!get? || new_record?
|
18
|
+
end
|
19
|
+
|
20
|
+
def new_record?
|
21
|
+
!id
|
22
|
+
end
|
23
|
+
|
24
|
+
def resource
|
25
|
+
super.tap do |r|
|
26
|
+
r.attributes = attributes if assign_attributes?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DecentExposure
|
2
|
+
class Configuration
|
3
|
+
def initialize(&block)
|
4
|
+
instance_exec(&block) if block_given?
|
5
|
+
end
|
6
|
+
|
7
|
+
def merge(other)
|
8
|
+
options.merge(other)
|
9
|
+
end
|
10
|
+
|
11
|
+
def options
|
12
|
+
@options ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(key,value)
|
16
|
+
self.options[key] = value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'decent_exposure/strategizer'
|
2
|
+
require 'decent_exposure/configuration'
|
3
|
+
|
4
|
+
module DecentExposure
|
5
|
+
module Expose
|
6
|
+
def self.extended(base)
|
7
|
+
base.class_eval do
|
8
|
+
class_attribute :_default_exposure
|
9
|
+
def _resources
|
10
|
+
@_resources ||= {}
|
11
|
+
end
|
12
|
+
hide_action :_resources
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def _exposures
|
17
|
+
@_exposures ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def _decent_configurations
|
21
|
+
@_decent_configurations ||= Hash.new(Configuration.new)
|
22
|
+
end
|
23
|
+
|
24
|
+
def decent_configuration(name=:default,&block)
|
25
|
+
_decent_configurations[name] = Configuration.new(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_exposure(&block)
|
29
|
+
warn "[DEPRECATION] `default_exposure` is deprecated, and will " \
|
30
|
+
"be removed in DecentExposure 2.1 without a replacement. Please " \
|
31
|
+
"use a custom strategy instead.\n" \
|
32
|
+
"#{caller.first}"
|
33
|
+
self._default_exposure = block
|
34
|
+
end
|
35
|
+
|
36
|
+
def expose!(*args, &block)
|
37
|
+
set_callback(:process_action, :before, args.first)
|
38
|
+
expose(*args, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def expose(name, options={:default_exposure => _default_exposure}, &block)
|
42
|
+
config = options[:config] || :default
|
43
|
+
options = _decent_configurations[config].merge(options)
|
44
|
+
|
45
|
+
_exposures[name] = exposure = Strategizer.new(name, options, &block).strategy
|
46
|
+
|
47
|
+
define_method(name) do
|
48
|
+
return _resources[name] if _resources.has_key?(name)
|
49
|
+
_resources[name] = exposure.call(self)
|
50
|
+
end
|
51
|
+
|
52
|
+
define_method("#{name}=") do |value|
|
53
|
+
_resources[name] = value
|
54
|
+
end
|
55
|
+
|
56
|
+
helper_method name
|
57
|
+
hide_action name
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'decent_exposure/inflector'
|
2
|
+
|
3
|
+
module DecentExposure
|
4
|
+
class Exposure
|
5
|
+
attr_accessor :inflector, :strategy, :options
|
6
|
+
|
7
|
+
def initialize(name, strategy, options)
|
8
|
+
self.strategy = strategy
|
9
|
+
self.options = options
|
10
|
+
self.inflector = DecentExposure::Inflector.new(name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(controller)
|
14
|
+
strategy.new(controller, inflector, options).resource
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'active_support/core_ext/string/inflections'
|
3
|
+
|
4
|
+
module DecentExposure
|
5
|
+
class Inflector
|
6
|
+
attr_reader :original
|
7
|
+
alias name original
|
8
|
+
|
9
|
+
def initialize(name)
|
10
|
+
@original = name.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def constant
|
14
|
+
original.classify.constantize
|
15
|
+
end
|
16
|
+
|
17
|
+
def parameter
|
18
|
+
singular + "_id"
|
19
|
+
end
|
20
|
+
|
21
|
+
def singular
|
22
|
+
original.parameterize
|
23
|
+
end
|
24
|
+
|
25
|
+
def plural
|
26
|
+
original.pluralize
|
27
|
+
end
|
28
|
+
alias collection plural
|
29
|
+
|
30
|
+
def plural?
|
31
|
+
plural == original
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'decent_exposure/exposure'
|
2
|
+
require 'decent_exposure/active_record_with_eager_attributes_strategy'
|
3
|
+
|
4
|
+
module DecentExposure
|
5
|
+
class Strategizer
|
6
|
+
attr_accessor :name, :block, :default_exposure, :options, :custom_strategy_class
|
7
|
+
|
8
|
+
def initialize(name, options={})
|
9
|
+
self.name = name
|
10
|
+
self.default_exposure = options.delete(:default_exposure)
|
11
|
+
self.custom_strategy_class = options.delete(:strategy)
|
12
|
+
self.options = options
|
13
|
+
self.block = Proc.new if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def strategy
|
17
|
+
[block_strategy,
|
18
|
+
default_exposure_strategy,
|
19
|
+
exposure_strategy].detect(&applicable)
|
20
|
+
end
|
21
|
+
|
22
|
+
def model
|
23
|
+
options[:model] || name
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def applicable
|
29
|
+
lambda { |s| s }
|
30
|
+
end
|
31
|
+
|
32
|
+
def exposure_strategy
|
33
|
+
Exposure.new(model, exposure_strategy_class, options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def block_strategy
|
37
|
+
BlockStrategy.new(block) if block
|
38
|
+
end
|
39
|
+
|
40
|
+
def default_exposure_strategy
|
41
|
+
DefaultStrategy.new(name, default_exposure) if default_exposure
|
42
|
+
end
|
43
|
+
|
44
|
+
def exposure_strategy_class
|
45
|
+
custom_strategy_class || ActiveRecordWithEagerAttributesStrategy
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
DefaultStrategy = Struct.new(:name, :block) do
|
50
|
+
def call(controller)
|
51
|
+
controller.instance_exec(name, &block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
BlockStrategy = Struct.new(:block) do
|
56
|
+
def call(controller)
|
57
|
+
controller.instance_eval(&block)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module DecentExposure
|
2
|
+
class Strategy
|
3
|
+
attr_reader :controller, :inflector, :options
|
4
|
+
|
5
|
+
def initialize(controller, inflector, options={})
|
6
|
+
@controller, @inflector, @options = controller, inflector, options
|
7
|
+
end
|
8
|
+
|
9
|
+
def name
|
10
|
+
inflector.name
|
11
|
+
end
|
12
|
+
|
13
|
+
def resource
|
14
|
+
raise 'Implement in subclass'
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def model
|
20
|
+
inflector.constant
|
21
|
+
end
|
22
|
+
|
23
|
+
def params
|
24
|
+
controller.send(params_method)
|
25
|
+
end
|
26
|
+
|
27
|
+
def request
|
28
|
+
controller.request
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def params_method
|
34
|
+
options[:params] || :params
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
metadata
CHANGED
@@ -1,72 +1,62 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: decent_exposure
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
5
|
-
prerelease:
|
4
|
+
version: 2.0.0.rc1
|
5
|
+
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Stephen Caudill
|
9
9
|
- Jon Larkowski
|
10
|
+
- Joshua Davey
|
10
11
|
autorequire:
|
11
12
|
bindir: bin
|
12
13
|
cert_chain: []
|
13
|
-
date: 2012-
|
14
|
+
date: 2012-07-27 00:00:00.000000000Z
|
14
15
|
dependencies:
|
15
16
|
- !ruby/object:Gem::Dependency
|
16
17
|
name: rspec
|
17
|
-
requirement: &
|
18
|
+
requirement: &70110875340260 !ruby/object:Gem::Requirement
|
18
19
|
none: false
|
19
20
|
requirements:
|
20
21
|
- - ~>
|
21
22
|
- !ruby/object:Gem::Version
|
22
|
-
version: 2.
|
23
|
+
version: '2.7'
|
23
24
|
type: :development
|
24
25
|
prerelease: false
|
25
|
-
version_requirements: *
|
26
|
+
version_requirements: *70110875340260
|
26
27
|
- !ruby/object:Gem::Dependency
|
27
|
-
name:
|
28
|
-
requirement: &
|
28
|
+
name: rspec-rails
|
29
|
+
requirement: &70110875338840 !ruby/object:Gem::Requirement
|
29
30
|
none: false
|
30
31
|
requirements:
|
31
32
|
- - ~>
|
32
33
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
34
|
+
version: '2.7'
|
34
35
|
type: :development
|
35
36
|
prerelease: false
|
36
|
-
version_requirements: *
|
37
|
-
- !ruby/object:Gem::Dependency
|
38
|
-
name: ruby-debug19
|
39
|
-
requirement: &70182141019360 !ruby/object:Gem::Requirement
|
40
|
-
none: false
|
41
|
-
requirements:
|
42
|
-
- - ~>
|
43
|
-
- !ruby/object:Gem::Version
|
44
|
-
version: 0.11.6
|
45
|
-
type: :development
|
46
|
-
prerelease: false
|
47
|
-
version_requirements: *70182141019360
|
37
|
+
version_requirements: *70110875338840
|
48
38
|
- !ruby/object:Gem::Dependency
|
49
39
|
name: actionpack
|
50
|
-
requirement: &
|
40
|
+
requirement: &70110875338380 !ruby/object:Gem::Requirement
|
51
41
|
none: false
|
52
42
|
requirements:
|
53
|
-
- -
|
43
|
+
- - ~>
|
54
44
|
- !ruby/object:Gem::Version
|
55
|
-
version: '
|
45
|
+
version: '3.1'
|
56
46
|
type: :development
|
57
47
|
prerelease: false
|
58
|
-
version_requirements: *
|
48
|
+
version_requirements: *70110875338380
|
59
49
|
- !ruby/object:Gem::Dependency
|
60
50
|
name: activesupport
|
61
|
-
requirement: &
|
51
|
+
requirement: &70110875337880 !ruby/object:Gem::Requirement
|
62
52
|
none: false
|
63
53
|
requirements:
|
64
|
-
- -
|
54
|
+
- - ~>
|
65
55
|
- !ruby/object:Gem::Version
|
66
|
-
version: '
|
56
|
+
version: '3.1'
|
67
57
|
type: :development
|
68
58
|
prerelease: false
|
69
|
-
version_requirements: *
|
59
|
+
version_requirements: *70110875337880
|
70
60
|
description: ! "\n DecentExposure helps you program to an interface, rather than
|
71
61
|
an\n implementation in your Rails controllers. The fact of the matter is that\n
|
72
62
|
\ sharing state via instance variables in controllers promotes close coupling\n
|
@@ -78,14 +68,17 @@ executables: []
|
|
78
68
|
extensions: []
|
79
69
|
extra_rdoc_files: []
|
80
70
|
files:
|
81
|
-
- lib/decent_exposure/
|
82
|
-
- lib/decent_exposure/
|
71
|
+
- lib/decent_exposure/active_record_strategy.rb
|
72
|
+
- lib/decent_exposure/active_record_with_eager_attributes_strategy.rb
|
73
|
+
- lib/decent_exposure/configuration.rb
|
74
|
+
- lib/decent_exposure/expose.rb
|
75
|
+
- lib/decent_exposure/exposure.rb
|
76
|
+
- lib/decent_exposure/inflector.rb
|
77
|
+
- lib/decent_exposure/strategizer.rb
|
78
|
+
- lib/decent_exposure/strategy.rb
|
83
79
|
- lib/decent_exposure/version.rb
|
84
80
|
- lib/decent_exposure.rb
|
85
81
|
- README.md
|
86
|
-
- COPYING
|
87
|
-
- init.rb
|
88
|
-
- rails/init.rb
|
89
82
|
homepage: http://github.com/voxdolo/decent_exposure
|
90
83
|
licenses: []
|
91
84
|
post_install_message:
|
@@ -107,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
100
|
version: 1.3.6
|
108
101
|
requirements: []
|
109
102
|
rubyforge_project:
|
110
|
-
rubygems_version: 1.8.
|
103
|
+
rubygems_version: 1.8.10
|
111
104
|
signing_key:
|
112
105
|
specification_version: 3
|
113
106
|
summary: A helper for creating declarative interfaces in controllers
|
data/COPYING
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
2
|
-
Version 2, December 2004
|
3
|
-
|
4
|
-
Everyone is permitted to copy and distribute verbatim or modified
|
5
|
-
copies of this license document, and changing it is allowed as long
|
6
|
-
as the name is changed.
|
7
|
-
|
8
|
-
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
9
|
-
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
10
|
-
|
11
|
-
0. You just DO WHAT THE FUCK YOU WANT TO.
|
12
|
-
|
data/init.rb
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
module DecentExposure
|
2
|
-
module DefaultExposure
|
3
|
-
def self.included(klass)
|
4
|
-
klass.extend(DecentExposure)
|
5
|
-
if klass.respond_to?(:class_attribute)
|
6
|
-
klass.class_attribute(:_default_exposure)
|
7
|
-
else
|
8
|
-
klass.superclass_delegating_accessor(:_default_exposure)
|
9
|
-
end
|
10
|
-
klass.default_exposure do |name|
|
11
|
-
collection = name.to_s.pluralize
|
12
|
-
if respond_to?(collection) && collection != name.to_s && send(collection).respond_to?(:scoped)
|
13
|
-
proxy = send(collection)
|
14
|
-
else
|
15
|
-
proxy = name.to_s.classify.constantize
|
16
|
-
end
|
17
|
-
|
18
|
-
if id = params["#{name}_id"] || params[:id]
|
19
|
-
proxy.find(id).tap do |r|
|
20
|
-
r.attributes = params[name] unless request.get?
|
21
|
-
end
|
22
|
-
else
|
23
|
-
proxy.new(params[name])
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
require 'decent_exposure/default_exposure'
|
2
|
-
|
3
|
-
module DecentExposure
|
4
|
-
if defined? Rails::Railtie
|
5
|
-
class Railtie < Rails::Railtie
|
6
|
-
initializer "decent_exposure.extend_action_controller_base" do |app|
|
7
|
-
ActiveSupport.on_load(:action_controller) do
|
8
|
-
DecentExposure::Railtie.insert
|
9
|
-
end
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
class Railtie
|
15
|
-
def self.insert
|
16
|
-
ActionController::Base.send(:include, DecentExposure::DefaultExposure)
|
17
|
-
ActionController::Base.protected_instance_variables.push("@_resources")
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
data/rails/init.rb
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
Kernel.load File.join(File.dirname(__FILE__), '..', 'init.rb')
|