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 CHANGED
@@ -1,3 +1,7 @@
1
+ *ResponderController 0.1.3*
2
+
3
+ * Render BadScopes as 400s, not 422s
4
+
1
5
  *ResponderController 0.1.2*
2
6
 
3
7
  * If a requested scope errors out, raise a BadScope exception. Tell rails (if present) to render these as 422s.
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.3
1
+ 0.2.0
@@ -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
- class BadScope < StandardError
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
- # Uncaught BadScope exceptions become 400s
15
- if defined? ActionDispatch
16
- ActionDispatch::ShowExceptions.rescue_responses[BadScope.name] = :bad_request
17
- end
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, :to => "self.class"
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
- scopes_from_request = (model_class.scopes.keys & params.keys.collect { |k| k.to_sym })
145
- query = scopes_from_request.inject(query) do |query, scope|
146
- query.send scope, *params[scope.to_s] rescue raise BadScope.new
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.1.3"
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-19}
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
- - 1
8
- - 3
9
- version: 0.1.3
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-19 00:00:00 -07:00
17
+ date: 2010-05-21 00:00:00 -07:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency