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