responder_controller 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +4 -0
- data/README.rdoc +14 -0
- data/VERSION +1 -1
- data/lib/responder_controller.rb +55 -10
- data/responder_controller.gemspec +2 -2
- data/spec/responder_controller_spec.rb +70 -0
- metadata +4 -4
data/CHANGELOG
CHANGED
data/README.rdoc
CHANGED
@@ -40,6 +40,20 @@ This is me abstracting more for my own apps. If it's handy for you, go nuts.
|
|
40
40
|
serves_model :user
|
41
41
|
end
|
42
42
|
|
43
|
+
=== Forbid a certain scope
|
44
|
+
|
45
|
+
class PostsController < ApplicationController
|
46
|
+
include ResponderController
|
47
|
+
serves_scopes :except => :authored_by # asking for it will 403
|
48
|
+
end
|
49
|
+
|
50
|
+
=== Or use a white list instead
|
51
|
+
|
52
|
+
class PostsController < ApplicationController
|
53
|
+
include ResponderController
|
54
|
+
serves_scopes :only => :recent
|
55
|
+
end
|
56
|
+
|
43
57
|
=== Serve resources in a namespace:
|
44
58
|
|
45
59
|
class PostsController < ApplicationController
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/responder_controller.rb
CHANGED
@@ -3,7 +3,20 @@ require 'active_support/core_ext/string/inflections'
|
|
3
3
|
require 'active_support/core_ext/module/delegation'
|
4
4
|
|
5
5
|
module ResponderController
|
6
|
-
|
6
|
+
# Root of scope-related exceptions.
|
7
|
+
class ScopeError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
# Raised when an active record scope itself raises an exception.
|
11
|
+
#
|
12
|
+
# If this exception bubbles up, rails will render it as a 400.
|
13
|
+
class BadScope < ScopeError
|
14
|
+
end
|
15
|
+
|
16
|
+
# Raised when attempting to call a scope forbidden by ClassMethods.serves_scopes.
|
17
|
+
#
|
18
|
+
# If this exception bubbles up, rails will render it as a 403.
|
19
|
+
class ForbiddenScope < ScopeError
|
7
20
|
end
|
8
21
|
|
9
22
|
def self.included(mod)
|
@@ -11,10 +24,11 @@ module ResponderController
|
|
11
24
|
mod.send :include, InstanceMethods
|
12
25
|
mod.send :include, Actions
|
13
26
|
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
27
|
+
# Declare http statuses to return for uncaught scope errors.
|
28
|
+
ActionDispatch::ShowExceptions.rescue_responses.update({
|
29
|
+
BadScope.name => :bad_request,
|
30
|
+
ForbiddenScope.name => :forbidden
|
31
|
+
}) if defined? ActionDispatch
|
18
32
|
end
|
19
33
|
|
20
34
|
# Configure how the controller finds and serves models of what flavor.
|
@@ -39,6 +53,33 @@ module ResponderController
|
|
39
53
|
@model_class_name = model_class_name.to_s
|
40
54
|
end
|
41
55
|
|
56
|
+
# Declare what active record scopes to allow or forbid to requests.
|
57
|
+
#
|
58
|
+
# .serves_scopes follows the regular :only/:except form: white-listed scopes are passed by
|
59
|
+
# name as <tt>:only => [:allowed, :scopes]</tt> or <tt>:only => :just_one</tt>. Similarly,
|
60
|
+
# black-listed ones are passed under <tt>:except</tt>.
|
61
|
+
#
|
62
|
+
# If a white-list is passed, all other requested scopes (i.e. scopes named by query parameters)
|
63
|
+
# will be denied, raising <tt>ForbiddenScope</tt>. If a black-list is passed, only they will
|
64
|
+
# raise the exception.
|
65
|
+
def serves_scopes(options = nil)
|
66
|
+
@serves_scopes ||= {}
|
67
|
+
|
68
|
+
if options
|
69
|
+
raise TypeError unless options.is_a? Hash
|
70
|
+
|
71
|
+
new_keys = @serves_scopes.keys | options.keys
|
72
|
+
unless new_keys == [:only] or new_keys == [:except]
|
73
|
+
raise ArgumentError.new("serves_scopes takes exactly one of :only and :except")
|
74
|
+
end
|
75
|
+
|
76
|
+
@serves_scopes[options.keys.first] ||= []
|
77
|
+
@serves_scopes[options.keys.first].concat [*options.values.first]
|
78
|
+
end
|
79
|
+
|
80
|
+
@serves_scopes
|
81
|
+
end
|
82
|
+
|
42
83
|
# Declare leading arguments ("responder context") for +respond_with+ calls.
|
43
84
|
#
|
44
85
|
# +respond_with+ creates urls from models. To avoid strongly coupling models to a url
|
@@ -102,7 +143,7 @@ module ResponderController
|
|
102
143
|
|
103
144
|
parent_name_parts = parent_model_class_name.split('/')
|
104
145
|
parent_modules = parent_name_parts[0...-1].collect(&:to_sym)
|
105
|
-
parent_id = "#{parent_name_parts.last}_id".to_sym
|
146
|
+
parent_id = "#{parent_name_parts.last}_id".to_sym # TODO: primary key
|
106
147
|
|
107
148
|
scope do |query|
|
108
149
|
query.where parent_id => params[parent_id]
|
@@ -117,7 +158,8 @@ module ResponderController
|
|
117
158
|
|
118
159
|
# Instance methods that support the Actions module.
|
119
160
|
module InstanceMethods
|
120
|
-
delegate :model_class_name, :model_class, :scopes, :responds_within, :
|
161
|
+
delegate :model_class_name, :model_class, :scopes, :responds_within, :serves_scopes,
|
162
|
+
:to => "self.class"
|
121
163
|
|
122
164
|
# Apply scopes to the given query.
|
123
165
|
#
|
@@ -141,9 +183,12 @@ module ResponderController
|
|
141
183
|
end
|
142
184
|
end
|
143
185
|
|
144
|
-
|
145
|
-
|
146
|
-
|
186
|
+
requested = (model_class.scopes.keys & params.keys.collect { |k| k.to_sym })
|
187
|
+
raise ForbiddenScope if serves_scopes[:only] && (requested - serves_scopes[:only]).any?
|
188
|
+
raise ForbiddenScope if serves_scopes[:except] && (requested & serves_scopes[:except]).any?
|
189
|
+
|
190
|
+
query = requested.inject(query) do |query, scope|
|
191
|
+
query.send scope, *params[scope.to_s] rescue raise BadScope
|
147
192
|
end
|
148
193
|
|
149
194
|
query
|
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{responder_controller}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Phil Smith"]
|
12
|
-
s.date = %q{2010-05-
|
12
|
+
s.date = %q{2010-05-21}
|
13
13
|
s.description = %q{Responders make crud controllers tiny, this wraps the rest.}
|
14
14
|
s.email = %q{phil.h.smith@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -205,6 +205,76 @@ describe "ResponderController" do
|
|
205
205
|
end
|
206
206
|
end
|
207
207
|
|
208
|
+
describe '.serves_scopes' do
|
209
|
+
before :each do
|
210
|
+
@controller = PostsController.new
|
211
|
+
@controller.params['commented_on_by'] = 'you'
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'can specify a white list of active record scopes to serve' do
|
215
|
+
PostsController.serves_scopes :only => [:recent, :authored_by, :published_after]
|
216
|
+
lambda do
|
217
|
+
@controller.scope @query
|
218
|
+
end.should raise_error(ResponderController::ForbiddenScope)
|
219
|
+
end
|
220
|
+
|
221
|
+
it 'can specify just one scope to white list' do
|
222
|
+
PostsController.serves_scopes :only => :recent
|
223
|
+
lambda do
|
224
|
+
@controller.scope @query
|
225
|
+
end.should raise_error(ResponderController::ForbiddenScope)
|
226
|
+
end
|
227
|
+
|
228
|
+
it 'can specify a black list of active record scopes to deny' do
|
229
|
+
PostsController.serves_scopes :except => [:commented_on_by, :unpublished]
|
230
|
+
lambda do
|
231
|
+
@controller.scope @query
|
232
|
+
end.should raise_error(ResponderController::ForbiddenScope)
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'can specify just one scope to black list' do
|
236
|
+
PostsController.serves_scopes :except => :commented_on_by
|
237
|
+
lambda do
|
238
|
+
@controller.scope @query
|
239
|
+
end.should raise_error(ResponderController::ForbiddenScope)
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'whines if passed anything other than a hash' do
|
243
|
+
lambda do
|
244
|
+
PostsController.serves_scopes 'cupcakes!'
|
245
|
+
end.should raise_error TypeError
|
246
|
+
end
|
247
|
+
|
248
|
+
it 'whines about keys other than :only and :except' do
|
249
|
+
lambda do
|
250
|
+
PostsController.serves_scopes 'only' => :recent
|
251
|
+
end.should raise_error ArgumentError
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'whines when both :only and :except are passed' do
|
255
|
+
lambda do
|
256
|
+
PostsController.serves_scopes :only => :recent, :except => :commented_on_by
|
257
|
+
end.should raise_error ArgumentError
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'whines if both :only and :except are passed between different calls' do
|
261
|
+
PostsController.serves_scopes :only => :recent
|
262
|
+
lambda do
|
263
|
+
PostsController.serves_scopes :except => :commented_on_by
|
264
|
+
end.should raise_error ArgumentError
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'accumulates scopes passed over multiple calls' do
|
268
|
+
PostsController.serves_scopes :only => :recent
|
269
|
+
PostsController.serves_scopes :only => :authored_by
|
270
|
+
PostsController.serves_scopes[:only].should == [:recent, :authored_by]
|
271
|
+
end
|
272
|
+
|
273
|
+
after :each do
|
274
|
+
PostsController.serves_scopes.clear # clean up
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
208
278
|
describe '#find_models' do
|
209
279
|
it 'is #scope #model_class.scoped' do
|
210
280
|
controller = PostsController.new
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: 0.2.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Phil Smith
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-05-
|
17
|
+
date: 2010-05-21 00:00:00 -07:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|