rc_rails 2.1.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.
Files changed (98) hide show
  1. data/.gitignore +7 -0
  2. data/CHANGELOG +355 -0
  3. data/Gemfile +5 -0
  4. data/Gemfile.lock.development +117 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.rdoc +71 -0
  7. data/Rakefile +33 -0
  8. data/Todo.txt +1 -0
  9. data/lib/rc_rails.rb +9 -0
  10. data/lib/resources_controller/actions.rb +147 -0
  11. data/lib/resources_controller/active_record/saved.rb +15 -0
  12. data/lib/resources_controller/helper.rb +123 -0
  13. data/lib/resources_controller/include_actions.rb +37 -0
  14. data/lib/resources_controller/named_route_helper.rb +154 -0
  15. data/lib/resources_controller/railtie.rb +14 -0
  16. data/lib/resources_controller/request_path_introspection.rb +83 -0
  17. data/lib/resources_controller/resource_methods.rb +32 -0
  18. data/lib/resources_controller/singleton_actions.rb +21 -0
  19. data/lib/resources_controller/specification.rb +119 -0
  20. data/lib/resources_controller/version.rb +3 -0
  21. data/lib/resources_controller.rb +849 -0
  22. data/resources_controller.gemspec +29 -0
  23. data/spec/app/database.yml +5 -0
  24. data/spec/app/views/accounts/show.html.erb +0 -0
  25. data/spec/app/views/addresses/edit.html.erb +0 -0
  26. data/spec/app/views/addresses/index.html.erb +0 -0
  27. data/spec/app/views/addresses/new.html.erb +0 -0
  28. data/spec/app/views/addresses/show.html.erb +0 -0
  29. data/spec/app/views/admin/forums/create.html.erb +0 -0
  30. data/spec/app/views/admin/forums/destroy.html.erb +0 -0
  31. data/spec/app/views/admin/forums/edit.html.erb +0 -0
  32. data/spec/app/views/admin/forums/index.html.erb +0 -0
  33. data/spec/app/views/admin/forums/new.html.erb +0 -0
  34. data/spec/app/views/admin/forums/show.html.erb +0 -0
  35. data/spec/app/views/admin/forums/update.html.erb +0 -0
  36. data/spec/app/views/comments/edit.html.erb +0 -0
  37. data/spec/app/views/comments/index.html.erb +0 -0
  38. data/spec/app/views/comments/new.html.erb +0 -0
  39. data/spec/app/views/comments/show.html.erb +0 -0
  40. data/spec/app/views/forum_posts/edit.html.erb +0 -0
  41. data/spec/app/views/forum_posts/index.html.erb +0 -0
  42. data/spec/app/views/forum_posts/new.html.erb +0 -0
  43. data/spec/app/views/forum_posts/show.html.erb +0 -0
  44. data/spec/app/views/forums/create.html.erb +0 -0
  45. data/spec/app/views/forums/destroy.html.erb +0 -0
  46. data/spec/app/views/forums/edit.html.erb +0 -0
  47. data/spec/app/views/forums/index.html.erb +0 -0
  48. data/spec/app/views/forums/new.html.erb +0 -0
  49. data/spec/app/views/forums/show.html.erb +0 -0
  50. data/spec/app/views/forums/update.html.erb +0 -0
  51. data/spec/app/views/infos/edit.html.erb +0 -0
  52. data/spec/app/views/infos/show.html.erb +0 -0
  53. data/spec/app/views/interests/index.html.erb +0 -0
  54. data/spec/app/views/interests/show.html.erb +0 -0
  55. data/spec/app/views/owners/edit.html.erb +0 -0
  56. data/spec/app/views/owners/new.html.erb +0 -0
  57. data/spec/app/views/owners/show.html.erb +0 -0
  58. data/spec/app/views/tags/index.html.erb +0 -0
  59. data/spec/app/views/tags/new.html.erb +0 -0
  60. data/spec/app/views/tags/show.html.erb +0 -0
  61. data/spec/app/views/users/edit.html.erb +0 -0
  62. data/spec/app/views/users/index.html.erb +0 -0
  63. data/spec/app/views/users/show.html.erb +0 -0
  64. data/spec/app.rb +315 -0
  65. data/spec/controllers/accounts_controller_spec.rb +77 -0
  66. data/spec/controllers/addresses_controller_spec.rb +346 -0
  67. data/spec/controllers/admin_forums_controller_spec.rb +638 -0
  68. data/spec/controllers/comments_controller_spec.rb +380 -0
  69. data/spec/controllers/comments_controller_with_models_spec.rb +202 -0
  70. data/spec/controllers/forum_posts_controller_spec.rb +426 -0
  71. data/spec/controllers/forums_controller_spec.rb +694 -0
  72. data/spec/controllers/infos_controller_spec.rb +71 -0
  73. data/spec/controllers/interests_controller_via_forum_spec.rb +80 -0
  74. data/spec/controllers/interests_controller_via_user_spec.rb +114 -0
  75. data/spec/controllers/owners_controller_spec.rb +277 -0
  76. data/spec/controllers/resource_saved_spec.rb +47 -0
  77. data/spec/controllers/resource_service_in_forums_controller_spec.rb +37 -0
  78. data/spec/controllers/resource_service_in_infos_controller_spec.rb +36 -0
  79. data/spec/controllers/resource_service_in_interests_controller_via_forum_spec.rb +51 -0
  80. data/spec/controllers/tags_controller_spec.rb +83 -0
  81. data/spec/controllers/tags_controller_via_account_info_spec.rb +131 -0
  82. data/spec/controllers/tags_controller_via_forum_post_comment_spec.rb +144 -0
  83. data/spec/controllers/tags_controller_via_forum_post_spec.rb +133 -0
  84. data/spec/controllers/tags_controller_via_forum_spec.rb +173 -0
  85. data/spec/controllers/tags_controller_via_user_address_spec.rb +130 -0
  86. data/spec/controllers/users_controller_spec.rb +248 -0
  87. data/spec/lib/action_view_helper_spec.rb +143 -0
  88. data/spec/lib/bug_0001_spec.rb +22 -0
  89. data/spec/lib/include_actions_spec.rb +35 -0
  90. data/spec/lib/load_enclosing_resources_spec.rb +245 -0
  91. data/spec/lib/request_path_introspection_spec.rb +130 -0
  92. data/spec/lib/resource_methods_spec.rb +204 -0
  93. data/spec/lib/resources_controller_spec.rb +57 -0
  94. data/spec/models/comment_saved_spec.rb +24 -0
  95. data/spec/rspec_generator_task.rb +105 -0
  96. data/spec/spec_helper.rb +17 -0
  97. data/spec/verify_rcov.rb +52 -0
  98. metadata +193 -0
@@ -0,0 +1,849 @@
1
+ require 'resources_controller/active_record/saved'
2
+ require 'resources_controller/railtie' if defined?(Rails)
3
+
4
+ require 'resources_controller/actions'
5
+ require 'resources_controller/helper'
6
+ require 'resources_controller/include_actions'
7
+ require 'resources_controller/named_route_helper'
8
+ require 'resources_controller/request_path_introspection'
9
+ require 'resources_controller/resource_methods'
10
+ require 'resources_controller/singleton_actions'
11
+ require 'resources_controller/specification'
12
+
13
+ # With resources_controller you can quickly add
14
+ # an ActiveResource compliant controller for your your RESTful models.
15
+ #
16
+ # = Examples
17
+ # Here are some examples - for more on how to use RC go to the Usage section at the bottom,
18
+ # for syntax head to resources_controller_for
19
+ #
20
+ # ==== Example 1: Super simple usage
21
+ # Here's a simple example of how it works with a Forums has many Posts model:
22
+ #
23
+ # class ForumsController < ApplicationController
24
+ # resources_controller_for :forums
25
+ # end
26
+ #
27
+ # Your controller will get the standard CRUD actions, @forum will be set in member actions, @forums in
28
+ # index.
29
+ #
30
+ # ==== Example 2: Specifying enclosing resources
31
+ # class PostsController < ApplicationController
32
+ # resources_controller_for :posts, :in => :forum
33
+ # end
34
+ #
35
+ # As above, but the controller will load @forum on every action, and use @forum to find and create @posts
36
+ #
37
+ # ==== Wildcard enclosing resources
38
+ # All of the above examples will work for any routes that match what it specified
39
+ #
40
+ # PATH RESOURCES CONTROLLER WILL DO:
41
+ #
42
+ # Example 1 /forums @forums = Forum.find(:all)
43
+ #
44
+ # /users/2/forums @user = User.find(2)
45
+ # @forums = @user.forums.find(:all)
46
+ #
47
+ # Example 2 /posts This won't work as the controller specified
48
+ # that :posts are :in => :forum
49
+ #
50
+ # /forums/2/posts @forum = Forum.find(2)
51
+ # @posts = @forum.posts.find(:all)
52
+ #
53
+ # /sites/4/forums/3/posts @site = Site.find(4)
54
+ # @forum = @site.forums.find(3)
55
+ # @posts = @forum.posts.find(:all)
56
+ #
57
+ # /users/2/posts/1 This won't work as the controller specified
58
+ # that :posts are :in => :forum
59
+ #
60
+ #
61
+ # It is up to you which routes to open to the controller (in config/routes.rb). When
62
+ # you do, RC will use the route segments to drill down to the specified resource. This means
63
+ # that if User 3 does not have Post 5, then /users/3/posts/5 will raise a RecordNotFound Error.
64
+ # You dont' have to write any extra code to do this oft repeated controller pattern.
65
+ #
66
+ # With RC, your route specification flows through to the controller - no need to repeat yourself.
67
+ #
68
+ # If you don't want to have RC match wildcard resources just pass :load_enclosing => false
69
+ #
70
+ # resources_controller_for :posts, :in => :forum, :load_enclosing => false
71
+ #
72
+ # ==== Example 3: Singleton resource
73
+ # Here's an example of a singleton, the account pattern that is so common.
74
+ #
75
+ # class AccountController < ApplicationController
76
+ # resources_controller_for :account, :class => User, :singleton => true do
77
+ # @current_user
78
+ # end
79
+ # end
80
+ #
81
+ # Your controller will use the block to find the resource. The @account will be assigned to @current_user
82
+ #
83
+ # ==== Example 4: Allowing PostsController to be used all over
84
+ # First thing to do is remove :in => :forum
85
+ #
86
+ # class PostsController < ApplicationController
87
+ # resources_controller_for :posts
88
+ # end
89
+ #
90
+ # This will now work for /users/2/posts.
91
+ #
92
+ # ==== Example 4 and a bit: Mapping non standard resources
93
+ # How about /account/posts? The account is found in a non standard way - RC won't be able
94
+ # to figure out how tofind it if it appears in the route. So we give it some help.
95
+ #
96
+ # (in PostsController)
97
+ #
98
+ # map_enclosing_resource :account, :singleton => true, :class => User, :find => :current_user
99
+ #
100
+ # Now, if :account apears in any part of a route (for PostsController) it will be mapped to
101
+ # (in this case) the current_user method of teh PostsController.
102
+ #
103
+ # To make the :account mapping available to all, just chuck it in ApplicationController
104
+ #
105
+ # This will work for any resource which can't be inferred from its route segment name
106
+ #
107
+ # map_enclosing_resource :users, :segment => :peeps, :key => 'peep_id'
108
+ # map_enclosing_resource :posts, :class => OddlyNamedPostClass
109
+ #
110
+ # ==== Example 5: Singleton association
111
+ # Here's another singleton example - one where it corresponds to a has_one or belongs_to association
112
+ #
113
+ # class ImageController < ApplicationController
114
+ # resources_controller_for :image, :singleton => true
115
+ # end
116
+ #
117
+ # When invoked with /users/3/image RC will find @user, and use @user.image to find the resource, and
118
+ # @user.build_image, to create a new resource.
119
+ #
120
+ # ==== Example 6: :resource_path (equivalent resource path): aliasing a named route to a RESTful route
121
+ #
122
+ # You may have a named route that maps a url to a particular controller and action,
123
+ # this causes resources_controller problems as it relies on the route to load the
124
+ # resources. You can get around this by specifying :resource_path as a param in routes.rb
125
+ #
126
+ # map.root :controller => :forums, :action => :index, :resource_path => '/forums'
127
+ #
128
+ # When the controller is invoked via the '' url, rc will use :resource_path to recognize the
129
+ # route.
130
+ #
131
+ # This is only necessary if you have wildcard enclosing resources enabled (the default)
132
+ #
133
+ # ==== Putting it all together
134
+ #
135
+ # An exmaple app
136
+ #
137
+ # config/routes.rb:
138
+ #
139
+ # map.resource :account do |account|
140
+ # account.resource :image
141
+ # account.resources :posts
142
+ # end
143
+ #
144
+ # map.resources :users do |user|
145
+ # user.resource :image
146
+ # user.resources :posts
147
+ # end
148
+ #
149
+ # map.resources :forums do |forum|
150
+ # forum.resources :posts
151
+ # forum.resource :image
152
+ # end
153
+ #
154
+ # map.root :controller => :forums, :action => :index, :resource_path => '/forums'
155
+ #
156
+ # app/controllers:
157
+ #
158
+ # class ApplicationController < ActionController::Base
159
+ # map_enclosing_resource :account, :singleton => true, :find => :current_user
160
+ #
161
+ # def current_user # get it from session or whatnot
162
+ # end
163
+ #
164
+ # class ForumsController < AplicationController
165
+ # resources_controller_for :forums
166
+ # end
167
+ #
168
+ # class PostsController < AplicationController
169
+ # resources_controller_for :posts
170
+ # end
171
+ #
172
+ # class UsersController < AplicationController
173
+ # resources_controller_for :users
174
+ # end
175
+ #
176
+ # class ImageController < AplicationController
177
+ # resources_controller_for :image, :singleton => true
178
+ # end
179
+ #
180
+ # class AccountController < ApplicationController
181
+ # resources_controller_for :account, :singleton => true, :find => :current_user
182
+ # end
183
+ #
184
+ # This is how the app will handle the following routes:
185
+ #
186
+ # PATH CONTROLLER WHICH WILL DO:
187
+ #
188
+ # /forums forums @forums = Forum.find(:all)
189
+ #
190
+ # /forums/2/posts posts @forum = Forum.find(2)
191
+ # @posts = @forum.forums.find(:all)
192
+ #
193
+ # /forums/2/image image @forum = Forum.find(2)
194
+ # @image = @forum.image
195
+ #
196
+ # /image <no route>
197
+ #
198
+ # /posts <no route>
199
+ #
200
+ # /users/2/posts/3 posts @user = User.find(2)
201
+ # @post = @user.posts.find(3)
202
+ #
203
+ # /users/2/image POST image @user = User.find(2)
204
+ # @image = @user.build_image(params[:image])
205
+ #
206
+ # /account account @account = self.current_user
207
+ #
208
+ # /account/image image @account = self.current_user
209
+ # @image = @account.image
210
+ #
211
+ # /account/posts/3 PUT posts @account = self.current_user
212
+ # @post = @account.posts.find(3)
213
+ # @post.update_attributes(params[:post])
214
+ #
215
+ # === Views
216
+ #
217
+ # Ok - so how do I write the views?
218
+ #
219
+ # For most cases, just in exactly the way you would expect to. RC sets the instance variables
220
+ # to what they should be.
221
+ #
222
+ # But, in some cases, you are going to have different variables set - for example
223
+ #
224
+ # /users/1/posts => @user, @posts
225
+ # /forums/2/posts => @forum, @posts
226
+ #
227
+ # Here are some options (all are appropriate for different circumstances):
228
+ # * test for the existence of @user or @forum in the view, and display it differently
229
+ # * have two different controllers UserPostsController and ForumPostsController, with different views
230
+ # (and direct the routes to them in routes.rb)
231
+ # * use enclosing_resource - which always refers to the... immediately enclosing resource.
232
+ #
233
+ # Using the last technique, you might write your posts index as follows
234
+ # (here assuming that both Forum and User have .name)
235
+ #
236
+ # <h1>Posts for <%= link_to enclosing_resource_path, "#{enclosing_resource_name.humanize}: #{enclosing_resource.name}" %></h1>
237
+ #
238
+ # <%= render :partial => 'post', :collection => @posts %>
239
+ #
240
+ # Notice *enclosing_resource_name* - this will be something like 'user', or 'post'.
241
+ # Also *enclosing_resource_path* - in RC you get all of the named route helpers relativised to the current resource
242
+ # and enclosing_resource. See NamedRouteHelper for more details.
243
+ #
244
+ # This can useful when writing the _post partial:
245
+ #
246
+ # <p>
247
+ # <%= post.name %>
248
+ # <%= link_to 'edit', edit_resource_path(tag) %>
249
+ # <%= link_to 'destroy', resource_path(tag), :method => :delete %>
250
+ # </p>
251
+ #
252
+ # when viewed at /users/1/posts it will show
253
+ #
254
+ # <p>
255
+ # Cool post
256
+ # <a href="/users/1/posts/1/edit">edit</a>
257
+ # <a href="js nightmare with /users/1/posts/1">delete</a>
258
+ # </p>
259
+ # ...
260
+ #
261
+ # when viewd at /forums/1/posts it will show
262
+ #
263
+ # <p>
264
+ # Other post
265
+ # <a href="/forums/1/posts/3/edit">edit</a>
266
+ # <a href="js nightmare with /forums/1/posts/3">delete</a>
267
+ # </p>
268
+ # ...
269
+ #
270
+ # This is like polymorphic urls, except that RC will just use whatever enclosing resources are loaded to generate the urls/paths.
271
+ #
272
+ # = Usage
273
+ # To use RC, there are just three class methods on controller to learn.
274
+ #
275
+ # resources_controller_for <name>, <options>, <&block>
276
+ #
277
+ # ClassMethods#nested_in <name>, <options>, <&block>
278
+ #
279
+ # map_enclosing_resource <name>, <options>, <&block>
280
+ #
281
+ # === Customising finding and creating
282
+ # If you want to implement something like query params you can override *find_resources*. If you want to change the
283
+ # way your new resources are created you can override *new_resource*.
284
+ #
285
+ # class PostsController < ApplicationController
286
+ # resources_controller_for :posts
287
+ #
288
+ # def find_resources
289
+ # resource_service.find :all, :order => params[:sort_by]
290
+ # end
291
+ #
292
+ # # you can call super to help yourself to the existing implementation
293
+ # def new_resource
294
+ # super.tap {|r| r.ip_address = request.ip_address }
295
+ # end
296
+ #
297
+ # In the same way, you can override *find_resource*.
298
+ #
299
+ # === Writing controller actions
300
+ #
301
+ # You can make use of RC internals to simplify your actions.
302
+ #
303
+ # Here's an example where you want to re-order an acts_as_list model. You define a class method
304
+ # on the model (say *order_by_ids* which takes and array of ids). You can then make use of *resource_service*
305
+ # (which makes use of awesome rails magic) to send correctly scoped messages to your models.
306
+ #
307
+ # Here's how to write an order action
308
+ #
309
+ # def order
310
+ # resource_service.order_by_ids["things_order"]
311
+ # end
312
+ #
313
+ # the route
314
+ #
315
+ # map.resources :things, :collection => {:order => :put}
316
+ #
317
+ # and the view can conatin a scriptaculous drag and drop with param name 'things_order'
318
+ #
319
+ # When this controller is invoked of /things the :order_by_ids message will be sent to the Thing class,
320
+ # when it's invoked by /foos/1/things, then :order_by_ids message will be send to Foo.find(1).things association
321
+ #
322
+ # === using non standard ids
323
+ #
324
+ # Lets say you want to set to_param to login, and use find_by_login
325
+ # for your users in your URLs, with routes as follows:
326
+ #
327
+ # map.reosurces :users do |user|
328
+ # user.resources :addresses
329
+ # end
330
+ #
331
+ # First, the users controller needs to find reosurces using find_by_login
332
+ #
333
+ # class UsersController < ApplicationController
334
+ # resources_controller_for :users
335
+ #
336
+ # protected
337
+ # def find_resource(id = params[:id])
338
+ # resource_service.find_by_login(id)
339
+ # end
340
+ # end
341
+ #
342
+ # This controller will find users (for editing, showing, and destroying) as
343
+ # directed. (this controller will work for any route where user is the
344
+ # last resource, including the /users/dave route)
345
+ #
346
+ # Now you need to specify that the user as enclosing resource needs to be found
347
+ # with find_by_login. For the addresses case above, you would do this:
348
+ #
349
+ # class AddressesController < ApplicationController
350
+ # resources_controller_for :addresses
351
+ # nested_in :user do
352
+ # User.find_by_login(params[:user_id])
353
+ # end
354
+ # end
355
+ #
356
+ # If you wanted to open up more nested resources under user, you could repeat
357
+ # this specification in all such controllers, alternatively, you could map the
358
+ # resource in the ApplicationController, which would be usable by any controller
359
+ #
360
+ # If you know that user is never nested (i.e. /users/dave/addresses), then do this:
361
+ #
362
+ # class ApplicationController < ActionController::Base
363
+ # map_enclosing_resource :user do
364
+ # User.find(params[:user_id])
365
+ # end
366
+ # end
367
+ #
368
+ # or, if user is sometimes nested (i.e. /forums/1/users/dave/addresses), do this:
369
+ #
370
+ # map_enclosing_resource :user do
371
+ # ((enclosing_resource && enclosing_resource.users) || User).find(params[:user_id])
372
+ # end
373
+ #
374
+ # Your Addresses controller will now be the very simple one, and the resource map will
375
+ # load user as specified when it is hit by a route /users/dave/addresses.
376
+ #
377
+ # class AddressesController < ApplicationController
378
+ # resources_controller_for :addresses
379
+ # end
380
+ #
381
+ module ResourcesController
382
+ mattr_accessor :actions, :singleton_actions
383
+ self.actions = ResourcesController::Actions
384
+ self.singleton_actions = ResourcesController::SingletonActions
385
+
386
+ def self.extended(base)
387
+ base.class_eval do
388
+ class_attribute :resource_specification_map
389
+ self.resource_specification_map = {}
390
+ end
391
+ end
392
+
393
+ # Specifies that this controller is a REST style controller for the named resource
394
+ #
395
+ # Enclosing resources are loaded automatically by default, you can turn this off with
396
+ # :load_enclosing (see options below)
397
+ #
398
+ # resources_controller_for <name>, <options>, <&block>
399
+ #
400
+ # ==== Options:
401
+ # * <tt>:singleton:</tt> (default false) set this to true if the resource is a Singleton
402
+ # * <tt>:find:</tt> (default null) set this to a symbol or Proc to specify how to find the resource.
403
+ # Use this if the resource is found in an unconventional way. Passing a block has the same effect as
404
+ # setting :find => a Proc
405
+ # * <tt>:in:</tt> specify the enclosing resources, by name. ClassMethods#nested_in can be used to
406
+ # specify this more fully.
407
+ # * <tt>:load_enclosing:</tt> (default true) loads enclosing resources automatically.
408
+ # * <tt>:actions:</tt> (default nil) set this to false if you don't want the default RC actions. Set this
409
+ # to a module to use that module for your own actions.
410
+ # * <tt>:only:</tt> only include the specified actions.
411
+ # * <tt>:except:</tt> include all actions except the specified actions.
412
+ #
413
+ # ===== Options for unconvential use
414
+ # (otherwise these are all inferred from the _name_)
415
+ # * <tt>:route:</tt> the route name (without name_prefix) if it can't be inferred from _name_.
416
+ # For a collection resource this should be plural, for a singleton it should be singular.
417
+ # * <tt>:source:</tt> a string or symbol (e.g. :users, or :user). This is used to find the class or association name
418
+ # * <tt>:class:</tt> a Class. This is the class of the resource (if it can't be inferred from _name_ or :source)
419
+ # * <tt>:segment:</tt> (e.g. 'users') the segment name in the route that is matched
420
+ #
421
+ # === The :in option
422
+ # The default behavior is to set up before filters that load the enclosing resource, and to use associations on
423
+ # that model to find and create the resources. See ClassMethods#nested_in for more details on this, and
424
+ # customising the default behaviour.
425
+ #
426
+ # === load_enclosing_resources
427
+ # By default, a before_filter is added by resources_controller called :load_enclosing_resources - which
428
+ # does all the work of loading the enclosing resources. You can use ActionControllers standard filter
429
+ # mechanisms to control when this filter is invoked. For example - you can choose not to load resources
430
+ # on an action
431
+ #
432
+ # resources_controller_for :foos
433
+ # skip_before_filter :load_enclosing_resources, :only => :static_page
434
+ #
435
+ # Or, you can change the order of when the filter is invoked by adding the filter call yourself (rc will
436
+ # only add the filter if it doesn't exist)
437
+ #
438
+ # before_filter :do_something
439
+ # prepend_before_filter :load_enclosing_resources
440
+ # resources_controller_for :foos
441
+ # before_filter :do_something_else # chain => [:load_enclosing_resources, :do_something, :do_something_else]
442
+ #
443
+ # === Default actions module
444
+ # If you have your own actions module you prefer to use other than the standard resources_controller ones
445
+ # you can set ResourcesController.actions to that module to have this be included by default
446
+ #
447
+ # ResourcesController.actions = MyAwesomeActions
448
+ # ResourcesController.singleton_actions = MyAweseomeSingletonActions
449
+ #
450
+ # class AwesomenessController < ApplicationController
451
+ # resources_controller_for :awesomenesses # includes MyAwesomeActions by default
452
+ # end
453
+ def resources_controller_for(name, options = {}, &block)
454
+ options.assert_valid_keys(:class, :source, :singleton, :actions, :in, :find, :load_enclosing, :route, :segment, :as, :only, :except, :resource_methods)
455
+ when_options = {:only => options.delete(:only), :except => options.delete(:except)}
456
+
457
+ unless included_modules.include? ResourcesController::InstanceMethods
458
+ class_attribute :specifications, :route_name
459
+ hide_action :specifications, :route_name
460
+ extend ResourcesController::ClassMethods
461
+ helper ResourcesController::Helper
462
+ include ResourcesController::InstanceMethods, ResourcesController::NamedRouteHelper
463
+ include ResourcesController::ResourceMethods unless options.delete(:resource_methods) == false || included_modules.include?(ResourcesController::ResourceMethods)
464
+ end
465
+
466
+ before_filter(:load_enclosing_resources, when_options.dup) unless load_enclosing_resources_filter_exists?
467
+
468
+ self.specifications = []
469
+ specifications << '*' unless options.delete(:load_enclosing) == false
470
+
471
+ unless (actions = options.delete(:actions)) == false
472
+ actions ||= options[:singleton] ? ResourcesController.singleton_actions : ResourcesController.actions
473
+ include_actions actions, when_options
474
+ end
475
+
476
+ route = (options.delete(:route) || name).to_s
477
+ name = options[:singleton] ? name.to_s : name.to_s.singularize
478
+ self.route_name = options[:singleton] ? route : route.singularize
479
+
480
+ nested_in(*options.delete(:in)) if options[:in]
481
+
482
+ class_attribute :resource_specification, :instance_writer => false
483
+ self.resource_specification = Specification.new(name, options, &block)
484
+ end
485
+
486
+ # Creates a resource specification mapping. Use this to specify how to find an enclosing resource that
487
+ # does not obey usual rails conventions. Most commonly this would be a singleton resource.
488
+ #
489
+ # See Specification#new for details of how to call this
490
+ def map_enclosing_resource(name, options = {}, &block)
491
+ spec = Specification.new(name, options, &block)
492
+ resource_specification_map[spec.segment] = spec
493
+ end
494
+
495
+ # this will be deprecated soon as it's badly named - use map_enclosing_resource
496
+ def map_resource(*args, &block)
497
+ map_enclosing_resource(*args, &block)
498
+ end
499
+
500
+ # Include the specified module, optionally specifying which public methods to include, for example:
501
+ # include_actions ActionMixin, :only => :index
502
+ # include_actions ActionMixin, :except => [:create, :new]
503
+ def include_actions(mixin, options = {})
504
+ mixin.extend(IncludeActions) unless mixin.respond_to?(:include_actions)
505
+ mixin.include_actions(self, options)
506
+ end
507
+
508
+ private
509
+ def load_enclosing_resources_filter_exists?
510
+ if respond_to?(:find_filter) # BC 2.0-stable branch
511
+ find_filter(:load_enclosing_resources)
512
+ else
513
+ _process_action_callbacks.detect {|c| c.filter == :load_enclosing_resources}
514
+ end
515
+ end
516
+
517
+ module ClassMethods
518
+ # Specifies that this controller has a particular enclosing resource.
519
+ #
520
+ # This can be called with an array of symbols (in which case options can't be specified) or
521
+ # a symbol with options.
522
+ #
523
+ # See Specification#new for details of how to call this.
524
+ def nested_in(*names, &block)
525
+ options = names.extract_options!
526
+ raise ArgumentError, "when giving more than one nesting, you may not specify options or a block" if names.length > 1 and (block_given? or options.length > 0)
527
+
528
+ # convert :polymorphic option to '?'
529
+ if options.delete(:polymorphic)
530
+ raise ArgumentError, "when specifying :polymorphic => true, no block or other options may be given" if block_given? or options.length > 0
531
+ names = ["?#{names.first}"]
532
+ end
533
+
534
+ # ignore first '*' if it has already been specified by :load_enclosing == true
535
+ names.shift if specifications == ['*'] && names.first == '*'
536
+
537
+ names.each do |name|
538
+ ensure_sane_wildcard if name == '*'
539
+ specifications << (name.to_s =~ /^(\*|\?(.*))$/ ? name.to_s : Specification.new(name, options, &block))
540
+ end
541
+ end
542
+
543
+ private
544
+ # ensure that specifications array is determinate w.r.t route matching
545
+ def ensure_sane_wildcard
546
+ idx = specifications.length
547
+ while (idx -= 1) >= 0
548
+ if specifications[idx] == '*'
549
+ raise ArgumentError, "Can only specify one wildcard '*' in between resource specifications"
550
+ elsif specifications[idx].is_a?(Specification)
551
+ break
552
+ end
553
+ end
554
+ true
555
+ end
556
+ end
557
+
558
+ module InstanceMethods
559
+ def self.included(controller)
560
+ controller.send :hide_action, *instance_methods
561
+ end
562
+
563
+ def resource_service=(service)
564
+ @resource_service = service
565
+ end
566
+
567
+ def name_prefix
568
+ @name_prefix ||= ''
569
+ end
570
+
571
+ # name of the singular resource
572
+ def resource_name
573
+ resource_specification.name
574
+ end
575
+
576
+ # name of the resource collection
577
+ def resources_name
578
+ @resources_name ||= resource_specification.name.pluralize
579
+ end
580
+
581
+ # returns the controller's resource class
582
+ def resource_class
583
+ resource_specification.klass
584
+ end
585
+
586
+ # returns the controller's current resource.
587
+ def resource
588
+ instance_variable_get("@#{resource_name}")
589
+ end
590
+
591
+ # sets the controller's current resource, and
592
+ # decorates the object with a save hook, so we know if it's been saved
593
+ def resource=(record)
594
+ instance_variable_set("@#{resource_name}", record)
595
+ end
596
+
597
+ # returns the controller's current resources collection
598
+ def resources
599
+ instance_variable_get("@#{resources_name}")
600
+ end
601
+
602
+ # sets the controller's current resource collection
603
+ def resources=(collection)
604
+ instance_variable_set("@#{resources_name}", collection)
605
+ end
606
+
607
+ # returns the immediately enclosing resource
608
+ def enclosing_resource
609
+ enclosing_resources.last
610
+ end
611
+
612
+ # returns the name of the immediately enclosing resource
613
+ def enclosing_resource_name
614
+ @enclosing_resource_name
615
+ end
616
+
617
+ # returns the resource service for the controller - this will be lazilly created
618
+ # to a ResourceService, or a SingletonResourceService (if :singleton => true)
619
+ def resource_service
620
+ @resource_service ||= resource_specification.singleton? ? SingletonResourceService.new(self) : ResourceService.new(self)
621
+ end
622
+
623
+ # returns the instance resource_specification
624
+ def resource_specification
625
+ self.class.resource_specification
626
+ end
627
+
628
+ # returns an array of the controller's enclosing (nested in) resources
629
+ def enclosing_resources
630
+ @enclosing_resources ||= []
631
+ end
632
+
633
+ # returns an array of the collection (non singleton) enclosing resources, this is used for generating routes.
634
+ def enclosing_collection_resources
635
+ @enclosing_collection_resources ||= []
636
+ end
637
+
638
+ # NOTE: This method is overly complicated and unecessary. It's much clearer just to keep
639
+ # track of record saves yourself, this is here for BC. For an example of how it should be
640
+ # done look at the actions module in http://github.com/ianwhite/response_for_rc
641
+ #
642
+ # Has the resource been saved successfully?, if no save has been attempted, save the
643
+ # record and return the result
644
+ #
645
+ # This method uses the @resource_saved tracking var, or the model's state itself if
646
+ # that is not available (which means if you do resource.update_attributes, then this
647
+ # method will return the correct result)
648
+ def resource_saved?
649
+ save_resource if @resource_saved.nil? && !resource.validation_attempted?
650
+ @resource_saved = resource.saved? if @resource_saved.nil?
651
+ @resource_saved
652
+ end
653
+
654
+ # NOTE: it's clearer to just keep track of record saves yourself, this is here for BC
655
+ # See the comment on #resource_saved?
656
+ #
657
+ # @resource_saved = resource.update_attributes(params[resource_name])
658
+ #
659
+ # Save the resource, and keep track of the result
660
+ def save_resource
661
+ @resource_saved = resource.save
662
+ end
663
+
664
+ private
665
+ # this is the before_filter that loads all specified and wildcard resources
666
+ def load_enclosing_resources
667
+ namespace_segments.each {|segment| update_name_prefix("#{segment}_") }
668
+ specifications.each_with_index do |spec, idx|
669
+ case spec
670
+ when '*' then load_wildcards_from(idx)
671
+ when /^\?(.*)/ then load_wildcard($1)
672
+ else load_enclosing_resource_from_specification(spec)
673
+ end
674
+ end
675
+ end
676
+
677
+ # load a wildcard resource by either
678
+ # * matching the segment to mapped resource specification, or
679
+ # * creating one using the segment name
680
+ # Optionally takes a variable name to set the instance variable as (for polymorphic use)
681
+ def load_wildcard(as = nil)
682
+ seg = nesting_segments[enclosing_resources.size] or ResourcesController.raise_resource_mismatch(self)
683
+
684
+ segment = seg[:segment]
685
+ singleton = seg[:singleton]
686
+
687
+ if resource_specification_map[segment]
688
+ spec = resource_specification_map[segment]
689
+ spec = spec.dup.tap {|s| s.as = as} if as
690
+ else
691
+ spec = Specification.new(singleton ? segment : segment.singularize, :singleton => singleton, :as => as)
692
+ end
693
+ load_enclosing_resource_from_specification(spec)
694
+ end
695
+
696
+ # loads a series of wildcard resources, from the specified specification idx
697
+ #
698
+ # To do this, we need to figure out where the next specified resource is
699
+ # and how many single wildcards are prior to that. What is left over from
700
+ # the current route enclosing names will be the number of wildcards we need to load
701
+ def load_wildcards_from(start)
702
+ specs = specifications.slice(start..-1)
703
+ encls = nesting_segments.slice(enclosing_resources.size..-1)
704
+
705
+ if spec = specs.find {|s| s.is_a?(Specification)}
706
+ spec_seg = encls.index({:segment => spec.segment, :singleton => spec.singleton?}) or ResourcesController.raise_resource_mismatch(self)
707
+ number_of_wildcards = spec_seg - (specs.index(spec) -1)
708
+ else
709
+ number_of_wildcards = encls.length - (specs.length - 1)
710
+ end
711
+
712
+ number_of_wildcards.times { load_wildcard }
713
+ end
714
+
715
+ def load_enclosing_resource_from_specification(spec)
716
+ spec.segment == nesting_segments[enclosing_resources.size][:segment] or ResourcesController.raise_resource_mismatch(self)
717
+ spec.find_from(self).tap do |resource|
718
+ add_enclosing_resource(resource, :name => spec.name, :name_prefix => spec.name_prefix, :is_singleton => spec.singleton?, :as => spec.as)
719
+ end
720
+ end
721
+
722
+ def add_enclosing_resource(resource, options = {})
723
+ name = options[:name] || resource.class.name.underscore
724
+ update_name_prefix(options[:name_prefix] || (options[:name_prefix] == false ? '' : "#{name}_"))
725
+ enclosing_resources << resource
726
+ enclosing_collection_resources << resource unless options[:is_singleton]
727
+ instance_variable_set("@enclosing_resource_name", options[:name])
728
+ instance_variable_set("@#{name}", resource)
729
+ instance_variable_set("@#{options[:as]}", resource) if options[:as]
730
+ end
731
+
732
+ # The name prefix is used for forwarding urls and will be different depending on
733
+ # which route the controller was invoked by. The resource specifications build
734
+ # up the name prefix as the resources are loaded.
735
+ def update_name_prefix(name_prefix)
736
+ @name_prefix = "#{@name_prefix}#{name_prefix}"
737
+ end
738
+ end
739
+
740
+ # Proxy class to provide a consistent API for resource_service. This is mostly
741
+ # required for Singleton resources. Also allows decoration of the resource service with custom finders
742
+ class ResourceService < ActiveSupport::BasicObject
743
+ attr_reader :controller
744
+ delegate :resource_specification, :resource_class, :enclosing_resource, :to => :controller
745
+
746
+ def initialize(controller)
747
+ @controller = controller
748
+ end
749
+
750
+ def method_missing(*args, &block)
751
+ service.send(*args, &block)
752
+ end
753
+
754
+ def find(*args, &block)
755
+ resource_specification.find ? resource_specification.find_custom(controller) : super
756
+ end
757
+
758
+ # build association on the enclosing resource if there is one, otherwise call new
759
+ def new(*args, &block)
760
+ enclosing_resource ? service.build(*args, &block) : service.new(*args, &block)
761
+ end
762
+
763
+ # find the resource
764
+ # If we have a resource service, we call destroy on it with the reosurce id, so that any callbacks can be triggered
765
+ # Otherwise, just call destroy on the resource
766
+ def destroy(*args)
767
+ resource = find(*args)
768
+ if enclosing_resource
769
+ service.destroy(*args)
770
+ resource
771
+ else
772
+ resource.destroy
773
+ end
774
+ end
775
+
776
+ def respond_to?(method, include_private = false)
777
+ super || service.respond_to?(method)
778
+ end
779
+
780
+ def service
781
+ @service ||= enclosing_resource ? enclosing_resource.send(resource_specification.source) : resource_class
782
+ end
783
+ end
784
+
785
+ class SingletonResourceService < ResourceService
786
+ def find(*args)
787
+ if resource_specification.find
788
+ resource_specification.find_custom(controller)
789
+ elsif controller.enclosing_resources.size > 0
790
+ enclosing_resource.send(resource_specification.source)
791
+ else
792
+ ::ResourcesController.raise_cant_find_singleton(controller.resource_name, controller.resource_class)
793
+ end
794
+ end
795
+
796
+ # build association on the enclosing resource if there is one, otherwise call new
797
+ def new(*args, &block)
798
+ enclosing_resource ? enclosing_resource.send("build_#{resource_specification.source}", *args, &block) : service.new(*args, &block)
799
+ end
800
+
801
+ def destroy(*args)
802
+ find.destroy
803
+ end
804
+
805
+ def service
806
+ resource_class
807
+ end
808
+ end
809
+
810
+ class CantFindSingleton < RuntimeError #:nodoc:
811
+ end
812
+
813
+ class ResourceMismatch < RuntimeError #:nodoc:
814
+ end
815
+
816
+ class << self
817
+ def raise_cant_find_singleton(name, klass) #:nodoc:
818
+ raise CantFindSingleton, <<-end_str
819
+ Can't get singleton resource from class #{klass.name}. You have have probably done something like:
820
+
821
+ nested_in :#{name}, :singleton => true # <= where this is the first nested_in
822
+
823
+ You should tell resources_controller how to find the singleton resource like this:
824
+
825
+ nested_in :#{name}, :singleton => true do
826
+ #{klass.name}.find(<.. your find args here ..>)
827
+ end
828
+
829
+ Or:
830
+ nested_in :#{name}, :singleton => true, :find => <.. method name or lambda ..>
831
+
832
+ Or, you may be relying on the route to load the resource, in which case you need to give RC some
833
+ help. Do this by mapping the route segment to a resource in the controller, or a parent or mixin
834
+
835
+ map_enclosing_resource :#{name}, :segment => ..., :singleton => true <.. as above ..>
836
+ end_str
837
+ end
838
+
839
+ def raise_resource_mismatch(controller) #:nodoc:
840
+ raise ResourceMismatch, <<-end_str
841
+ resources_controller can't match the route to the resource specification
842
+ path: #{controller.send(:request_path)}
843
+ specification: enclosing: [#{controller.specifications.collect{|s| s.is_a?(Specification) ? ":#{s.segment}" : s}.join(', ')}], resource :#{controller.resource_specification.segment}
844
+
845
+ the successfully loaded enclosing resources are: #{controller.enclosing_resources.join(', ')}
846
+ end_str
847
+ end
848
+ end
849
+ end