Olivier El Mekki

Get rake routes to recognize constraint classes in rails

I’ve been playing recently with the new rails-3 route constraints behaviour. Quite nice, actually, I really enjoy being able to add total custom code to determine if a route match. Of course (?), my first use of that has been subdomain constraints. Here is an example :

  constraints( ClientSubdomain ) do
    resource :organization, :only => %w( show edit update destroy ) do
      get 'close', :on => :member
    end
 
    root :to => 'static_page#get_page', :page => 'home_client'
  end
 
  constraints( AdminSubdomain ) do
    scope :module => 'admin' do
      resources :organizations
    end
 
    root :to => 'admin/organizations#index'
  end

So here, if I’m on the admin subdomain, I can handle multiple organizations, my clients ones. And if I am on a client subdomain, the organization resource is a singleton (client must only handle its own organization, right?).
Ok, let’s see what rake routes outputs.

close_organization GET    /organization/close(.:format)     {:action=>"close", :controller=>"organizations"}
 edit_organization GET    /organization/edit(.:format)      {:action=>"edit", :controller=>"organizations"}
      organization GET    /organization(.:format)           {:action=>"show", :controller=>"organizations"}
                   PUT    /organization(.:format)           {:action=>"update", :controller=>"organizations"}
                   DELETE /organization(.:format)           {:action=>"destroy", :controller=>"organizations"}
              root        /(.:format)                       {:action=>"get_page", :controller=>"static_page"}
     organizations GET    /organizations(.:format)          {:action=>"index", :controller=>"admin/organizations"}
                   POST   /organizations(.:format)          {:action=>"create", :controller=>"admin/organizations"}
  new_organization GET    /organizations/new(.:format)      {:action=>"new", :controller=>"admin/organizations"}
                   GET    /organizations/:id/edit(.:format) {:action=>"edit", :controller=>"admin/organizations"}
                   GET    /organizations/:id(.:format)      {:action=>"show", :controller=>"admin/organizations"}
                   PUT    /organizations/:id(.:format)      {:action=>"update", :controller=>"admin/organizations"}
                   DELETE /organizations/:id(.:format)      {:action=>"destroy", :controller=>"admin/organizations"}
              root        /(.:format)                       {:action=>"index", :controller=>"admin/organizations"}

Erf, not really helpful.

After playing a while with mapper, routeset and routes, I came to a solution.

Monkey patch for Constraints

First, we need to expose Constraints#constraints to the world. Create a lib/route_constraints.rb:

module ActionDispatch
  module Routing
    class Mapper
      class Constraints
        attr_reader :constraints
      end
    end
  end
end

Be sure to require it in config/environment.rb.

Rake task

Then, put this content in lib/tasks/route_with_constraints.rake:
Gist

namespace :routes do
  desc 'Print out all defined routes in match order, with names, per constraint class. Target specific constraint class with CONSTRAINT=x. Target specific controller with CONTROLLER=x.'
  task :constrained => :environment do
    Rails.application.reload_routes!
 
    constraints_routes = Hash.new
 
    Rails.application.routes.routes.each do |route|
      group = (route.app.class == ActionDispatch::Routing::Mapper::Constraints ? route.app.constraints.to_s : 'No constraint class')
      constraints_routes[group] ||= []
      constraints_routes[group] < < route
    end
 
    requested_constraint = ENV['CONSTRAINT']
 
    constraints_routes.each do |group, all_routes|
      if requested_constraint.nil? or group == requested_constraint
        puts "\n\nConstraint class : #{group}\n\n"
 
        if ENV['CONTROLLER']
          all_routes = all_routes.select{ |route| route.defaults[:controller] == ENV['CONTROLLER'] }
        end
 
        routes = all_routes.collect do |route|
 
          reqs = route.requirements.dup
          reqs[:to] = route.app unless route.app.class.name.to_s =~ /^ActionDispatch::Routing/
          reqs = reqs.empty? ? "" : reqs.inspect
 
          {:name => route.name.to_s, :verb => route.verb.to_s, :path => route.path, :reqs => reqs}
        end
 
        routes.reject! { |r| r[:path] =~ %r{/rails/info/properties} } # Skip the route if it's internal info route
 
        name_width = routes.map{ |r| r[:name].length }.max
        verb_width = routes.map{ |r| r[:verb].length }.max
        path_width = routes.map{ |r| r[:path].length }.max
 
        routes.each do |r|
          puts "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
        end
      end
    end
  end
end

Ok, now we have a rake routes:constrained task. Let see its output :

Constraint class : No constraint class

Constraint class : AdminSubdomain

   organizations GET    /organizations(.:format)          {:action=>"index", :controller=>"admin/organizations"}
                 POST   /organizations(.:format)          {:action=>"create", :controller=>"admin/organizations"}
new_organization GET    /organizations/new(.:format)      {:action=>"new", :controller=>"admin/organizations"}
                 GET    /organizations/:id/edit(.:format) {:action=>"edit", :controller=>"admin/organizations"}
                 GET    /organizations/:id(.:format)      {:action=>"show", :controller=>"admin/organizations"}
                 PUT    /organizations/:id(.:format)      {:action=>"update", :controller=>"admin/organizations"}
                 DELETE /organizations/:id(.:format)      {:action=>"destroy", :controller=>"admin/organizations"}
            root        /(.:format)                       {:action=>"index", :controller=>"admin/organizations"}

Constraint class : ClientSubdomain

close_organization GET    /organization/close(.:format) {:action=>"close", :controller=>"organizations"}
 edit_organization GET    /organization/edit(.:format)  {:action=>"edit", :controller=>"organizations"}
      organization GET    /organization(.:format)       {:action=>"show", :controller=>"organizations"}
                   PUT    /organization(.:format)       {:action=>"update", :controller=>"organizations"}
                   DELETE /organization(.:format)       {:action=>"destroy", :controller=>"organizations"}
              root        /(.:format)                   {:action=>"get_page", :controller=>"static_page"}

Way better :)

Zookshop : création de boutiques en ligne à faible coût

Travailleurs du web, combien de fois avez-vous été contactés par des clients souhaitant créer une boutique en ligne pour finalement vous rendre compte que leur budget était tout à fait insuffisant?

Lire la suite

Le développement top-down en uad

Cet article fait suite à la conversation que j’ai eue avec Patrick Fratczak sur la manière de faire du TDD.

Voici donc la méthode que j’emploi pour faire du user acceptance testing. L’exemple décrit ici utilise rails, cucumber et rspec, mais cela fonctionne également bien avant Typolight, cucumber et phpspec.

Lire la suite

Getters et setters en PHP

Venant du monde de ruby, j’ai pris l’habitude de ne pas faire de distinction dans l’utilisation d’un objet entre une méthode et un attribut. En ruby, un attribut se récupère exactement de la même manière qu’une méthode s’appelle.
Lire la suite

Typolight et les modèles : M comme dans VC

Si on me posait la question du problème majeur dans le core de Typolight, je répondrais : l’implémentation incomplète du pattern MVC. La couche modèle est difficilement utilisable telle-quelle et provoque l’abondance de requêtes SQL dans les contrôleurs. Il y a aussi le problème de la présence de HTML dans les contrôleurs, mais cela ne concernera pas ce tutoriel. Nous verrons cette fois comment rendre aux modèles ce qui appartient aux modèles.

Lire la suite