Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 334 lines (246 sloc) 11.905 kb
336c8ab @brynary Add Code Climate badge
brynary authored
1 # objectify [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/bitlove/objectify)
4746081 @jamesgolick Initial commit to objectify.
jamesgolick authored
2
061c1b0 @jamesgolick add blog post to README
jamesgolick authored
3 Objectify is a framework that codifies good object oriented design practices for building maintainable rails applications. For more on the motivations that led to objectify, check out this blog post: http://jamesgolick.com/2012/5/22/objectify-a-better-way-to-build-rails-applications.html
c59bd2a @jamesgolick start documenting. this is painful.
jamesgolick authored
4
2decf43 @jamesgolick more documenting
jamesgolick authored
5 ## How it works
c59bd2a @jamesgolick start documenting. this is painful.
jamesgolick authored
6
7 Objectify has two primary components:
8
fa91e2f @gilesbowkett text and formatting tweaks
gilesbowkett authored
9 1. A request execution framework that separates the responsibilities that are typically jammed together in rails controller actions in to 3 types of components: Policies, Services, and Responders. Properly separating and assigning these responsibilities makes code far more testable, and facilitates better reuse of components.
c59bd2a @jamesgolick start documenting. this is painful.
jamesgolick authored
10
71118ee @jamesgolick spacing
jamesgolick authored
11 The flow of an objectify request is as follows:
c59bd2a @jamesgolick start documenting. this is painful.
jamesgolick authored
12
bb5b1bd @jamesgolick note about routing
jamesgolick authored
13 0. Objectify actions are configured in the routes file:
14
15 ```ruby
16 # config/routes.rb
17 # ... snip ...
18 objectify.resources :pictures
19 ```
20
21 Objectify currently only supports resourceful actions, but that's just a temporary thing.
22
fa91e2f @gilesbowkett text and formatting tweaks
gilesbowkett authored
23 1. The policy chain is resolved (based on the various levels of configuration) and executed. Objectify calls the `#allowed?(...)` method on each policy in the chain. If one of the policies fails, the chain short-circuits at that point, and objectify executes the configured responder for that policy.
c59bd2a @jamesgolick start documenting. this is painful.
jamesgolick authored
24
a95d00f @jamesgolick more indentation shit
jamesgolick authored
25 An example Policy:
c59bd2a @jamesgolick start documenting. this is painful.
jamesgolick authored
26
a95d00f @jamesgolick more indentation shit
jamesgolick authored
27 ```ruby
28 class RequiresLoginPolicy
29 # more on how current user gets injected below
336c8ab @brynary Add Code Climate badge
brynary authored
30 def allowed?(current_user)
a95d00f @jamesgolick more indentation shit
jamesgolick authored
31 !current_user.nil?
32 end
71118ee @jamesgolick spacing
jamesgolick authored
33 end
a95d00f @jamesgolick more indentation shit
jamesgolick authored
34 ```
c59bd2a @jamesgolick start documenting. this is painful.
jamesgolick authored
35
a95d00f @jamesgolick more indentation shit
jamesgolick authored
36 A responder, in case that policy fails.
c59bd2a @jamesgolick start documenting. this is painful.
jamesgolick authored
37
a95d00f @jamesgolick more indentation shit
jamesgolick authored
38 ```ruby
39 class UnauthenticatedResponder
7249a4e @jamesgolick update the docs
jamesgolick authored
40 def call(format, routes)
41 format.any { redirect_to routes.login_url }
a95d00f @jamesgolick more indentation shit
jamesgolick authored
42 end
71118ee @jamesgolick spacing
jamesgolick authored
43 end
a95d00f @jamesgolick more indentation shit
jamesgolick authored
44 ```
c59bd2a @jamesgolick start documenting. this is painful.
jamesgolick authored
45
a95d00f @jamesgolick more indentation shit
jamesgolick authored
46 Here's how you setup the RequiresLoginPolicy to run by default (you can configure specific actions to ignore it), and connect the policy with its responder.
c59bd2a @jamesgolick start documenting. this is painful.
jamesgolick authored
47
a95d00f @jamesgolick more indentation shit
jamesgolick authored
48 ```ruby
49 # config/routes.rb
50 MyApp::Application.routes.draw do
51 objectify.defaults :policies => :requires_login
52 objectify.policy_responders :requires_login => :unauthenticated
53 end
54 ```
4746081 @jamesgolick Initial commit to objectify.
jamesgolick authored
55
71118ee @jamesgolick spacing
jamesgolick authored
56 2. If all the policies succeed, the service for that action is executed. A service is typically responsible for fetching and / or manipulating data.
2decf43 @jamesgolick more documenting
jamesgolick authored
57
a95d00f @jamesgolick more indentation shit
jamesgolick authored
58 A very simple example of a service:
6d75162 @jamesgolick formatting
jamesgolick authored
59
a95d00f @jamesgolick more indentation shit
jamesgolick authored
60 ```ruby
61 class PicturesCreateService
62 # the current_user and the request's params will be automatically injected here.
63 def call(current_user, params)
64 current_user.pictures.create params[:picture]
65 end
71118ee @jamesgolick spacing
jamesgolick authored
66 end
a95d00f @jamesgolick more indentation shit
jamesgolick authored
67 ```
71118ee @jamesgolick spacing
jamesgolick authored
68
fa91e2f @gilesbowkett text and formatting tweaks
gilesbowkett authored
69 3. Finally, the responder is executed. Following with our `Pictures#create` example:
71118ee @jamesgolick spacing
jamesgolick authored
70
a95d00f @jamesgolick more indentation shit
jamesgolick authored
71 ```ruby
72 class PicturesCreateResponder
73 # service_result is exactly what it sounds like
7249a4e @jamesgolick update the docs
jamesgolick authored
74 def call(service_result, format)
a95d00f @jamesgolick more indentation shit
jamesgolick authored
75 if service_result.persisted?
7249a4e @jamesgolick update the docs
jamesgolick authored
76 format.any { redirect_to service_result }
a95d00f @jamesgolick more indentation shit
jamesgolick authored
77 else
7249a4e @jamesgolick update the docs
jamesgolick authored
78 # the service_result is always the only thing passed to the view
79 # (hint: use a presenter)
80 # you can access it with the `objectify_data` helper.
81 format.any { render :template => "pictures/new.html.erb" }
a95d00f @jamesgolick more indentation shit
jamesgolick authored
82 end
71118ee @jamesgolick spacing
jamesgolick authored
83 end
28dee24 @jamesgolick more formattingggg
jamesgolick authored
84 end
a95d00f @jamesgolick more indentation shit
jamesgolick authored
85 ```
2decf43 @jamesgolick more documenting
jamesgolick authored
86
8b299ff @jamesgolick README stuff about the new injector configs
jamesgolick authored
87 2. A dependency injection framework. Objectify automatically injects dependencies into objects it manages based on parameter names. So, if you have a service method signature like `PictureCreationService#call(params)`, objectify will automatically inject the request's params when it calls that method. It's very simple to create custom injections. More on that below.
a49a9d4 @jamesgolick reorder that stuff
jamesgolick authored
88
89
3b5e91b @jamesgolick legacy mode explanation
jamesgolick authored
90 ## What if I have a bunch of existing rails code?
91
fa91e2f @gilesbowkett text and formatting tweaks
gilesbowkett authored
92 Objectify has a legacy mode that allows you to execute the policy chain as a `before_filter` in your ApplicationController. You can also configure policies (and `skip_policies`) for your "legacy" actions. That way, access control code is shared between the legacy and objectified components of your application.
3b5e91b @jamesgolick legacy mode explanation
jamesgolick authored
93
93e1ea4 @jamesgolick haven't released that yet
jamesgolick authored
94 I completely rewrote our legacy authentication system as a set of objectify policies, resolvers, and services - I'm gonna package that up and release it soon.
3b5e91b @jamesgolick legacy mode explanation
jamesgolick authored
95
96 Here's how to run the policy chain in your ApplicationController - it'll figure out which policies to run itself:
97
98 ```ruby
99 class ApplicationController < ActionController::Base
100 include Objectify::Rails::ControllerHelpers
101
102 around_filter :objectify_around_filter
103 before_filter :execute_policy_chain
104 end
105 ```
106
107 And to configure policies for a legacy action:
108
109 ```ruby
110 # config/routes.rb
111 MyApp::Application.routes.draw do
112 objectify.defaults :policies => :requires_login
113 objectify.policy_responders :requires_login => :unauthenticated
114 objectify.legacy_action :controller, :action, :policies => [:x, :y, :z],
115 :skip_policies => [:requires_login]
116 end
117 ```
118
119 Then, you need to create an ObjectifyController that inherits from ApplicationController, and configure objectify to use that:
120
121 ```ruby
544f394 @jamesgolick that wasn't working anyway
jamesgolick authored
122 # app/controllers/objectify_controller.rb
3b5e91b @jamesgolick legacy mode explanation
jamesgolick authored
123 class ObjectifyController < ApplicationController
124 include Objectify::Rails::LegacyControllerBehaviour
125 end
126 ```
127
128 ```ruby
129 # config/application.rb
130 module MyApp
131 class Application < Rails::Application
132 # ...snip...
133 objectify.objectify_controller = "objectify"
134 end
135 end
136 ```
137
f5ffa6b @jamesgolick reorder
jamesgolick authored
138
139 ## Custom Injections
140
8b299ff @jamesgolick README stuff about the new injector configs
jamesgolick authored
141 There are a few ways to customize what gets injected when. By default, when objectify sees a parameter called `something`, it'll first look to see if something is specifically configured for that name, then it'll attempt to satisfy it by calling `Something.new`. If that doesn't exist, it'll try `SomethingResolver.new`, which it'll then call `#call` on. If that doesn't exist, it'll raise an error.
142
143 You can configure the injector in 3 ways. The first is used to specify an implemenation.
144
145 Let's say you had a PictureCreationService whose constructor took a parameter called `storage`.
146
147 ```ruby
148 class PictureCreationService
149 def initialize(storage)
150 @storage = storage
151 end
152
153 # ... more code ...
154 end
155 ```
156
157 You can tell the injector what to supply for that parameter like this:
158
159 ```ruby
160 objectify.implementations :storage => :s3_storage
161 ```
162
163 Another option is to specify a value. For example, you might have a service class with a page_size parameter.
164
165 ```ruby
166 class PicturesIndexService
167 def initialize(page_size)
168 @page_size = page_size
169 end
170
171 # ... more code ...
172 end
173 ```
174
175 You can tell the injector what size to make the pages like this:
176
177 ```ruby
178 objectify.values :page_size => 20
179 ```
180
181 Finally, you can tell objectify about `resolvers`. Resolvers are objects that know how to fulfill parameters. For example, several of the above methods have parameters named `current_user`. Here's how to create a custom resolver for it that'll automatically get found by name.
f5ffa6b @jamesgolick reorder
jamesgolick authored
182
183 ```ruby
184 # app/resolvers/current_user_resolver.rb
185 class CurrentUserResolver
186 def initialize(user_finder = User)
187 @user_finder = user_finder
188 end
189
190 # note that resolvers themselves get injected
191 def call(session)
192 @user_finder.find_by_id(session[:current_user_id])
193 end
194 end
195 ```
196
8b299ff @jamesgolick README stuff about the new injector configs
jamesgolick authored
197 If you wanted to explicitly configure that resolver, you'd do it like this:
198
199 ```ruby
200 objectify.resolvers :current_user => :current_user
201 ```
202
203 If that resolver was in the namespace ObjectifyAuth, you'd configure it like this:
204
205 ```ruby
206 objectify.resolvers :current_user => "objectify_auth/current_user"
207 ```
208
f5ffa6b @jamesgolick reorder
jamesgolick authored
209 ### Why did you constructor-inject the User constant in to the CurrentUserResolver?
336c8ab @brynary Add Code Climate badge
brynary authored
210
f5ffa6b @jamesgolick reorder
jamesgolick authored
211 Because that makes it possible to test in isolation.
212
213 ```ruby
214 describe "CurrentUserResolver" do
215 before do
216 @user = stub("User")
217 @user_finder = stub("UserFinder", :find_by_id => nil)
218 @user_finder.stubs(:find_by_id).with(10).returns(@user)
219
220 @resolver = CurrentUserResolver.new(@user_finder)
221 end
222
223 it "returns whatever the finder returns" do
224 @resolver.call({:current_user_id => 42}).should be_nil
225 @resolver.call({:current_user_id => 10}).should == @user
226 end
227 end
228 ```
229
eec8704 @jamesgolick add some documentation about decorators
jamesgolick authored
230 ### Decorators
231
232 Decorators are a great way to create truly modular and composable software. Here's a great example.
233
234 In objectify\_auth, there's a SessionsCreateService that you can use as the basis for the creat action in your /sessions resource. By default, it does very little:
235
236 ```ruby
237 class SessionsCreateService
238 def initialize(authenticator, session_creator)
239 @authenticator = authenticator
240 @session_creator = session_creator
241 end
242
243 def call(params, session)
244 @authenticator.call(params[:email], params[:password]).tap do |user|
245 @session_creator.call(session) if user
246 end
247 end
248 end
249 ```
250
251 Let's say we wanted to add remember token issuance. We could rewrite the entire SessionsCreationService or extend (with inheritance) it to do that, but then we'd have to retest the whole unit again. A decorator allows us to avoid that:
252
253 ```ruby
254 class SessionsCreateServiceWithRememberToken
255 def initialize(sessions_create_service, remember_token_generator)
256 @sessions_create_service = sessions_create_service
257 @remember_token_generator = remember_token_generator
258 end
259
260 def call(params, session, cookies)
261 @sessions_create_service.call(params, session).tap do |user|
262 if user
263 token = @remember_token_generator.call(user)
264 cookies[:remember_token] = { ... }
265 end
266 end
267 end
268 end
269 ```
270
271 This makes for a very simple and easy to test extension to our SessionsCreateService. We can tell objectify to use this decorator like this:
272
273 ```ruby
274 # config/routes.rb
275 objectify.decorators :sessions_create_service => :sessions_create_service_with_remember_token
276 ```
277
278 If we wanted to specify additional decorators, it'd look like this:
279
280 ```ruby
281 # config/routes.rb
282 objectify.decorators :sessions_create_service => [:sessions_create_service_with_remember_token, :sessions_create_service_with_captcha_verification]
283 ```
284
1277250 @jamesgolick some notes about views
jamesgolick authored
285 ## Views
286
287 Objectify has two major impacts on your views.
288
fa91e2f @gilesbowkett text and formatting tweaks
gilesbowkett authored
289 1. You can only pass one variable from an objectified action to the controller. You do that by calling `renderer.data(the_object_you_want_to_pass)`. Then, you call `objectify_data` in the view to fetch the data. If it's not there, it'll raise an error. Use a presenter or some kind of other struct object to pass multiple objects to your views.
1277250 @jamesgolick some notes about views
jamesgolick authored
290
fa91e2f @gilesbowkett text and formatting tweaks
gilesbowkett authored
291 2. You can reuse your policies in your views. `require "objectify/rails/helpers"` and add `Objectify::Rails::Helpers` to your helpers list, and you'll get a helper called `#policy_allowed?(policy_name)`. Yay code reuse.
1277250 @jamesgolick some notes about views
jamesgolick authored
292
1aa9e80 @jamesgolick installation notes
jamesgolick authored
293 ## Installation
294
295 ```ruby
296 # Gemfile
297 gem "objectify", "> 0"
298
299 # config/application.rb
300 module MyApp
301 class Application < Rails::Application
302 # only have to require this if you want objectify logging
336c8ab @brynary Add Code Climate badge
brynary authored
303 require "objectify/rails/log_subscriber"
1aa9e80 @jamesgolick installation notes
jamesgolick authored
304 include Objectify::Rails::Application
305 end
306 end
307 ```
308
46f5021 @jamesgolick some notes about work that's outstanding
jamesgolick authored
309 ## Issues
310
311 We're using this thing in production to serve millions of requests every day. However, it's far from being complete. Here are some of the problems that still need solving:
312
313 * Support for all the kinds of routing that rails does.
314 * Caching of policy results per-request, so we don't have to run them twice if they're used in views.
315 * Smarter injection strategies, and possibly caching.
316 * ???
317
3e39603 @jamesgolick some credits
jamesgolick authored
318 ## Credits
319
320 * Author: James Golick @jamesgolick
321 * Advice (and the idea for injections based on method parameter names): Gary Bernhardt @garybernhardt
322 * Feedback: Jake Douglas @jakedouglas
323 * Feedback: Julie Haché @juliehache
324 * The gem name: Andrew Kiellor @akiellor
325
326 ## The other objectify gem
327
328 If you were looking for the gem that *used* to be called objectify on rubygems.org, it's here: https://github.com/akiellor/objectify
329
2decf43 @jamesgolick more documenting
jamesgolick authored
330 ## Copyright
4746081 @jamesgolick Initial commit to objectify.
jamesgolick authored
331
413f262 @jamesgolick BitLove
jamesgolick authored
332 Copyright (c) 2012 James Golick, BitLove Inc. See LICENSE.txt for
4746081 @jamesgolick Initial commit to objectify.
jamesgolick authored
333 further details.
Something went wrong with that request. Please try again.