Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 342 lines (269 sloc) 9.258 kb
98fd4b1 @voxdolo Add WIP notice to README and mark unimplemented features
voxdolo authored
1 ## NOTICE OF WORK IN PROGRESS
2
3 This README represents planned functionality. Much of it is implemented. Still
4 more is yet to be implemented. We've endeavored to note where the functionality
cdab520 @voxdolo More tweaks on the NOTIMPLEMENTED tag
voxdolo authored
5 has yet to be implemented by tagging it **#NOTIMPLEMENTED** and indicating
98fd4b1 @voxdolo Add WIP notice to README and mark unimplemented features
voxdolo authored
6 different ways of achieving the same effect where possible.
7
46d8152 @voxdolo Update the README
voxdolo authored
8 ## Mad Decent
9
10 Rails controllers are the sweaty armpit of every rails app. This is due, in
11 large part, to the fact that they expose their instance variables directly to
12 their views. This means that your instance variables are your interface... and
13 that you've broken encapsulation. Instance variables are meant to be private,
14 for Science's sake!
15
16 What `decent_exposure` proposes is that you go from this:
17
18 ```ruby
19 class Controller
20 def new
21 @person = Person.new(params[:person])
22 end
23
24 def create
25 @person = Person.new(params[:person])
26 if @person.save
27 redirect_to(@person)
28 else
29 render :new
30 end
31 end
32
33 def edit
34 @person = Person.find(params[:id])
35 end
36
37 def update
38 @person = Person.find(params[:id])
39 if @person.update_attributes(params[:person])
40 redirect_to(@person)
41 else
42 render :edit
43 end
44 end
45 end
46 ```
47
48 To something like this:
49
50 ```ruby
51 class Controller
52 expose(:person)
53
54 def create
55 if person.save
56 redirect_to(person)
57 else
58 render :new
59 end
60 end
61
62 def update
63 if person.save
64 redirect_to(person)
65 else
66 render :edit
67 end
68 end
69 end
70 ```
71
72 `decent_exposure` makes it easy to define named methods that are made available
73 to your views and which memoize the resultant values. It also tucks away the
74 details of the common fetching, initializing and updating of resources and
75 their parameters.
76
77 That's neat and all, but the real advantage comes when it's time to refactor
78 (because you've encapsulated now). What happens when you need to scope your
79 `Person` resource from a `Company`? Which implementation isolates those changes
80 better? In that particular example, `decent_exposure` goes one step farther and
81 will handle the scoping for you (with a smidge of configuration) while still
82 handling all that repetitive initialization, as we'll see next.
83
84 Even if you decide not to use `decent_exposure`, do yourself a favor and stop
85 using instance variables in your views. Your code will be cleaner and easier to
86 refactor as a result. If you want to learn more about his approach, I've
87 expanded on my thoughts in the article [A Diatribe on Maintaining State][1].
88
f3208c5 @voxdolo Initial README
voxdolo authored
89 ## Environmental Awareness
90
91 Well, no it won't lessen your carbon footprint, but it does take a lot of
ae96952 @voxdolo Typo fix
voxdolo authored
92 cues from what's going on around it...
f3208c5 @voxdolo Initial README
voxdolo authored
93
94 `decent_exposure` will build the requested object in one of a couple of ways
46d8152 @voxdolo Update the README
voxdolo authored
95 depending on what the `params` make available to it. At its simplest, when an
96 `id` is present in the `params` hash, `decent_exposure` will attempt to find a
97 record. In absence of `params[:id]` `decent_exposure` will try to build a new
98 record.
99
100 Once the object has been obtained, it attempts to set the attributes of the
101 resulting object. Thus, a newly minted `person` instance will get any
102 attributes set that've been passed along in `params[:person]`. When you
103 interact with `person` in your create action, just call save on it and handle
104 the valid/invalid branch. Let's revisit our previous example:
105
106 ```ruby
107 class Controller
108 expose(:person)
109
110 def create
111 if person.save
112 redirect_to(person)
113 else
114 render :new
f3208c5 @voxdolo Initial README
voxdolo authored
115 end
46d8152 @voxdolo Update the README
voxdolo authored
116 end
117 end
118 ```
119
120 Behind the scenes, `decent_exposure` has essentially done this:
121
122 ```ruby
123 person.attributes = params[:person]
124 ```
125
126 In Rails, this assignment is actually a merge with the current attributes and
127 it marks attributes as dirty as you would expect. This is why you're simply
128 able to call `save` on the `person` instance in the create action, rather than
129 the typical `update_attributes(params[:person])`.
130
131 **An Aside**
f3208c5 @voxdolo Initial README
voxdolo authored
132
133 Did you notice there's no `new` action? Yeah, that's because we don't need it.
134 More often than not actions that respond to `GET` requests are just setting up
135 state. Since we've declared an interface to our state and made it available to
136 the view (a.k.a. the place where we actually want to access it), we just let
137 Rails do it's magic and render the `new` view, lazily evaluating `person` when
138 we actually need it.
139
46d8152 @voxdolo Update the README
voxdolo authored
140 **A Caveat**
141
142 Rails conveniently responds with a 404 if you get a record not found in the
143 controller. Since we don't find the object until we're in the view in this
144 paradigm, we get an ugly `ActionView::TemplateError` instead. If this is
63514a6 @voxdolo Implement #expose!
voxdolo authored
145 problematic for you, consider using the `expose!` method to circumvent lazy
146 evaluation and eagerly evaluate whilst still in the controller.
f3208c5 @voxdolo Initial README
voxdolo authored
147
148 ## Usage
149
46d8152 @voxdolo Update the README
voxdolo authored
150 In an effort to make the examples below a bit less magical, we'll offer a
151 simplified explanation for how the exposed resource would be queried for
152 (assuming you are using `ActiveRecord`).
f3208c5 @voxdolo Initial README
voxdolo authored
153
46d8152 @voxdolo Update the README
voxdolo authored
154 ### Obtaining an instance of an object:
f3208c5 @voxdolo Initial README
voxdolo authored
155
46d8152 @voxdolo Update the README
voxdolo authored
156 ```ruby
157 expose(:person)
158 ```
159
160 **Query Explanation**
161
162 <table>
163 <tr>
164 <td><code>id</code> present?</td>
165 <td>Query</td>
166 </tr>
167 <tr>
168 <td><code>true</code></td>
169 <td><code>Person.find(params[:id])</code></td>
170 </tr>
171 <tr>
172 <td><code>false</code></td>
173 <td><code>Person.new(params[:person])</code></td>
174 </tr>
175 </table>
176
87083b4 @voxdolo Update README: retrieving collections works
voxdolo authored
177 ### Obtaining a collection of objects
f3208c5 @voxdolo Initial README
voxdolo authored
178
46d8152 @voxdolo Update the README
voxdolo authored
179 ```ruby
180 expose(:people)
181 ```
f3208c5 @voxdolo Initial README
voxdolo authored
182
46d8152 @voxdolo Update the README
voxdolo authored
183 **Query Explanation**
f3208c5 @voxdolo Initial README
voxdolo authored
184
46d8152 @voxdolo Update the README
voxdolo authored
185 <table>
186 <tr>
187 <td>Query</td>
188 </tr>
189 <tr>
190 <td><code>Person.scoped</code></td>
191 </tr>
192 </table>
f3208c5 @voxdolo Initial README
voxdolo authored
193
46d8152 @voxdolo Update the README
voxdolo authored
194 ### Scoping your object queries
f3208c5 @voxdolo Initial README
voxdolo authored
195
196 Want to scope your queries to ensure object hierarchy? `decent_exposure`
197 automatically scopes singular forms of a resource from a plural form where
198 they're defined:
199
46d8152 @voxdolo Update the README
voxdolo authored
200 ```ruby
201 expose(:people)
202 expose(:person)
203 ```
f3208c5 @voxdolo Initial README
voxdolo authored
204
46d8152 @voxdolo Update the README
voxdolo authored
205 **Query Explanation**
206
207 <table>
208 <tr>
209 <td><code>id</code> present?</td>
210 <td>Query</td>
211 </tr>
212 <tr>
213 <td><code>true</code></td>
214 <td><code>Person.scoped.find(params[:id])</code></td>
215 </tr>
216 <tr>
217 <td><code>false</code></td>
218 <td><code>Person.scoped.new(params[:person])</code></td>
219 </tr>
220 </table>
f3208c5 @voxdolo Initial README
voxdolo authored
221
222 How about a more realistic scenario where the object hierarchy specifies
3eec112 @voxdolo Implement :scope option
voxdolo authored
223 something useful, like only finding people in a given company:
f3208c5 @voxdolo Initial README
voxdolo authored
224
46d8152 @voxdolo Update the README
voxdolo authored
225 ```ruby
226 expose(:company)
227 expose(:people, scope: :company)
228 expose(:person)
229 ```
f3208c5 @voxdolo Initial README
voxdolo authored
230
46d8152 @voxdolo Update the README
voxdolo authored
231 **Query Explanation**
232
233 <table>
234 <tr>
235 <td>person <code>id</code> present?</td>
236 <td>Query</td>
237 </tr>
238 <tr>
239 <td><code>true</code></td>
240 <td><code>Company.find(params[:company_id]).people.find(params[:id])</code></td>
241 </tr>
242 <tr>
243 <td><code>false</code></td>
244 <td><code>Company.find(params[:company_id]).people.new(params[:person])</code></td>
245 </tr>
246 </table>
247
248 ### Further configuration
f3208c5 @voxdolo Initial README
voxdolo authored
249
250 `decent_exposure` is a configurable beast. Let's take a look at some of the
251 things you can do:
252
eedcae8 @voxdolo Specifying the model name is, in fact, implemented
voxdolo authored
253 **Specify the model name:**
f3208c5 @voxdolo Initial README
voxdolo authored
254
46d8152 @voxdolo Update the README
voxdolo authored
255 ```ruby
256 expose(:company, model: :enterprisey_company)
257 ```
f3208c5 @voxdolo Initial README
voxdolo authored
258
cdab520 @voxdolo More tweaks on the NOTIMPLEMENTED tag
voxdolo authored
259 **Specify the finder method (**#NOTIMPLEMENTED**, use a custom strategy):**
f3208c5 @voxdolo Initial README
voxdolo authored
260
46d8152 @voxdolo Update the README
voxdolo authored
261 ```ruby
262 expose(:company, finder: :find_by_slug)
263 ```
f3208c5 @voxdolo Initial README
voxdolo authored
264
cdab520 @voxdolo More tweaks on the NOTIMPLEMENTED tag
voxdolo authored
265 **Specify the parameter accessor (**#NOTIMPLEMENTED**, use a custom strategy):**
874aeb1 @spraints Add custom parameter handling to the readme/sketch.
spraints authored
266
46d8152 @voxdolo Update the README
voxdolo authored
267 ```ruby
268 expose(:company, params: :company_params)
269 ```
874aeb1 @spraints Add custom parameter handling to the readme/sketch.
spraints authored
270
f3208c5 @voxdolo Initial README
voxdolo authored
271 ### Getting your hands dirty
272
273 While we try to make things as easy for you as possible, sometimes you just
274 need to go off the beaten path. For those times, `expose` takes a block which
275 it lazily evaluates and returns the result of when called. So for instance:
276
46d8152 @voxdolo Update the README
voxdolo authored
277 ```ruby
278 expose(:environment) { Rails.env }
279 ```
f3208c5 @voxdolo Initial README
voxdolo authored
280
46d8152 @voxdolo Update the README
voxdolo authored
281 This block is evaluated and the memoized result is returned whenever you call
282 `environment`.
f3208c5 @voxdolo Initial README
voxdolo authored
283
d80fc1c @voxdolo Merge jgdavey/nunu
voxdolo authored
284 ### Custom strategies
285
286 For the times when custom behavior is needed for resource finding,
287 `decent_exposure` provides a base class for extending. For example, if
288 scoping a resource from `current_user` is not and option, but you'd like
289 to verify a resource's relationship to the `current_user`, you can use a
290 custom strategy like the following:
291
292 ```ruby
293 class VerifiableStrategy < DecentExposure::Strategy
294 delegate :current_user, :to => :controller
295
296 def resource
297 instance = model.find(params[:id])
298 if current_user != instance.user
299 raise ActiveRecord::RecordNotFound
300 end
301 instance
302 end
303 end
304 ```
305
306 You would then use your custom strategy in your controller:
307
46d8152 @voxdolo Update the README
voxdolo authored
308 ```ruby
309 expose(:post, strategy: VerifiableStrategy)
310 ```
d80fc1c @voxdolo Merge jgdavey/nunu
voxdolo authored
311
312 The API only necessitates you to define `resource`, but provides some
313 common helpers to access common things, such as the `params` hash. For
314 everything else, you can delegate to `controller`, which is the same as
315 `self` in the context of a normal controller action.
316
cdab520 @voxdolo More tweaks on the NOTIMPLEMENTED tag
voxdolo authored
317 ### Customizing your exposures (**#NOTIMPLEMENTED**, use a custom strategy)
f3208c5 @voxdolo Initial README
voxdolo authored
318
319 For most things, you'll be able to pass a few configuration options and get
46d8152 @voxdolo Update the README
voxdolo authored
320 the desired behavior. For changes you want to affect every call to `expose` in
321 a controller or controllers inheriting from it (e.g. `ApplicationController`,
322 if you need to change the behavior for all your controllers), you can define
323 an `exposure` configuration block:
f3208c5 @voxdolo Initial README
voxdolo authored
324
46d8152 @voxdolo Update the README
voxdolo authored
325 ```ruby
326 exposure(:example) do
327 orm :mem_cache
328 model { Thing }
329 finder :find_by_thing
330 scope { model.scoped.further }
331 end
332 ```
333
334 If you only want to use that exposure in one call to `expose`, you can do so
335 like this:
336
337 ```ruby
338 expose(:foo, exposure: :example)
339 ```
f3208c5 @voxdolo Initial README
voxdolo authored
340
46d8152 @voxdolo Update the README
voxdolo authored
341 [1]: http://blog.voxdolo.me/a-diatribe-on-maintaining-state.html
Something went wrong with that request. Please try again.