responder_controller 0.2.0 → 0.3.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 +6 -0
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/responder_controller.rb +4 -297
- data/lib/responder_controller/actions.rb +57 -0
- data/lib/responder_controller/class_methods.rb +126 -0
- data/lib/responder_controller/instance_methods.rb +118 -0
- data/responder_controller.gemspec +14 -5
- data/spec/responder_controller/actions_spec.rb +140 -0
- data/spec/responder_controller/class_methods_spec.rb +266 -0
- data/spec/responder_controller/instance_methods_spec.rb +239 -0
- data/spec/responder_controller_spec.rb +2 -586
- metadata +13 -4
data/CHANGELOG
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
*ResponderController 0.2.0*
|
2
|
+
|
3
|
+
* Added .serves_scopes, which takes :only and :except options to restrict the list of active
|
4
|
+
record scopes that #index will serve. If a forbidden scope is requested, ForbiddenScope will be
|
5
|
+
raised, which will render as a 403 if uncaught.
|
6
|
+
|
1
7
|
*ResponderController 0.1.3*
|
2
8
|
|
3
9
|
* Render BadScopes as 400s, not 422s
|
data/Rakefile
CHANGED
@@ -10,7 +10,7 @@ begin
|
|
10
10
|
gem.email = "phil.h.smith@gmail.com"
|
11
11
|
gem.homepage = "http://github.com/phs/responder_controller"
|
12
12
|
gem.authors = ["Phil Smith"]
|
13
|
-
gem.add_dependency "activesupport", ">= 3.0.0.
|
13
|
+
gem.add_dependency "activesupport", ">= 3.0.0.beta3"
|
14
14
|
gem.add_development_dependency "rspec", ">= 1.2.9"
|
15
15
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
16
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/lib/responder_controller.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
require 'active_support/inflector'
|
2
2
|
require 'active_support/core_ext/string/inflections'
|
3
|
-
require 'active_support/core_ext/module/delegation'
|
4
3
|
|
5
4
|
module ResponderController
|
5
|
+
autoload :ClassMethods, 'responder_controller/class_methods'
|
6
|
+
autoload :InstanceMethods, 'responder_controller/instance_methods'
|
7
|
+
autoload :Actions, 'responder_controller/actions'
|
8
|
+
|
6
9
|
# Root of scope-related exceptions.
|
7
10
|
class ScopeError < StandardError
|
8
11
|
end
|
@@ -30,300 +33,4 @@ module ResponderController
|
|
30
33
|
ForbiddenScope.name => :forbidden
|
31
34
|
}) if defined? ActionDispatch
|
32
35
|
end
|
33
|
-
|
34
|
-
# Configure how the controller finds and serves models of what flavor.
|
35
|
-
module ClassMethods
|
36
|
-
# The underscored, fully-qualified name of the served model class.
|
37
|
-
#
|
38
|
-
# By default, it is the underscored controller class name, without +_controller+.
|
39
|
-
def model_class_name
|
40
|
-
@model_class_name || name.underscore.gsub(/_controller$/, '').singularize
|
41
|
-
end
|
42
|
-
|
43
|
-
# Declare the underscored, fully-qualified name of the served model class.
|
44
|
-
#
|
45
|
-
# Modules are declared with separating slashes, such as in <tt>admin/setting</tt>. Strings
|
46
|
-
# or symbols are accepted, but other values (including actual classes) will raise
|
47
|
-
# <tt>ArgumentError</tt>s.
|
48
|
-
def serves_model(model_class_name)
|
49
|
-
unless model_class_name.is_a? String or model_class_name.is_a? Symbol
|
50
|
-
raise ArgumentError.new "Model must be a string or symbol"
|
51
|
-
end
|
52
|
-
|
53
|
-
@model_class_name = model_class_name.to_s
|
54
|
-
end
|
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
|
-
|
83
|
-
# Declare leading arguments ("responder context") for +respond_with+ calls.
|
84
|
-
#
|
85
|
-
# +respond_with+ creates urls from models. To avoid strongly coupling models to a url
|
86
|
-
# structure, it can take any number of leading parameters a la +polymorphic_url+.
|
87
|
-
# +responds_within+ declares these leading parameters, to be used on each +respond_with+ call.
|
88
|
-
#
|
89
|
-
# It takes either a varargs or a block, but not both. In
|
90
|
-
# InstanceMethods#respond_with_contextual, the blocks are called with +instance_exec+, taking
|
91
|
-
# the model (or models) as a parameter. They should return an array.
|
92
|
-
def responds_within(*args, &block)
|
93
|
-
if block and args.any?
|
94
|
-
raise ArgumentError.new("responds_within can take arguments or a block, but not both")
|
95
|
-
elsif block or args.any?
|
96
|
-
@responds_within ||= []
|
97
|
-
if not args.empty?
|
98
|
-
@responds_within.concat args
|
99
|
-
else
|
100
|
-
@responds_within << block
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
@responds_within || model_class_name.split('/')[0...-1].collect { |m| m.to_sym }
|
105
|
-
end
|
106
|
-
|
107
|
-
# The served model class, identified by #model_class_name.
|
108
|
-
def model_class
|
109
|
-
model_class_name.camelize.constantize
|
110
|
-
end
|
111
|
-
|
112
|
-
# Declare a class-level scope for model collections.
|
113
|
-
#
|
114
|
-
# The model class is expected to respond to +all+, returning an Enumerable of models.
|
115
|
-
# Declared scopes are applied to (and replace) this collection, suitable for active record
|
116
|
-
# scopes.
|
117
|
-
#
|
118
|
-
# It takes one of a string, symbol or block. Symbols and strings are called as methods on the
|
119
|
-
# collection without arguments. Blocks are called with +instance_exec+ taking the current,
|
120
|
-
# accumulated query and returning the new, scoped one.
|
121
|
-
def scope(*args, &block)
|
122
|
-
scope = args.first || block
|
123
|
-
|
124
|
-
scope = scope.to_sym if String === scope
|
125
|
-
unless scope.is_a? Symbol or scope.is_a? Proc
|
126
|
-
raise ArgumentError.new "Scope must be a string, symbol or block"
|
127
|
-
end
|
128
|
-
|
129
|
-
(@scopes ||= []) << scope
|
130
|
-
end
|
131
|
-
|
132
|
-
# The array of declared class-level scopes, as symbols or procs.
|
133
|
-
attr_reader :scopes
|
134
|
-
|
135
|
-
# Declare a (non-singleton) parent resource class.
|
136
|
-
#
|
137
|
-
# <tt>children_of 'accounts/user'</tt> implies a scope and some responder context. The scope
|
138
|
-
# performs an ActiveRecord <tt>where :user_id => params[:user_id]</tt>. The responder context
|
139
|
-
# is a call to <tt>#responds_within</tt> declaring the parent model's modules along with the
|
140
|
-
# parent itself, found with <tt>Accounts::User.find(params[:user_id])</tt>.
|
141
|
-
def children_of(parent_model_class_name)
|
142
|
-
parent_model_class_name = parent_model_class_name.to_s.underscore
|
143
|
-
|
144
|
-
parent_name_parts = parent_model_class_name.split('/')
|
145
|
-
parent_modules = parent_name_parts[0...-1].collect(&:to_sym)
|
146
|
-
parent_id = "#{parent_name_parts.last}_id".to_sym # TODO: primary key
|
147
|
-
|
148
|
-
scope do |query|
|
149
|
-
query.where parent_id => params[parent_id]
|
150
|
-
end
|
151
|
-
|
152
|
-
responds_within do
|
153
|
-
parent = parent_model_class_name.camelize.constantize.find params[parent_id]
|
154
|
-
parent_modules + [parent]
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
# Instance methods that support the Actions module.
|
160
|
-
module InstanceMethods
|
161
|
-
delegate :model_class_name, :model_class, :scopes, :responds_within, :serves_scopes,
|
162
|
-
:to => "self.class"
|
163
|
-
|
164
|
-
# Apply scopes to the given query.
|
165
|
-
#
|
166
|
-
# Applicable scopes come from two places. They are either declared at the class level with
|
167
|
-
# <tt>ClassMethods#scope</tt>, or named in the request itself. The former is good for
|
168
|
-
# defining topics or enforcing security, while the latter is free slicing and dicing for
|
169
|
-
# clients.
|
170
|
-
#
|
171
|
-
# Class-level scopes are applied first. Request scopes come after, and are discovered by
|
172
|
-
# examining +params+. If any +params+ key matches a name found in
|
173
|
-
# <tt>ClassMethods#model_class.scopes.keys</tt>, then it is taken to be a scope and is
|
174
|
-
# applied. The values under that +params+ key are passed along as arguments.
|
175
|
-
def scope(query)
|
176
|
-
query = (scopes || []).inject(query) do |query, scope|
|
177
|
-
if Symbol === scope and model_class.scopes.key? scope
|
178
|
-
query.send scope
|
179
|
-
elsif Proc === scope
|
180
|
-
instance_exec query, &scope
|
181
|
-
else
|
182
|
-
raise ArgumentError.new "Unknown scope #{model_class}.#{scope}"
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
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
|
192
|
-
end
|
193
|
-
|
194
|
-
query
|
195
|
-
end
|
196
|
-
|
197
|
-
# Find all models in #scope.
|
198
|
-
#
|
199
|
-
# The initial query is <tt>ClassMethods#model_class.scoped</tt>.
|
200
|
-
def find_models
|
201
|
-
scope model_class.scoped
|
202
|
-
end
|
203
|
-
|
204
|
-
# Find a particular model.
|
205
|
-
#
|
206
|
-
# #find_models is asked to find a model with <tt>params[:id]</tt>. This ensures that
|
207
|
-
# class-level scopes are enforced (potentially for security.)
|
208
|
-
def find_model
|
209
|
-
find_models.find(params[:id])
|
210
|
-
end
|
211
|
-
|
212
|
-
# The underscored model class name, as a symbol.
|
213
|
-
#
|
214
|
-
# Model modules are omitted.
|
215
|
-
def model_slug
|
216
|
-
model_class_name.split('/').last.to_sym
|
217
|
-
end
|
218
|
-
|
219
|
-
# Like #model_slug, but plural.
|
220
|
-
def models_slug
|
221
|
-
model_slug.to_s.pluralize.to_sym
|
222
|
-
end
|
223
|
-
|
224
|
-
# The name of the instance variable holding a single model instance.
|
225
|
-
def model_ivar
|
226
|
-
"@#{model_slug}"
|
227
|
-
end
|
228
|
-
|
229
|
-
# The name of the instance variable holding a collection of models.
|
230
|
-
def models_ivar
|
231
|
-
model_ivar.pluralize
|
232
|
-
end
|
233
|
-
|
234
|
-
# Retrive #models_ivar
|
235
|
-
def models
|
236
|
-
instance_variable_get models_ivar
|
237
|
-
end
|
238
|
-
|
239
|
-
# Assign #models_ivar
|
240
|
-
def models=(models)
|
241
|
-
instance_variable_set models_ivar, models
|
242
|
-
end
|
243
|
-
|
244
|
-
# Retrive #model_ivar
|
245
|
-
def model
|
246
|
-
instance_variable_get model_ivar
|
247
|
-
end
|
248
|
-
|
249
|
-
# Assign #model_ivar
|
250
|
-
def model=(model)
|
251
|
-
instance_variable_set model_ivar, model
|
252
|
-
end
|
253
|
-
|
254
|
-
# Apply ClassMethods#responds_within to the given model (or symbol.)
|
255
|
-
#
|
256
|
-
# "Apply" just means turning +responds_within+ into an array and appending +model+ to the
|
257
|
-
# end. If +responds_within+ is an array, it used directly.
|
258
|
-
#
|
259
|
-
# If it is a proc, it is called with +instance_exec+, passing +model+ in. It should return an
|
260
|
-
# array, which +model+ will be appended to. (So, don't include it in the return value.)
|
261
|
-
def responder_context(model)
|
262
|
-
context = responds_within.collect do |o|
|
263
|
-
o = instance_exec model, &o if o.is_a? Proc
|
264
|
-
o
|
265
|
-
end.flatten + [model]
|
266
|
-
end
|
267
|
-
|
268
|
-
# Pass +model+ through InstanceMethods#responder_context, and pass that to #respond_with.
|
269
|
-
def respond_with_contextual(model)
|
270
|
-
respond_with *responder_context(model)
|
271
|
-
end
|
272
|
-
end
|
273
|
-
|
274
|
-
# The seven standard restful actions.
|
275
|
-
module Actions
|
276
|
-
# Find, assign and respond with models.
|
277
|
-
def index
|
278
|
-
self.models = find_models
|
279
|
-
respond_with_contextual models
|
280
|
-
end
|
281
|
-
|
282
|
-
# Find, assign and respond with a single model.
|
283
|
-
def show
|
284
|
-
self.model = find_model
|
285
|
-
respond_with_contextual model
|
286
|
-
end
|
287
|
-
|
288
|
-
# Build (but do not save), assign and respond with a new model.
|
289
|
-
#
|
290
|
-
# The new model is built from the <tt>InstanceMethods#find_models</tt> collection, meaning it
|
291
|
-
# could inherit any properties implied by those scopes.
|
292
|
-
def new
|
293
|
-
self.model = find_models.build
|
294
|
-
respond_with_contextual model
|
295
|
-
end
|
296
|
-
|
297
|
-
# Find, assign and respond with a single model.
|
298
|
-
def edit
|
299
|
-
self.model = find_model
|
300
|
-
respond_with_contextual model
|
301
|
-
end
|
302
|
-
|
303
|
-
# Build, save, assign and respond with a new model.
|
304
|
-
#
|
305
|
-
# The model is created with attributes from the request params, under the
|
306
|
-
# <tt>InstanceMethods#model_slug</tt> key.
|
307
|
-
def create
|
308
|
-
self.model = find_models.build(params[model_slug])
|
309
|
-
model.save
|
310
|
-
respond_with_contextual model
|
311
|
-
end
|
312
|
-
|
313
|
-
# Find, update, assign and respond with a single model.
|
314
|
-
#
|
315
|
-
# The new attributes are taken from the request params, under the
|
316
|
-
# <tt>InstanceMethods#model_slug</tt> key.
|
317
|
-
def update
|
318
|
-
self.model = find_model
|
319
|
-
model.update_attributes(params[model_slug])
|
320
|
-
respond_with_contextual model
|
321
|
-
end
|
322
|
-
|
323
|
-
# Find and destroy a model. Respond with <tt>InstanceMethods#models_slug</tt>.
|
324
|
-
def destroy
|
325
|
-
find_model.destroy
|
326
|
-
respond_with_contextual models_slug
|
327
|
-
end
|
328
|
-
end
|
329
36
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ResponderController
|
2
|
+
# The seven standard restful actions.
|
3
|
+
module Actions
|
4
|
+
# Find, assign and respond with models.to_a.
|
5
|
+
def index
|
6
|
+
self.models = find_models
|
7
|
+
respond_with_contextual models.to_a
|
8
|
+
end
|
9
|
+
|
10
|
+
# Find, assign and respond with a single model.
|
11
|
+
def show
|
12
|
+
self.model = find_model
|
13
|
+
respond_with_contextual model
|
14
|
+
end
|
15
|
+
|
16
|
+
# Build (but do not save), assign and respond with a new model.
|
17
|
+
#
|
18
|
+
# The new model is built from the <tt>InstanceMethods#find_models</tt> collection, meaning it
|
19
|
+
# could inherit any properties implied by those scopes.
|
20
|
+
def new
|
21
|
+
self.model = find_models.build
|
22
|
+
respond_with_contextual model
|
23
|
+
end
|
24
|
+
|
25
|
+
# Find, assign and respond with a single model.
|
26
|
+
def edit
|
27
|
+
self.model = find_model
|
28
|
+
respond_with_contextual model
|
29
|
+
end
|
30
|
+
|
31
|
+
# Build, save, assign and respond with a new model.
|
32
|
+
#
|
33
|
+
# The model is created with attributes from the request params, under the
|
34
|
+
# <tt>InstanceMethods#model_slug</tt> key.
|
35
|
+
def create
|
36
|
+
self.model = find_models.build(params[model_slug])
|
37
|
+
model.save
|
38
|
+
respond_with_contextual model
|
39
|
+
end
|
40
|
+
|
41
|
+
# Find, update, assign and respond with a single model.
|
42
|
+
#
|
43
|
+
# The new attributes are taken from the request params, under the
|
44
|
+
# <tt>InstanceMethods#model_slug</tt> key.
|
45
|
+
def update
|
46
|
+
self.model = find_model
|
47
|
+
model.update_attributes(params[model_slug])
|
48
|
+
respond_with_contextual model
|
49
|
+
end
|
50
|
+
|
51
|
+
# Find and destroy a model. Respond with <tt>InstanceMethods#models_slug</tt>.
|
52
|
+
def destroy
|
53
|
+
find_model.destroy
|
54
|
+
respond_with_contextual models_slug
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module ResponderController
|
2
|
+
# Configure how the controller finds and serves models of what flavor.
|
3
|
+
module ClassMethods
|
4
|
+
# The underscored, fully-qualified name of the served model class.
|
5
|
+
#
|
6
|
+
# By default, it is the underscored controller class name, without +_controller+.
|
7
|
+
def model_class_name
|
8
|
+
@model_class_name || name.underscore.gsub(/_controller$/, '').singularize
|
9
|
+
end
|
10
|
+
|
11
|
+
# Declare the underscored, fully-qualified name of the served model class.
|
12
|
+
#
|
13
|
+
# Modules are declared with separating slashes, such as in <tt>admin/setting</tt>. Strings
|
14
|
+
# or symbols are accepted, but other values (including actual classes) will raise
|
15
|
+
# <tt>ArgumentError</tt>s.
|
16
|
+
def serves_model(model_class_name)
|
17
|
+
unless model_class_name.is_a? String or model_class_name.is_a? Symbol
|
18
|
+
raise ArgumentError.new "Model must be a string or symbol"
|
19
|
+
end
|
20
|
+
|
21
|
+
@model_class_name = model_class_name.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
# Declare what active record scopes to allow or forbid to requests.
|
25
|
+
#
|
26
|
+
# .serves_scopes follows the regular :only/:except form: white-listed scopes are passed by
|
27
|
+
# name as <tt>:only => [:allowed, :scopes]</tt> or <tt>:only => :just_one</tt>. Similarly,
|
28
|
+
# black-listed ones are passed under <tt>:except</tt>.
|
29
|
+
#
|
30
|
+
# If a white-list is passed, all other requested scopes (i.e. scopes named by query parameters)
|
31
|
+
# will be denied, raising <tt>ForbiddenScope</tt>. If a black-list is passed, only they will
|
32
|
+
# raise the exception.
|
33
|
+
def serves_scopes(options = nil)
|
34
|
+
@serves_scopes ||= {}
|
35
|
+
|
36
|
+
if options
|
37
|
+
raise TypeError unless options.is_a? Hash
|
38
|
+
|
39
|
+
new_keys = @serves_scopes.keys | options.keys
|
40
|
+
unless new_keys == [:only] or new_keys == [:except]
|
41
|
+
raise ArgumentError.new("serves_scopes takes exactly one of :only and :except")
|
42
|
+
end
|
43
|
+
|
44
|
+
@serves_scopes[options.keys.first] ||= []
|
45
|
+
@serves_scopes[options.keys.first].concat [*options.values.first]
|
46
|
+
end
|
47
|
+
|
48
|
+
@serves_scopes
|
49
|
+
end
|
50
|
+
|
51
|
+
# Declare leading arguments ("responder context") for +respond_with+ calls.
|
52
|
+
#
|
53
|
+
# +respond_with+ creates urls from models. To avoid strongly coupling models to a url
|
54
|
+
# structure, it can take any number of leading parameters a la +polymorphic_url+.
|
55
|
+
# +responds_within+ declares these leading parameters, to be used on each +respond_with+ call.
|
56
|
+
#
|
57
|
+
# It takes either a varargs or a block, but not both. In
|
58
|
+
# InstanceMethods#respond_with_contextual, the blocks are called with +instance_exec+, taking
|
59
|
+
# the model (or models) as a parameter. They should return an array.
|
60
|
+
def responds_within(*args, &block)
|
61
|
+
if block and args.any?
|
62
|
+
raise ArgumentError.new("responds_within can take arguments or a block, but not both")
|
63
|
+
elsif block or args.any?
|
64
|
+
@responds_within ||= []
|
65
|
+
if not args.empty?
|
66
|
+
@responds_within.concat args
|
67
|
+
else
|
68
|
+
@responds_within << block
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
@responds_within || model_class_name.split('/')[0...-1].collect { |m| m.to_sym }
|
73
|
+
end
|
74
|
+
|
75
|
+
# The served model class, identified by #model_class_name.
|
76
|
+
def model_class
|
77
|
+
model_class_name.camelize.constantize
|
78
|
+
end
|
79
|
+
|
80
|
+
# Declare a class-level scope for model collections.
|
81
|
+
#
|
82
|
+
# The model class is expected to respond to +all+, returning an Enumerable of models.
|
83
|
+
# Declared scopes are applied to (and replace) this collection, suitable for active record
|
84
|
+
# scopes.
|
85
|
+
#
|
86
|
+
# It takes one of a string, symbol or block. Symbols and strings are called as methods on the
|
87
|
+
# collection without arguments. Blocks are called with +instance_exec+ taking the current,
|
88
|
+
# accumulated query and returning the new, scoped one.
|
89
|
+
def scope(*args, &block)
|
90
|
+
scope = args.first || block
|
91
|
+
|
92
|
+
scope = scope.to_sym if String === scope
|
93
|
+
unless scope.is_a? Symbol or scope.is_a? Proc
|
94
|
+
raise ArgumentError.new "Scope must be a string, symbol or block"
|
95
|
+
end
|
96
|
+
|
97
|
+
(@scopes ||= []) << scope
|
98
|
+
end
|
99
|
+
|
100
|
+
# The array of declared class-level scopes, as symbols or procs.
|
101
|
+
attr_reader :scopes
|
102
|
+
|
103
|
+
# Declare a (non-singleton) parent resource class.
|
104
|
+
#
|
105
|
+
# <tt>children_of 'accounts/user'</tt> implies a scope and some responder context. The scope
|
106
|
+
# performs an ActiveRecord <tt>where :user_id => params[:user_id]</tt>. The responder context
|
107
|
+
# is a call to <tt>#responds_within</tt> declaring the parent model's modules along with the
|
108
|
+
# parent itself, found with <tt>Accounts::User.find(params[:user_id])</tt>.
|
109
|
+
def children_of(parent_model_class_name)
|
110
|
+
parent_model_class_name = parent_model_class_name.to_s.underscore
|
111
|
+
|
112
|
+
parent_name_parts = parent_model_class_name.split('/')
|
113
|
+
parent_modules = parent_name_parts[0...-1].collect(&:to_sym)
|
114
|
+
parent_id = "#{parent_name_parts.last}_id".to_sym # TODO: primary key
|
115
|
+
|
116
|
+
scope do |query|
|
117
|
+
query.where parent_id => params[parent_id]
|
118
|
+
end
|
119
|
+
|
120
|
+
responds_within do
|
121
|
+
parent = parent_model_class_name.camelize.constantize.find params[parent_id]
|
122
|
+
parent_modules + [parent]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|