objectify 0.0.3 → 0.0.5
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/.document +5 -0
- data/.gitignore +47 -2
- data/.rspec +1 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +105 -16
- data/LICENSE.txt +20 -0
- data/README.md +230 -0
- data/Rakefile +14 -7
- data/lib/objectify.rb +1 -26
- data/lib/objectify/config/action.rb +31 -0
- data/lib/objectify/config/context.rb +97 -0
- data/lib/objectify/config/policies.rb +18 -0
- data/lib/objectify/executor.rb +21 -0
- data/lib/objectify/injector.rb +39 -0
- data/lib/objectify/instantiator.rb +14 -0
- data/lib/objectify/instrumentation.rb +12 -0
- data/lib/objectify/logging.rb +22 -0
- data/lib/objectify/policy_chain_executor.rb +27 -0
- data/lib/objectify/rails/application.rb +16 -0
- data/lib/objectify/rails/controller.rb +117 -0
- data/lib/objectify/rails/helpers.rb +14 -0
- data/lib/objectify/rails/log_subscriber.rb +59 -0
- data/lib/objectify/rails/railtie.rb +11 -0
- data/lib/objectify/rails/renderer.rb +47 -0
- data/lib/objectify/rails/routing.rb +97 -0
- data/lib/objectify/resolver.rb +27 -0
- data/lib/objectify/resolver_locator.rb +73 -0
- data/lib/objectify/route.rb +4 -0
- data/lib/objectify/version.rb +1 -1
- data/objectify.gemspec +13 -8
- data/spec/config/action_spec.rb +79 -0
- data/spec/config/context_spec.rb +137 -0
- data/spec/config/policies_spec.rb +23 -0
- data/spec/executor_spec.rb +47 -0
- data/spec/injector_spec.rb +88 -0
- data/spec/instantiator_spec.rb +22 -0
- data/spec/policy_chain_executor_spec.rb +60 -0
- data/spec/rails/renderer_spec.rb +63 -0
- data/spec/rails/routing_spec.rb +171 -0
- data/spec/resolver_locator_spec.rb +109 -0
- data/spec/resolver_spec.rb +28 -0
- data/spec/route_spec.rb +11 -0
- data/spec/spec_helper.rb +13 -5
- metadata +142 -62
- data/spec/objectify_spec.rb +0 -60
data/.document
ADDED
data/.gitignore
CHANGED
@@ -1,3 +1,48 @@
|
|
1
|
+
# rcov generated
|
2
|
+
coverage
|
3
|
+
|
4
|
+
# rdoc generated
|
5
|
+
rdoc
|
6
|
+
|
7
|
+
# yard generated
|
8
|
+
doc
|
9
|
+
.yardoc
|
10
|
+
|
11
|
+
# bundler
|
1
12
|
.bundle
|
2
|
-
|
3
|
-
|
13
|
+
|
14
|
+
# built gems
|
15
|
+
*.gem
|
16
|
+
|
17
|
+
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
18
|
+
#
|
19
|
+
# * Create a file at ~/.gitignore
|
20
|
+
# * Include files you want ignored
|
21
|
+
# * Run: git config --global core.excludesfile ~/.gitignore
|
22
|
+
#
|
23
|
+
# After doing this, these files will be ignored in all your git projects,
|
24
|
+
# saving you from having to 'pollute' every project you touch with them
|
25
|
+
#
|
26
|
+
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
|
27
|
+
#
|
28
|
+
# For MacOS:
|
29
|
+
#
|
30
|
+
#.DS_Store
|
31
|
+
|
32
|
+
# For TextMate
|
33
|
+
#*.tmproj
|
34
|
+
#tmtags
|
35
|
+
|
36
|
+
# For emacs:
|
37
|
+
#*~
|
38
|
+
#\#*
|
39
|
+
#.\#*
|
40
|
+
|
41
|
+
# For vim:
|
42
|
+
#*.swp
|
43
|
+
|
44
|
+
# For redcar:
|
45
|
+
#.redcar
|
46
|
+
|
47
|
+
# For rubinius:
|
48
|
+
#*.rbc
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,28 +1,117 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
objectify (0.0.
|
5
|
-
|
4
|
+
bitlove-objectify (0.0.1)
|
5
|
+
i18n
|
6
|
+
rails (>= 3.0.0)
|
6
7
|
|
7
8
|
GEM
|
8
|
-
remote:
|
9
|
+
remote: https://rubygems.org/
|
9
10
|
specs:
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
11
|
+
actionmailer (3.1.1)
|
12
|
+
actionpack (= 3.1.1)
|
13
|
+
mail (~> 2.3.0)
|
14
|
+
actionpack (3.1.1)
|
15
|
+
activemodel (= 3.1.1)
|
16
|
+
activesupport (= 3.1.1)
|
17
|
+
builder (~> 3.0.0)
|
18
|
+
erubis (~> 2.7.0)
|
19
|
+
i18n (~> 0.6)
|
20
|
+
rack (~> 1.3.2)
|
21
|
+
rack-cache (~> 1.1)
|
22
|
+
rack-mount (~> 0.8.2)
|
23
|
+
rack-test (~> 0.6.1)
|
24
|
+
sprockets (~> 2.0.2)
|
25
|
+
activemodel (3.1.1)
|
26
|
+
activesupport (= 3.1.1)
|
27
|
+
builder (~> 3.0.0)
|
28
|
+
i18n (~> 0.6)
|
29
|
+
activerecord (3.1.1)
|
30
|
+
activemodel (= 3.1.1)
|
31
|
+
activesupport (= 3.1.1)
|
32
|
+
arel (~> 2.2.1)
|
33
|
+
tzinfo (~> 0.3.29)
|
34
|
+
activeresource (3.1.1)
|
35
|
+
activemodel (= 3.1.1)
|
36
|
+
activesupport (= 3.1.1)
|
37
|
+
activesupport (3.1.1)
|
38
|
+
multi_json (~> 1.0)
|
39
|
+
arel (2.2.3)
|
40
|
+
bourne (1.0)
|
41
|
+
mocha (= 0.9.8)
|
42
|
+
builder (3.0.0)
|
43
|
+
diff-lcs (1.1.3)
|
44
|
+
erubis (2.7.0)
|
45
|
+
git (1.2.5)
|
46
|
+
hike (1.2.1)
|
47
|
+
i18n (0.6.0)
|
48
|
+
jeweler (1.6.4)
|
49
|
+
bundler (~> 1.0)
|
50
|
+
git (>= 1.2.5)
|
51
|
+
rake
|
52
|
+
json (1.7.3)
|
53
|
+
mail (2.3.3)
|
54
|
+
i18n (>= 0.4.0)
|
55
|
+
mime-types (~> 1.16)
|
56
|
+
treetop (~> 1.4.8)
|
57
|
+
mime-types (1.18)
|
58
|
+
mocha (0.9.8)
|
59
|
+
rake
|
60
|
+
multi_json (1.3.5)
|
61
|
+
polyglot (0.3.3)
|
62
|
+
rack (1.3.6)
|
63
|
+
rack-cache (1.2)
|
64
|
+
rack (>= 0.4)
|
65
|
+
rack-mount (0.8.3)
|
66
|
+
rack (>= 1.0.0)
|
67
|
+
rack-ssl (1.3.2)
|
68
|
+
rack
|
69
|
+
rack-test (0.6.1)
|
70
|
+
rack (>= 1.0)
|
71
|
+
rails (3.1.1)
|
72
|
+
actionmailer (= 3.1.1)
|
73
|
+
actionpack (= 3.1.1)
|
74
|
+
activerecord (= 3.1.1)
|
75
|
+
activeresource (= 3.1.1)
|
76
|
+
activesupport (= 3.1.1)
|
77
|
+
bundler (~> 1.0)
|
78
|
+
railties (= 3.1.1)
|
79
|
+
railties (3.1.1)
|
80
|
+
actionpack (= 3.1.1)
|
81
|
+
activesupport (= 3.1.1)
|
82
|
+
rack-ssl (~> 1.3.2)
|
83
|
+
rake (>= 0.8.7)
|
84
|
+
rdoc (~> 3.4)
|
85
|
+
thor (~> 0.14.6)
|
86
|
+
rake (0.9.2.2)
|
87
|
+
rdoc (3.12)
|
88
|
+
json (~> 1.4)
|
89
|
+
rspec (2.4.0)
|
90
|
+
rspec-core (~> 2.4.0)
|
91
|
+
rspec-expectations (~> 2.4.0)
|
92
|
+
rspec-mocks (~> 2.4.0)
|
93
|
+
rspec-core (2.4.0)
|
94
|
+
rspec-expectations (2.4.0)
|
19
95
|
diff-lcs (~> 1.1.2)
|
20
|
-
rspec-mocks (2.
|
96
|
+
rspec-mocks (2.4.0)
|
97
|
+
sprockets (2.0.4)
|
98
|
+
hike (~> 1.2)
|
99
|
+
rack (~> 1.0)
|
100
|
+
tilt (!= 1.3.0, ~> 1.1)
|
101
|
+
thor (0.14.6)
|
102
|
+
tilt (1.3.3)
|
103
|
+
treetop (1.4.10)
|
104
|
+
polyglot
|
105
|
+
polyglot (>= 0.3.1)
|
106
|
+
tzinfo (0.3.33)
|
21
107
|
|
22
108
|
PLATFORMS
|
23
109
|
ruby
|
24
110
|
|
25
111
|
DEPENDENCIES
|
26
|
-
objectify!
|
27
|
-
|
28
|
-
|
112
|
+
bitlove-objectify!
|
113
|
+
bourne (= 1.0)
|
114
|
+
bundler (~> 1.0.0)
|
115
|
+
jeweler (~> 1.6.4)
|
116
|
+
mocha (= 0.9.8)
|
117
|
+
rspec (~> 2.4.0)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 James Golick, BitLove Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
# objectify
|
2
|
+
|
3
|
+
Objectify is a framework that codifies good object oriented design practices for building maintainable rails applications.
|
4
|
+
|
5
|
+
## How it works
|
6
|
+
|
7
|
+
Objectify has two primary components:
|
8
|
+
|
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 for far more testable code, and facilitates better reuse of components.
|
10
|
+
|
11
|
+
The flow of an objectify request is as follows:
|
12
|
+
|
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
|
+
|
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.
|
24
|
+
|
25
|
+
An example Policy:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class RequiresLoginPolicy
|
29
|
+
# more on how current user gets injected below
|
30
|
+
def allowed?(current_user)
|
31
|
+
!current_user.nil?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
A responder, in case that policy fails.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class UnauthenticatedResponder
|
40
|
+
# yes, at some point we probably need a better interface
|
41
|
+
# for handling responses, but this'll do for now.
|
42
|
+
def call(controller, renderer)
|
43
|
+
renderer.redirect_to controller.login_url
|
44
|
+
end
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
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.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# config/routes.rb
|
52
|
+
MyApp::Application.routes.draw do
|
53
|
+
objectify.defaults :policies => :requires_login
|
54
|
+
objectify.policy_responders :requires_login => :unauthenticated
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
2. If all the policies succeed, the service for that action is executed. A service is typically responsible for fetching and / or manipulating data.
|
59
|
+
|
60
|
+
A very simple example of a service:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class PicturesCreateService
|
64
|
+
# the current_user and the request's params will be automatically injected here.
|
65
|
+
def call(current_user, params)
|
66
|
+
current_user.pictures.create params[:picture]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
3. Finally, the responder is executed. Following with our Pictures#create example:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
class PicturesCreateResponder
|
75
|
+
# service_result is exactly what it sounds like
|
76
|
+
def call(service_result, controller, renderer)
|
77
|
+
if service_result.persisted?
|
78
|
+
renderer.redirect_to service_result
|
79
|
+
else
|
80
|
+
# this is the only way that you can pass data to the view layer
|
81
|
+
# and you can only pass one thing. Hint: use a presenter.
|
82
|
+
renderer.data(service_result)
|
83
|
+
renderer.render :template => "pictures/edit.html.erb"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
2. A dependency injection framework. Objectify automatically injects dependencies in to 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 by implementing Resolver classes. More on that below.
|
90
|
+
|
91
|
+
|
92
|
+
## What if I have a bunch of existing rails code?
|
93
|
+
|
94
|
+
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.
|
95
|
+
|
96
|
+
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.
|
97
|
+
|
98
|
+
Here's how to run the policy chain in your ApplicationController - it'll figure out which policies to run itself:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class ApplicationController < ActionController::Base
|
102
|
+
include Objectify::Rails::ControllerHelpers
|
103
|
+
|
104
|
+
around_filter :objectify_around_filter
|
105
|
+
before_filter :execute_policy_chain
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
And to configure policies for a legacy action:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
# config/routes.rb
|
113
|
+
MyApp::Application.routes.draw do
|
114
|
+
objectify.defaults :policies => :requires_login
|
115
|
+
objectify.policy_responders :requires_login => :unauthenticated
|
116
|
+
objectify.legacy_action :controller, :action, :policies => [:x, :y, :z],
|
117
|
+
:skip_policies => [:requires_login]
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
Then, you need to create an ObjectifyController that inherits from ApplicationController, and configure objectify to use that:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
# app/controllers/objectify_controller.rb
|
125
|
+
class ObjectifyController < ApplicationController
|
126
|
+
include Objectify::Rails::LegacyControllerBehaviour
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
# config/application.rb
|
132
|
+
module MyApp
|
133
|
+
class Application < Rails::Application
|
134
|
+
# ...snip...
|
135
|
+
objectify.objectify_controller = "objectify"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
|
141
|
+
## Custom Injections
|
142
|
+
|
143
|
+
Several of the above methods have parameters named 'current_user'. By default, objectify won't know how to inject a parameter by that name, so it'll raise an error when it encounters one. Here's how to create a custom resolver for it that'll automatically get found by name.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
# app/resolvers/current_user_resolver.rb
|
147
|
+
class CurrentUserResolver
|
148
|
+
def initialize(user_finder = User)
|
149
|
+
@user_finder = user_finder
|
150
|
+
end
|
151
|
+
|
152
|
+
# note that resolvers themselves get injected
|
153
|
+
def call(session)
|
154
|
+
@user_finder.find_by_id(session[:current_user_id])
|
155
|
+
end
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
### Why did you constructor-inject the User constant in to the CurrentUserResolver?
|
160
|
+
|
161
|
+
Because that makes it possible to test in isolation.
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
describe "CurrentUserResolver" do
|
165
|
+
before do
|
166
|
+
@user = stub("User")
|
167
|
+
@user_finder = stub("UserFinder", :find_by_id => nil)
|
168
|
+
@user_finder.stubs(:find_by_id).with(10).returns(@user)
|
169
|
+
|
170
|
+
@resolver = CurrentUserResolver.new(@user_finder)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "returns whatever the finder returns" do
|
174
|
+
@resolver.call({:current_user_id => 42}).should be_nil
|
175
|
+
@resolver.call({:current_user_id => 10}).should == @user
|
176
|
+
end
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
## Views
|
181
|
+
|
182
|
+
Objectify has two major impacts on your views.
|
183
|
+
|
184
|
+
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.
|
185
|
+
|
186
|
+
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.
|
187
|
+
|
188
|
+
## Installation
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
# Gemfile
|
192
|
+
gem "objectify", "> 0"
|
193
|
+
|
194
|
+
# config/application.rb
|
195
|
+
module MyApp
|
196
|
+
class Application < Rails::Application
|
197
|
+
require "objectify/rails/application"
|
198
|
+
require "objectify/rails/controller"
|
199
|
+
# only have to require this if you want objectify logging
|
200
|
+
require "objectify/rails/log_subscriber"
|
201
|
+
include Objectify::Rails::Application
|
202
|
+
end
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
## Issues
|
207
|
+
|
208
|
+
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:
|
209
|
+
|
210
|
+
* Support for all the kinds of routing that rails does.
|
211
|
+
* Caching of policy results per-request, so we don't have to run them twice if they're used in views.
|
212
|
+
* Smarter injection strategies, and possibly caching.
|
213
|
+
* ???
|
214
|
+
|
215
|
+
## Credits
|
216
|
+
|
217
|
+
* Author: James Golick @jamesgolick
|
218
|
+
* Advice (and the idea for injections based on method parameter names): Gary Bernhardt @garybernhardt
|
219
|
+
* Feedback: Jake Douglas @jakedouglas
|
220
|
+
* Feedback: Julie Haché @juliehache
|
221
|
+
* The gem name: Andrew Kiellor @akiellor
|
222
|
+
|
223
|
+
## The other objectify gem
|
224
|
+
|
225
|
+
If you were looking for the gem that *used* to be called objectify on rubygems.org, it's here: https://github.com/akiellor/objectify
|
226
|
+
|
227
|
+
## Copyright
|
228
|
+
|
229
|
+
Copyright (c) 2012 James Golick, BitLove Inc. See LICENSE.txt for
|
230
|
+
further details.
|