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 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