More on Full Circle REST
Some time ago I frothed on (hip people call this blogging) about how Rails was missing an opportunity to exploit the modelling work done in routes.rb. You can see my post here.
Anyway, in the intervening month or so, I have been working with Ian White’s nifty ResourcesController plugin. As is my wont, I bitch and moan a lot about what is wrong with ResourcesController (RC). The nice thing is that Ian listens and crafts some pretty damn nice code -a lot of my vision of Full-circle REST is starting to appear in RC. In fact, RC is so promising I have abandoned my own plugin and jumped into bed with Ian (in a manner of speaking).
RC can identify many of the RESTful resources of a concrete request with an increasingly concise invocation. It can handle singleton resources, arbitrary nesting and polymorphic nesting. For these features alone, I suggest you look into it.
RC identifies the abstract resources of the URI by examining the inbound request (it has evolved from doing this in a purely textual fashion to examining the matched route’s structure). But it could be better. It could identify the route (when the request arrives) more efficiently. It could link this concrete route directly and unambiguously to the abstract resource that generated it (keeping RC nice and DRY w.r.t routes.rb). And critically, the abstract resource could refer to its parents in the hierarchy of resources in routes.rb, allowing even more direct determination of the complete chain of abstract resources that is being supplied by the URI. Then we could have full-circle REST.
But at this point, RC is hampered by Rails itself. Three patches to edge are required to get us there. I’ve written the first two, and the third shouldn’t be to hard (volunteers?). I’m presenting them in reverse order because I think they’re easier to understand that way.
Patch #3 - Store the hierarchy of abstract REST resources that generate named routes. Currently, only a few strings (name_prefix) provide circumstantial evidence of the parents. Let’s be explicit and store a reference to the the parent in each ActionController::Resources::Resource instance. Then the abstract REST resources will be represented by a forest of trees -and that is pretty close to ideal.
Patch #2 - Store the generating abstract resource in the generated named route. I’ve written the code to do this, but I’m not proud. In fact, I think it is kinda ugly.
module ActionControllermodule Resourcesdef action_options_for(action, resource, method = nil)default_options = { :action => action.to_s, :resource => resource }require_id = !resource.kind_of?(SingletonResource)case default_options[:action]when “index”, “new” : default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements)when “create” : default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements)when “show”, “edit” : default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id))when “update” : default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id))when “destroy” : default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id))else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements)endendendend module ActionControllermodule Routingclass Routeattr_accessor :resourceendclass RouteBuilder # Construct and return a route with the given path and options (including the resource option)def build(path, options) # Wrap the path with slashespath = “/#{path}” unless path[0] == ?/path = “#{path}/” unless path[-1] == ?/ path = “/#{options[:path_prefix]}#{path}” if options[:path_prefix] resource = options.delete(:resource)segments = segments_for_route_path(path)defaults, requirements, conditions = divide_route_options(segments, options)requirements = assign_route_options(segments, defaults, requirements) route = Route.newroute.segments = segmentsroute.requirements = requirementsroute.conditions = conditionsroute.resource = resource if !route.significant_keys.include?(:action) && !route.requirements[:action]route.requirements[:action] = “index”route.significant_keys << :actionend if !route.significant_keys.include?(:controller)raise ArgumentError, “Illegal route: the :controller must be specified!”end routeendendendend
The actual code change is minimal, but it is not very clean in RouteBuilder -in fact, that code really should not have been required. FYI, this code was working about a month ago and I assume it should still work. Still, beware!
Patch #1 - Store the recognized route in the abstract request. Testing aside (see below), this one is simple:
module ActionControllerclass AbstractRequestattr_accessor :recognized_routeend module Routingclass RouteSetdef recognize(request)params, recognized_route = recognize_path(request.path, extract_request_environment(request))request.path_parameters = params.with_indifferent_accessrequest.recognized_route = recognized_route"#{params[:controller].camelize}Controller".constantizeend def recognize_path(path, environment={})routes.each do |route|result = route.recognize(path, environment) and return result, routeend allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } } if environment[:method] && !HTTP_METHODS.include?(environment[:method])raise NotImplemented.new(*allows)elsif !allows.empty?raise MethodNotAllowed.new(*allows)elseraise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"endendendendend
I’ve changed the return signature of recognize_path so there may be some undesired side-effects. But I haven’t found any yet. Also, the TestRequest object would need to be modified as well to set the recognized_route attribute appropriately because recognize is not invoked in testing (another volunteer?).
So, a quick recap: Rails invites us to model our REST resources (in routes.rb) and uses that information for recognizing inbound routes and generating routes in views. But there is a much value to our modeling work to be found in the controllers themselves. And to harvest that value, we need visibility into the abstract resources that generated the invoked route. Rails needs to evolve in this direction I call full-circle REST.
Technorati Tags: REST resources_controller routing Rails