responder_controller 0.1.3 → 0.2.0
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/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
|