Créer son microframework avec Rack (Partie 2)

projetsHier, nous avons terminé la journée avec un début de framework…

Passons en revue ce qu’il nous reste à faire.

Nous avons fait en sorte que les routes soient formatées sous forme d’expression rationnelle. Mais nous ne nous en sommes pas encore servi. En fait, je ne suis pas trop rentré dans le détail quand nous avons mis en place la méthode __urls__. Ce n’est pas grave, vous et moi avons gagné du temps, car, comme vous allez le voir, j’ai fait quelques modifications.

Et bien c’est tout. En effet, si nous voulons être conforme à l’exemple que j’avais donné, il ne restait que cela à traiter.

Dans la réalité, j’en ai fait un peu plus.

S’il y a bien une chose que je déteste, ce sont les pages d’erreur 404 par défaut. Et celle de Rack est une perle en la matière. Je vous propose donc d’ajouter la possibilité de customiser cette page. Et tant que nous y sommes, faisons-le également pour les erreurs 500 et 501.

Enfin, nous allons ajouter la gestion des sessions et un petit helper pour faciliter la mise en place des redirections.

Et les vues ? Et les modèles ?

Que nenni, comme je vous l’ai dit dans la première partie, l’objectif est d’avoir un backend pour des applications Cappuccino, alors pour les vues… En ce qui concerne les modèles, vous avez suffisamment de choix. C’est vrai, c’est facile les bases de données1. Mais bon, pour les sceptiques, si vous voulez, nous terminerons en fabriquant un petit blog2.

Revenons à nos routes. Nous avons mis en place la méthode Capcode.Route, prenant en paramètre un tableau de routes formatées sur un modèle d’expression régulière. L’idée était que pour une déclaration de ce type :

module Capcode
  class Hello < Route '/hello/(.*)'
    def get( arg )
      ...
    end
  end
  class World < Route '/world/([^\/]*)/(.*)'
    def get( arg1, arg2 )
      ...
    end
  end
end

Dans la méthode get de la classe Hello, si vous avons utilisé le chemin /hello/world alors, le paramètre arg contiendra la valeur world. De même, si nous utilisons le chemin /world/of/love, dans World.get, arg1 vaut of et arg2 vaut love. Maintenant, nous devons faire un choix. Que va-t-il se passer son nous utilisons les chemins /hello et /world/map ? Dans ces deux cas, je vous propose3 tout simplement que l’application se vautre4. La question qui se pose maintenant c’est comment doit réagir l’application, si nous utilisons le chemin /world/on/fire avec la déclaration suivante :

module Capcode
  class World < Route '/world/([^\/]*)/(.*)', '/world/on/(.*)'
    def get( arg1, arg2 )
      ...
    end
  end
end

Nous savons déjà que Rack prendre la route la plus adaptée. Donc dans le cas présent /world/of. Dans ce cas nous ne pouvons (et ne devons) capturer qu’une seule valeur. Or la méthode get en attend deux. Qu’à cela ne tienne, nous avons qu’à envoyer deux valeurs : la première étant celle capturée et la seconde sera tout simplement nil. Parce que finalement, si un développeur éprouve le besoin de faire un truc aussi tordu, il sera bien se débrouiller :|

Pour commencer, nous avons besoin de modifier un peu la méthode __urls__ de la classe renvoyée par Route. En effet, dans un cas comme celui que nous venons de voir, cette méthode renvoyait le tableau suivant :

[{'/world' => '([^\/]*)/(.*)', '/world/on' => '(.*)'}, Capcode::World]

Dans un cas nous nous attendons à capturer deux valeurs et dans l’autre une seule. Donc notre méthode get devra attendre deux paramètres. Mais pour savoir cela, et donc pouvoir faire l’appel à la méthode get dans la méthode call, il faut le déterminer. La seule solution consiste donc à lire chacune des regexp et à déterminer le nombre maximum de captures. Pour faire cela, je vous propose que nous fassions un petit hack de la classe Regexp. En fait, nous allons créer une méthode Regexp.number_of_captures qui nous donnera, pour l’objet Regexp le nombre de captures attendues :

class Regexp
  def number_of_captures
    c, x = 0, self.source.dup.gsub( /\\\(/, "" ).gsub( /\\\)/, "" )
    while( r = /(\([^\)]*\))/.match( x ) )
      c, x = c+1, r.post_match
    end
    c
  end
end

Ayant cela, nous allons modifier la déclaration de la méthode __urls__ de façon à renvoyer dans le tableau de sortie le nombre maximum de captures pour l’ensemble des routes du contrôleur :

module Capcode
  ...
  class << self
    ...
    def Route *u
      Class.new {
        meta_def(:__urls__){
          # < Route '/hello/world/([^\/]*)/id(\d*)', '/hello/(.*)'
          # # => [ {'/hello/world' => '([^\/]*)/id(\d*)', '/hello' => '(.*)'}, 2, <Capcode::Klass> ]
          h = {}
          max = 0
          u.each do |_u|
            m = /\/([^\/]*\(.*)/.match( _u )
            if m.nil?
              h[_u] = ''
            else
              h[m.pre_match] = m.captures[0]
              max = Regexp.new(m.captures[0]).number_of_captures if max < Regexp.new(m.captures[0]).number_of_captures
            end
          end
          [h, max, self]
        }
      }
      ...
    end
    ...
  end
  ...
end

Donc si nous reprenons notre exemple. Avec cette nouvelle version, __urls__ nous renverra cette fois-ci le résultat suivant :

[{'/world' => '([^\/]*)/(.*)', '/world/on' => '(.*)'}, 2, Capcode::World]

Nous pouvons maintenant modifier la méthode call. Mais avant cela, vous l’avez peut-être oublié, mais nous utilisons également la méthode __urls__ dans la méthode Capcode.run. En effet, c’est garce à elle que nous complétons la table des routes. Il ne faudra pas oublier de faire la correction. Je ne rentre pas dans le détail. Vous avez compris, et puis vous verrez cela lorsque je vous présenterai le code complet.

Jusqu’à maintenant, dans call nous somme resté très basique. En effet, nous vérifions le type de la requête et en fonction, nous appelons la méthode get ou post de notre contrôleur. Pour ce qui est du POST, nous pouvons laisser comme cela. En effet, nous ne sommes pas sensés envoyé de quelconques paramètres vie l’URL dans un POST. Pour le GET par contre, nous devons faire les éventuelles captures. La capture en elle-même n’a rien de bien compliqué. Nous devons simplement appliquer l’expression rationnelle rattachée au script_name sur le path_info. Ces deux dernières informations se récupèrent via la requête. Or souvenez-vous, nous avons récupéré l’objet Rack::Request au début de la méthode call. L’expression rationnelle quant à elle, sera récupérée via la méthode __urls__ :

sc = @request.script_name
sc = "/" if sc.size == 0
regexp = Regexp.new( self.class.__urls__[0][sc] )
nargs = self.class.__urls__[1]
 
args = regexp.match( Rack::Utils.unescape(@request.path_info).gsub( /^\//, "" ) )
if args.nil?
  raise Capcode::ParameterError, "Path info `#{@request.path_info}' does not match route regexp `#{regexp.source}'"
else
  args = args.captures
end
 
while args.size < nargs
  args << nil
end

A la fin de cet enchaînement, nous avons le tableau des valeurs des paramètres à passer à la méthode get. Vous remarquerez que l’on stocke la valeur du script_name dans la variable sc. Ceci afin de pouvoir tester qu’il ne s’agit pas d’une chaîne vide, et au besoin lui donner la valeur /. En effet, si vous passez par l’URL http://localhost:3000, alors le script_name est vide…

La méthode get n’attendant pas un tableau, mais bien des paramètres distincts, nous l’appellerons de la façon suivante :

get( *args )

Passons maintenant à la gestion des erreurs. Le plus simple pour cela est de faire un middleware Rack. Un middleware est une peu comme un contrôleur, mais il n’est pas associé à une route, mais utilisé lors de la création de l’application. Souvenez vous, dans la méthode Capcode.run nous enchainons plusieurs appels de middleware :

app = Rack::URLMap.new(@@__ROUTES)
app = Rack::ContentLength.new(app)
app = Rack::Lint.new(app)
app = Rack::ShowExceptions.new(app)
app = Rack::CommonLogger.new( app, Logger.new(conf[:log]) )

Nous allons donc en ajouter un à ce niveau, qui va se charger d’intercepter les erreurs.

Pour être cohérents, nous allons créer notre middleware dans le module Capcode. Cela ne devrait poser aucun problème puisque Capcode inclut le module Rack

Comme je l’ai dit, un middleware à la même tête qu’un contrôleur. La principale différence est que nous devons créer une méthode d’initialisation. Cette méthode prendra en paramètre l’application, en fera ce qu’il à a en faire et la stockera dans la variable de classe @app. Dans notre cas, nous n’avons rien de particulier à faire avec l’application. Nous interviendrons lors de l’appel de la réponse, donc dans la méthode call5. A ce niveau-là, nous allons récupérer le résultat du call de l’application, qui nous renverra un tableau contenant le statut de la réponse, le contenu de l’entête et le corps de la réponse. Si le statut est un 404, 500 ou 501, nous vérifierons si une méthode de surcharge existe pour l’erreur donnée, nous l’exécuterons et nous renverrons le tableau contenant le statut, le contenu de l’entête et le corps de la réponse modifié. Voici le squelette de notre middleware :

module Capcode
  ...
  class HTTPError
    def initialize(app)#:nodoc:
      @app = app
    end
 
    def call(env) #:nodoc:
      status, headers, body = @app.call(env)
 
      ...
 
      [status, headers, body]
    end
  end
  ...
end

Si nous laissons cela en l’état, nous avons alors fabriqué le premier middleware qui ne fait rien ! Nous allons donc ajouter les quelques lignes de code qui appellent la bonne fonction pour une valeur de statut donnée. Dans ces fonctions (une pour chaque code), nous allons modifier uniquement le corps de la réponse. Nous devrons également mettre à jour la valeur du Content-Length dans l’entête. Concernant les fonctions de surcharge, je vous propose6 de les appeler r404, r500 et r501. Donc ce que nous devons faire, c’est vérifier que ces méthodes existent et les appeler si c’est le cas. Sinon nous ne faisons rien. Voici donc notre middleware complet :

module Capcode
  ...
  class HTTPError
    def initialize(app)#:nodoc:
      @app = app
    end
 
    def call(env) #:nodoc:
      status, headers, body = @app.call(env)
 
      if self.methods.include? "r#{status}"
        body = self.call "r#{status}", env['REQUEST_PATH']
        headers['Content-Length'] = body.length.to_s
      end
 
      [status, headers, body]
    end
  end
  ...
end

Remarquez que nous passons à chaque méthode de surcharge la valeur du REQUEST_PATH, soit le chemin. Il nous suffit maintenant de rajouter ce middleware lors de la création de l’application dans Capcode.run. Coté l’utilisation, il suffit d’ajouter une méthode (r404, r500, r501) dans la classe Capcode::HTTPError :

require 'capcode'
 
module Capcode
  class HTTPError
    def r404(f)
      "Pas glop !!! #{f} est inconnu !!!"
    end
  end
 
  class Hello < Route '/time'
    def get( you )
       "Hello, it's '#{Time.now} !"
    end
  end
end
 
Capcode.run( :port => 3001, :host => "localhost" )

Réglons le cas des sessions maintenant. Il existe un middleware Rack pour gérer cela. En fait, il en existe plusieurs permettant de gérer des sessions via les cookies ou via memcached. Je ne retiendrai que la première solution, évitant ainsi aux utilisateurs de devoir installer memcached. Il suffit donc simplement d’ajouter le middleware Rack::Session::Cookie dans Capcode.run. Je me suis aussi amusé à ajouter une méthode session dans la classe renvoyée par Capcode.Route afin de faciliter l’utilisation de ces sessions. En effet, l’accès à la session se fait via la clé rack.session de l’environnement de l’application. J’ai donc ajouté le code suivant dans la classe :

def session
  @env['rack.session']
end

Terminons maintenant par la création d’un nouveau helper permettant de mettre en place des redirections. L’idée est la suivante. Dans un contrôleur, nous devons pouvoir ajouter un appel de méthode qui entraînera une redirection vers un contrôleur ou une URL donnée. Voici quelques exemples d’utilisation :

redirect( MonControleur )
# => Statut = 302, Location = /chemin
 
redirect( MonControleur, "hello", "world" )
# => Statut = 302, Location = /chemin/hello/world
 
redirect( "/hello" )
# => Statut = 302, Location = /hello
 
redirect( "http://monsite.com", "hello", "world" )
# => Statut = 302, Location = http://monsite.com/hello/world

Je pense que vous avez compris l’idée. Pour faire cela, nous avons besoin de modifier un peu la méthode call de la classe renvoyée par Capcode.Route. En effet, le helper redirect entraîne des modifications dans le statut et l’entête de la réponse. Or, pour le moment, la méthode call attend en retour de get ou post une chaîne correspondant au corps de la réponse. Nous allons donc modifier la méthode call de telle sorte qu’elle accepte une chaîne ou un tableau en retour de get et post. Si le résultat est une chaîne, alors nous considérerons qu’il s’agit du contenu du corps de la réponse. Si c’est un tableau, nous considérerons que la première valeur est le statut, la seconde un hachage contenant des données pour l’entête et le dernier est le corps de la réponse. Voici donc la nouvelle version de la méthode call :

def call( e ) #:nodoc:
  @env = e
  @response = Rack::Response.new
  @request = Rack::Request.new(@env)
 
  r = case @env["REQUEST_METHOD"]
    when "GET"
      sc = @request.script_name
      sc = "/" if sc.size == 0
      regexp = Regexp.new( self.class.__urls__[0][sc] )
      nargs = self.class.__urls__[1]
 
      args = regexp.match( Rack::Utils.unescape(@request.path_info).gsub( /^\//, "" ) )
      if args.nil?
        raise Capcode::ParameterError, "Path info `#{@request.path_info}' does not match route regexp `#{regexp.source}'"
      else
        args = args.captures
      end
 
      while args.size < nargs
        args << nil
      end
 
      get( *args )
    when "POST"
      post
  end
  if r.respond_to?(:to_ary)
    @response.status = r[0]
    r[1].each do |k,v|
      @response[k] = v
    end
    @response.body = r[2]
  else
    @response.write r
  end
 
  @response.finish
end

Pour le helper redirect, nous avons simplement besoin qu’il renvoi un tableau avec comme valeur de statut : 302, un champ Location contenant l’URL de redirection, et un corps vide :

module Capcode
  ...
  module Helpers
    ...
    def redirect( klass, *a )
      [302, {'Location' => URL(klass, *a)}, '']
    end
    ...
  end
end

Tout repose donc sur la méthode URL. C’est vrai quoi. Tant qu’à faire un helper, autant en faire deux… En effet URL sera lui aussi un helper que nous pourrons donc utiliser pour gérer les liens. Par exemple :

...
"<a href='#{URL(MonController, *args)}'>lien</a>"
...

La méthode URL URL prend donc en premier paramètre la classe du contrôleur ou le chemin et un tableau de paramètre à ajouter dans le chemin. Dans cette méthode, il faut donc commencer par déterminer si nous avons reçu une classe contrôleur ou un chemin. Facile ! Si la classe du premier paramètre est de type Class, c’est une class 8O Sinon c’est un chemin. Si c’est une classe, c’est donc un contrôleur, il faut rechercher son chemin. Pour cela il suffit de parcourir la table des routes (@@__ROUTES). Malheureusement nous sommes dans le module Capcode::Helpers et nous n’avons pas accès à cette table. Nous allons donc rajouter une méthode au module Capcode afin de pouvoir la récupérer :

module Capcode
  ...
  class << self
    ...
    def routes #:nodoc:
      @@__ROUTES
    end
    ...
  end
end

Maintenant que nous avons cela, nous pouvons mettre en place la méthode URL :

module Capcode
  ...
  module Helpers
    ...
    def URL( klass, *a )
      path = nil
      a = a.delete_if{ |x| x.nil? }
 
      if klass.class == Class
        Capcode.routes.each do |p, k|
          path = p if k.class == klass
        end
      else
        path = klass
      end
 
      path+((a.size>0)?("/"+a.join("/")):(""))
    end
    ...
  end
end

Et nous avons terminé. Voici donc la nouvelle version de Capcode :

require 'rubygems'
require 'rack'
require 'json'
require 'logger'
 
class Object
  def meta_def(m,&b)
    (class<<self;self end).send(:define_method,m,&b)
  end
end
 
class Regexp
  def number_of_captures
    c, x = 0, self.source.dup.gsub( /\\\(/, "" ).gsub( /\\\)/, "" )
    while( r = /(\([^\)]*\))/.match( x ) )
      c, x = c+1, r.post_match
    end
    c
  end
end
 
module Capcode
  CAPCOD_VERION="0.2.0"
  @@__ROUTES = {}
 
  class ParameterError < ArgumentError
  end
 
  module Helpers
    def json( d )
      @response['Content-Type'] = 'application/json'
      d.to_json
    end
 
    def redirect( klass, *a )
      [302, {'Location' => URL(klass, *a)}, '']
    end
 
    def URL( klass, *a )
      path = nil
      a = a.delete_if{ |x| x.nil? }
 
      if klass.class == Class
        Capcode.routes.each do |p, k|
          path = p if k.class == klass
        end
      else
        path = klass
      end
 
      path+((a.size>0)?("/"+a.join("/")):(""))
    end
  end
 
  include Rack
 
  class HTTPError
    def initialize(app)
      @app = app
    end
 
    def call(env)
      status, headers, body = @app.call(env)
 
      if self.methods.include? "r#{status}"
        body = self.call "r#{status}", env['REQUEST_PATH']
        headers['Content-Length'] = body.length.to_s
      end
 
      [status, headers, body]
    end
  end
 
  class << self
    def Route *u
      Class.new {
        meta_def(:__urls__){
          h = {}
          max = 0
          u.each do |_u|
            m = /\/([^\/]*\(.*)/.match( _u )
            if m.nil?
              h[_u] = ''
            else
              h[m.pre_match] = m.captures[0]
              max = Regexp.new(m.captures[0]).number_of_captures if max < Regexp.new(m.captures[0]).number_of_captures
            end
          end
          [h, max, self]
        }
 
        def params
          @request.params
        end
 
        def env
          @env
        end
 
        def session
          @env['rack.session']
        end
 
        def request
          @request
        end
 
        def response
          @response
        end
 
        def call( e )
          @env = e
          @response = Rack::Response.new
          @request = Rack::Request.new(@env)
 
          r = case @env["REQUEST_METHOD"]
            when "GET"
              sc = @request.script_name
              sc = "/" if sc.size == 0
              regexp = Regexp.new( self.class.__urls__[0][sc] )
              nargs = self.class.__urls__[1]
 
              args = regexp.match( Rack::Utils.unescape(@request.path_info).gsub( /^\//, "" ) )
              if args.nil?
                raise Capcode::ParameterError, "Path info `#{@request.path_info}' does not match route regexp `#{regexp.source}'"
              else
                args = args.captures
              end
 
              while args.size < nargs
                args << nil
              end
 
              get( *args )
            when "POST"
              post
          end
          if r.respond_to?(:to_ary)
            @response.status = r[0]
            r[1].each do |k,v|
              @response[k] = v
            end
            @response.body = r[2]
          else
            @response.write r
          end
 
          @response.finish
        end
 
        include Capcode::Helpers
      }      
    end
 
    def map( r, &b )
      @@__ROUTES[r] = yield
    end
 
    def run( args = {} )
      conf = {
        :port => args[:port]||3000, 
        :host => args[:host]||"localhost",
        :server => args[:server]||nil,
        :log => args[:log]||$stdout,
        :session => args[:session]||{}
      }
 
      if conf[:server].nil? || conf[:server] == "mongrel"
        begin
          require 'mongrel'
          conf[:server] = "mongrel"
        rescue LoadError 
          puts "!! could not load mongrel. Falling back to webrick."
          conf[:server] = "webrick"
        end
      end
 
      Capcode.constants.each do |k|
        begin
          if eval "Capcode::#{k}.public_methods(true).include?( '__urls__' )"
            u, m, c = eval "Capcode::#{k}.__urls__"
            u.keys.each do |_u|
              @@__ROUTES[_u] = c.new
            end
          end
        rescue => e
          raise e.message
        end
      end
 
      app = Rack::URLMap.new(@@__ROUTES)
      app = Rack::Session::Cookie.new( app, conf[:session] )
      app = Capcode::HTTPError.new(app)
      app = Rack::ContentLength.new(app)
      app = Rack::Lint.new(app)
      app = Rack::ShowExceptions.new(app)
      app = Rack::CommonLogger.new( app, Logger.new(conf[:log]) )
 
 
      case conf[:server]
      when "mongrel"
        puts "** Starting Mongrel on #{conf[:host]}:#{conf[:port]}"
        Rack::Handler::Mongrel.run( app, {:Port => conf[:port], :Host => conf[:host]} ) { |server|
          trap "SIGINT", proc { server.stop }
        }
      when "webrick"
        puts "** Starting WEBrick on #{conf[:host]}:#{conf[:port]}"
        Rack::Handler::WEBrick.run( app, {:Port => conf[:port], :BindAddress => conf[:host]} ) { |server|
          trap "SIGINT", proc { server.shutdown }
        }
      end
    end
 
    def routes
      @@__ROUTES
    end
  end
end

Bon, je vous avais promis un blog… Voici donc un blog :

$:.unshift( "../lib" )
require 'rubygems'
require 'capcode'
require 'couch_foo'
 
CouchFoo::Base.set_database(:host => "http://localhost:5984", :database => "my_blog")
 
class Story < CouchFoo::Base
  property :title, String
  property :body, String
  property :date, String
end
 
module Capcode
  class HTTPError
    def r404(f)
      "Pas glop !!! #{f} est inconnu !!!"
    end
  end
 
  class Index < Route '/'
    def get
      r = "<html><body>"
 
      story = Story.find( :all )
 
      story.each do |s|
        r += "<h2>#{s.title}</h2><small>#{s.date} - <a href='#{URL( Remove, s.id, s.rev )}'>Delete this entry</a></small><p>#{s.body}</p>"
      end
 
      r+"<hr /><a href='#{URL(Add)}'>Add a new entry</a></body></html>"
    end
  end
 
  class Remove < Route '/remove/([^\/]*)/(.*)'
    def get( id, rev )
      Story.delete(id, rev)
      redirect( Index )
    end
  end
 
  class Add < Route '/add'
    def get
      '
        <html><body>
          <h1>Add a new entry</h1>
          <form method="POST">
            Titre : <input type="text" name="title"><br />
            <textarea name="body"></textarea><br />
            <input type="submit">
          </form>
        </body></html>
      '
    end
    def post
      Story.create( :title => params['title'], :body => params['body'], :date => Time.now.to_s )
      redirect( Index )
    end
  end
end
 
Capcode.run( :port => 3001, :host => "localhost", :log => "blog.log" )

A ben oui ! Avec CouchDB et couch_foo tout de vient plus simple ;)

Nous en avons terminé avec cette présentation de Rack. Je vais encore faire quelques améliorations dans Capcode, et ensuite je le mettrai à disposition. La prochaine fois que nous en parlerons, cela sera pour développer avec Cappuccino.

1 “à croire que certains seraient tentés d’en faire leur métier !” — Stephane S. in Je sers la science et c’est ma joie
2 Le blog c’est un peu le hello world des framework Web finalement !?
3 Je peux proposer tout ce que je veux, de toute façon vous ne pouvez pas répondre …
4 Sinon pourquoi s’embêter à mettre des règles ?
5 Je vous avais bien dit “comme un contrôleur” !!!
6 Encore !!!

Matz à Paris en avril ! (conférence OW2)

J’ai la joie de vous annoncer la venue de Matz à Paris, à l’occasion de la conférence OW2 début avril. Voici le communiqué d’OW2 à ce sujet.

Objet: Première conférence annuelle OW2

Les Logiciels Libres pour l’Infrastructure Informatique

PARTICIPEZ A LA PREMIERE CONFERENCE ANNUELLE OW2

Profitez gratuitement de deux jours de découverte des logiciels d’entreprise qui vous rendront libre et performant en participant à la première conférence annuelle OW2 les 1er et 2 Avril 2009, à Paris, dans le cadre du salon Solutions Linux.

Vous optimisez votre temps : En participant à la conférence annuelle OW2 vous pourrez tout à la fois visiter le Solutions Linux, découvrir les dernières avancées des grands logiciels d’OW2 et discuter librement avec les développeurs eux-mêmes.

Vous anticipez l’avenir : Venez découvrir les évolutions de Ruby avec Yukihiro Matsumoto, le concepteur du langage Ruby et de Java pour l’entreprise avec Roberto Chinnici, le “lead” de la spécification Java EE 6.

Vous vous enrichissez : Le programme de la conférence comprend plus de 30 présentations, des intervenants venant d’Europe, d’Asie et des Amériques, sept sessions thématiques, quatre sessions “poster” et, bien sûr, le concours du meilleur Cas d’Utilisation.

Et c’est gratuit : Grâce à nos sponsors, ANR, Bull, EBMWebsourcing, Engineering, eXo Platform, Ingres, INRIA, Orange Labs, and Xwiki, la participation à la conférence est gratuite et ouverte à tous !

Une seule condition, il faut vous enregistrer sur http://www.solutionslinux.fr en prenant soin de couvrir la partie « Cycle Spécifique OW2 ». Plus d’information sur le site de la Conférence Annuelle OW2 .

Fondée en janvier 2007 OW2 est une communauté indépendante de développeurs dédiée au développement de logiciels middleware en open source. Le Consortium OW2 héberge plus de cent Projets technologiques dont Acceleo, ASM, Bonita, eXo Platorm, Funambol, JOnAS, Lomboz, Orbeon Forms, PetALS, SpagoBI et XWiki. La présence internationale du Consortium est assurée par le moyen de ses Chapitres Locaux OW2 en Europe, en Chine et au Brésil. Plus d’information sur: www.ow2.org.

Matz à Paris en avril ! (conférence OW2)

J’ai la joie de vous annoncer la venue de Matz à Paris, à l’occasion de la conférence OW2 début avril. Voici le communiqué d’OW2 à ce sujet.

Objet: Première conférence annuelle OW2

Les Logiciels Libres pour l’Infrastructure Informatique

PARTICIPEZ A LA PREMIERE CONFERENCE ANNUELLE OW2

Profitez gratuitement de deux jours de découverte des logiciels d’entreprise qui vous rendront libre et performant en participant à la première conférence annuelle OW2 les 1er et 2 Avril 2009, à Paris, dans le cadre du salon Solutions Linux.

Vous optimisez votre temps : En participant à la conférence annuelle OW2 vous pourrez tout à la fois visiter le Solutions Linux, découvrir les dernières avancées des grands logiciels d’OW2 et discuter librement avec les développeurs eux-mêmes.

Vous anticipez l’avenir : Venez découvrir les évolutions de Ruby avec Yukihiro Matsumoto, le concepteur du langage Ruby et de Java pour l’entreprise avec Roberto Chinnici, le “lead” de la spécification Java EE 6.

Vous vous enrichissez : Le programme de la conférence comprend plus de 30 présentations, des intervenants venant d’Europe, d’Asie et des Amériques, sept sessions thématiques, quatre sessions “poster” et, bien sûr, le concours du meilleur Cas d’Utilisation.

Et c’est gratuit : Grâce à nos sponsors, ANR, Bull, EBMWebsourcing, Engineering, eXo Platform, Ingres, INRIA, Orange Labs, and Xwiki, la participation à la conférence est gratuite et ouverte à tous !

Une seule condition, il faut vous enregistrer sur http://www.solutionslinux.fr en prenant soin de couvrir la partie « Cycle Spécifique OW2 ». Plus d’information sur le site de la Conférence Annuelle OW2 .

Fondée en janvier 2007 OW2 est une communauté indépendante de développeurs dédiée au développement de logiciels middleware en open source. Le Consortium OW2 héberge plus de cent Projets technologiques dont Acceleo, ASM, Bonita, eXo Platorm, Funambol, JOnAS, Lomboz, Orbeon Forms, PetALS, SpagoBI et XWiki. La présence internationale du Consortium est assurée par le moyen de ses Chapitres Locaux OW2 en Europe, en Chine et au Brésil. Plus d’information sur: www.ow2.org.

Créer son microframework avec Rack (Partie 1)

projetsDepuis que je développe exclusivement avec Cappuccino pour créer des applications Web, je me suis trouvé face à un dilemme de taille. En effet, si Cappuccino gère remarquablement la partie interface, il n’en reste pas moins qu’il faut faire un choix pour la partie serveur.

Pour cela n’importe quel langage fera l’affaire. Mais ma préférence allant vers Ruby, je ne vais parler que de ce dernier…

Bien entendu il y Rails ou Merb (permettez-moi de les mettre dans le même panier…). Mais c’est peut-être un peu overkill. En effet, avec Cappuccino, nous voulons simplement gérer des requêtes JSON, et à la rigueur pouvoir accéder à une base de données… Bref des choses élémentaires que Rails et Merb savent parfaitement gérer, et même très bien, mais c’est un peu réducteur comme travail pour de si gros frameworks. Ayant abandonné Bivouac je ne vous le proposerai pas, mais Camping peut-être une solution. En effet, ce framework est petit, simple à mettre en place, bref c’est un peu comme le candidat idéal. Un peu car lui aussi ne me satisfait pas. Premièrement parce que depuis qu’il a été quelque peu délaissé par _why, il avance lentement. Nous sommes toujours avec la version 1.5 qui date d’octobre 2006 et on nous promet une v2 depuis pas mal de temps maintenant. Ensuite parce qu’il s’agit d’un framework qui a été fait pour créer des applications web classiques et qui lui aussi propose des solutions qui vont au-delà de notre besoin.

Après de très courtes recherches, j’ai trouvé mon salut avec Rack. Rack n’est pas un framework, mais il est une base idéale pour en fabriquer un (Camping par exemple l’utilise pour sa future version 2.0).

Dans ma quête de simplicité, je me suis donc amusé avec Rack. Je vous propose donc de développer notre propre micro framework, nom de code Capcode.

cape_codLa première chose à faire est bien entendu d’installer Rack. Le gem existant, il n’y a aucune surprise : sudo gem install rack

Commençons par un premier exemple histoire de comprendre comment Rack fonctionne. Pour cela, rappelons les principes de base. Le navigateur envoie une requête de type HTTP vers une URL donnée, cette URL comprenant un nom de serveur, un port et un chemin. Par exemple dans http://www.monsite.com:3000/hello, www.monsite.com est le nom du serveur, 3000 le port et /hello le chemin. Il faut donc que nous soyons capables d’intercepter cette requête et de renvoyer un résultat.

Rack arrive avec l’exécutable rackup qui va, pour le moment, gérer pour nous la partie serveur et port. En effet, si vous regarder l’aide de rackup vous verrez que nous pouvons, lors du lancement, lui spécifier le host (l’interface) sur laquelle il doit écouter (option -o), le port (option -p) mais également le serveur à utiliser (option -s), ce dernier choix se faisons entre WEBrick ou Mongrel. Vous remarquez enfin que rackup attend un fichier de configuration (avec l’extension .ru). C’est sur ce fichier que nous allons nous concentrer pour le moment.

Le fichier de configuration de rackup est en fait un fichier Ruby dont l’ensemble du code sera passé en paramètre à la méthode d’initialisation de la DSL Rack::Builder. En effet, la création d’une application avec Rack commence par la création d’un objet Rack::Builder prenant en paramètre un bloc dans lequel nous décrivons le comportement de l’application. Oublions cela pour le moment.

Notre premier exemple sera très simple1. Nous allons faire en sorte de pouvoir renvoyer l’heure quand l’utilisateur interroge notre application sur le chemin /time. Voici le contenu de notre fichier (exemple.ru)

1
2
3
4
5
6
7
use Rack::ContentLength
 
map '/time' do
  run lambda { |env| 
    [200, { 'Content-Type' => 'text/html' }, "Il est #{Time::now}"] 
  }
end

Nous utilisons ici trois mots clés de la DSL Rack::Builder :

  • use permet de charger un middleware. A la ligne 1 nous chargeons donc Rack::ContentLength qui va se charger de remplir l’information Content-Length dans l’entête de la réponse.
  • map permet de mapper un chemin. Ici (ligne 3) nous précisons que pour le chemin /time la construction de la réponse se fera en prenant le contenu du bloc passé en paramètre.
  • run enfin permet d’appeler la méthode qui construit la réponse. Dans notre exemple run prend en paramètre un objet de type Proc renvoyant un tableau dont le premier élément est le code HTTP de réponse, le second est un hachage contenant des infirmations pour l’entête de la réponse et le dernier est une chaîne pour le corps de la réponse.

Nous pouvons maintenant tester que tout cela fonctionne. Pour cela il suffit simplement d’exécuter la commande suivante :

rackup exemple.ru

Par défaut, rackup utilise le port 9292 et l’interface 0.0.0.0. Depuis notre navigateur préféré, nous pouvons nous connecter à l’adresse http://localhost:9292/time

Ô joie, nous savons maintenant quelle heure il est ;)

Si vous essayez de vous connecter à l’adresse http://localhost:9292 vous aurez le droit à un insolent message du type Not Found: /. C’est tout à fait normal, car nous n’avons pas géré le cas du chemin /.

Maintenant que nous savons mapper un chemin, nous pouvons envisager d’aller un peu plus loin. Cependant, même si Rack n’est pas un framwork, il offre tout de même quelques facilités. Prenons le cas ou nous voulons servir des fichiers. Nous voulons pas exemple que si l’on fait un appel du type http://localhost:9292/index.html, le contenu du fichier index.html soit renvoyé. Pour cela nous pouvons éviter de tout écrire à la main en utilisant le helper Rack::File. Voici comment nous modifions l’exemple précédent :

1
2
3
4
5
6
7
8
9
10
11
12
require 'rack/file'
use Rack::ContentLength
 
map '/' do
  run Rack::File.new( "." )
end
 
map '/time' do
  run lambda { |env| 
    [200, { 'Content-Type' => 'text/html' }, "Il est #{Time::now}"] 
  }
end

Vous le voyez, nous avons mappé le chemin / en passant à la méthode run un objet Rack::File. Lors de l’initialisation de ce dernier, nous avons passé en paramètre le chemin d’accès, sur le disque, au répertoire contenant les fichiers — dans le cas présent, le répertoire courant. Ajoutez un fichier index.html à côté de notre fichier de configuration, relancer rackup et testez en vous rendant à l’adresse http://localhost:9292/index.html. Vous noterez, et c’est heureux, qu’il n’y a aucun changement si vous allez à l’adresse http://localhost:9292/time

Jusqu’à maintenant, nous avons utilisé rackup. C’est une solution très facile à mettre en place et nous pourrions nous arrêter là. Finalement, au JSON prêt, nous voyons facilement comment rendre le service dont nous avons besoin (à savoir créer un backend pour une application Cappuccino). Cependant, nous pouvons aller plus loin. Tout d’abord, j’aimerai ne pas être obligé d’utiliser rackup. C’est vrai, si nous voulons rester proche de Ruby (non pas que nous nous en sommes éloignés) autant créer un vrai script Ruby. Nous allons donc voir comment refaire ce que nous venons de développer, mais en pur Ruby.

Pour cela nous allons devoir aller un peu plus loin dans la compréhension de Rack.

En choisissant de ne pas passer par un fichier de configuration rackup c’est à nous de mettre en place l’environnement via Rack::Builder. C’est relativement simple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require 'rubygems'
require 'rack'
 
app = Rack::Builder.new {
  use Rack::ContentLength
 
  map '/' do
    run Rack::File.new( "." )
  end
 
  map '/time' do
    run lambda { |env| 
      [200, { 'Content-Type' => 'text/html' }, "Il est #{Time::now}"] 
    }
  end
}

Comme vous pouvez le voir, ce code est presque identique à celui du fichier de configuration de rackup, la seule différence vient du fait que nous avons passé les différents mapping dans un bloc en paramètre du constructeur de Rack::Builder. En fait, ce n’est pas tout. En effet, pour le moment nous avons déclaré une application (app) mais il faut la faire porter par un serveur. Pour cela nous allons utiliser un handler. Rack propose plusieurs handler dont un pour Mongrel, WEBrick, Thin, FastCGI, … Faisons simple et utilisons ce qu’il y a de plus standard. Pour cela, il suffit d’ajouter la ligne suivante dans notre script :

17
Rack::Handler::WEBrick.run( app, {:Port => 9292} )

Voilà, vous pouvez maintenant exécuter cet exemple :

ruby exemple.rb

Vous constaterez que son comportement est exactement le même qu’avec rackup. Mais nous pouvons faire encore mieux. En effet, écrire toute l’intelligence de notre application dans un bloc n’est pas une solution très élégante. Qu’à cela ne tienne, nous allons faire autrement. Le principe est assez simple en fait. Tout comme Rack propose des helpers nous allons créer les nôtres.

Un helper Rack est une classe du module Rack dans laquelle nous avons besoin de créer une simple méthode call. Cette méthode prend un paramètre qui correspond à un hachage contenant l’ensemble des informations de la requête. Voici donc comment écrire le helper qui nous donne l’heure :

1
2
3
4
5
6
7
8
9
10
11
12
require 'rubygems'
require 'rack'
 
module Rack
  class MyTime
    def call( env )
      response = Rack::Response.new
      response.write "Il est #{Time::now}"
      response.finish
    end
  end
end

Comme vous pouvez le voir, dans notre méthode call nous utilisons un objet Rack::Response pour formater le corps de la réponse. Bien entendu, nous pouvons également utiliser ce même objet pour ajouter des informations dans l’entête, …

Pour utiliser ce helper, et le helper Rack::File, nous allons créer un hachage dont les clés correspondent aux chemins d’accès et les valeurs sont les objets helper correspondant :

13
14
15
16
routes = {
  '/time' => Rack::MyTime.new,
  '/' => Rack::File.new( "." )
}

Pour terminer, il faut créer une application à partie de ce hachage. Pour cela nous utilisons la classe Rack::URLMap en lui passant en paramètre le hachage:

17
app = Rack::URLMap.new(routes)

Dans les exemples précédents nous avons utilisé Rack::ContentLength pour faciliter la mise en place de la valeur du Content-Length dans les entêtes de réponses. Nous pouvons faire de même :

18
app = Rack::ContentLength.new(app)

Enfin, nous terminons par passer l’application au serveur :

19
Rack::Handler::WEBrick.run( app, {:Port => 9292} )

Sauvegardez, tester… Aucune surprise !

Maintenant que nous connaissons les bases de Rack — et nous n’en utiliserons pas beaucoup plus — voyons comment créer notre micoframework…

Pour cela je vais partir de la fin… A savoir vous montrer un exemple de ce à quoi nous souhaitons2 aboutir3 :

require 'rubygems'
require 'capcode'
 
module Capcode
  class Hello < Route '/hello/(.*)'
    def get( r )
      "Hello #{r} it's #{Time.now} !"
    end
  end
 
  class Js < Route '/hello/json/(.*)'
    def get( r )
      json( { :who => r, :time => Time.now } )
    end
  end
end
 
Capcode.map( "/files" ) do
  Rack::File.new( "." )
end
 
Capcode.run( :port => 3001, :host => "localhost" )

La partie la plus intéressante ici est très certainement la réalisation de méthode qui va permettre la création des routes. Avant de nous y attaquer, voyons de quoi nous avons besoin. Si vous comptez bien, vous verrez que nous avons seulement besoin de 4 méthodes : Capcode.map, Capcode.run, json et Route. Mais prenons les choses dans l’ordre.

Comme vous pouvez le voir, une application Capcode est décrite dans le module Capcode. En fait, nous ne faisons là que reproduire ce que nous avons fait avec le dernier exemple avec Rack. En effet, pour réécrire le même exemple en utilisant un module MonModule (à la place de du module Rack), il suffit simplement de créer le module MonModule et d’un inclure le module Rack. Donc nous savons déjà que nous allons devoir lamentablement plagier Rack pour créer Capcode…

Chaque contrôleur4 est décrit dans une classe héritant de Route à qui nous passons le chemin d’accès. Vous vous en doutiez déjà, mais si Route prend un paramètre, c’est donc une méthode. En fait, pour être exacte c’est une méthode de classe de Capcode.

module Capcode
  include Rack
 
  class << self
    def Route *u
      # ...
    end
  end
end

Dans route, il faut non seulement faire quelque chose avec u mais également renvoyer une classe. Cette dernière étape est relativement simple. En effet, il suffit de créer cette classe et de la renvoyer :

module Capcode
  include Rack
 
  class << self
    def Route *u
      Class.new {
        # ...
      }
    end
  end
end

Nous pouvons y ajouter toutes les méthodes nécessaires. Nous avons vu que les classes applicatives de Rack ont toutes une méthode call. Or dans Capcode nous avons une méthode get (et post mais je n’en parlerai pas pour le moment). Vous l’avez certainement compris, mais en fait c’est la classe héritée de Route qui possède la méthode call et c’est cette dernière qui appelle la méthode get

module Capcode
  include Rack
 
  class << self
    def Route *u
      Class.new {
        # ...
 
        def call(e) 
          @env = e
          @response = Rack::Response.new
          @request = Rack::Request.new(@env)
 
          @case @env["REQUEST_METHOD"]
            when "GET"
              @response.write get
            when "POST"
              @response.write post
          end
          @response.finish
        end
 
        # ...
      }
    end
  end
end

Histoire de nous faciliter une peu la tache, j’ai ajouté la ligne @request = Rack::Request.new(@env) dans la méthode call afin de nous permettre de retrouver les données de la requête. Pour plus de détails je vous engage à regarder la documentation de Rack::Request.

J’ai également ajouté quelques petites méthodes qui, si elles n’ont pas une grande utilité, permettent de rendre les choses plus belles :

module Capcode
  include Rack
 
  class << self
    def Route *u
      Class.new {
        # ...
 
        def params
          @request.params
        end
 
        def env
          @env
        end
 
        def request
          @request
        end
 
        def response
          @response
        end
 
        def call(e) 
          @env = e
          @response = Rack::Response.new
          @request = Rack::Request.new(@env)
 
          @case @env["REQUEST_METHOD"]
            when "GET"
              @response.write get
            when "POST"
              @response.write post
          end
          @response.finish
        end
 
        # ...
      }
    end
  end
end

Et si nous faisions quelque chose avec ce paramètre u.

Nous avons besoin de stocker les routes passées en paramètre à Route. Pour cela nous allons faire un peu de meta-programmation en utilisant la méthode Object.meta_def de _why. En fait, nous allons créer dans la classe renvoyée par Route une méthode de classe : __urls__. Quand cette méthode est appelée, elle renvoie un tableau contenant les informations relatives aux routes de la classe. De telle sorte que si nous déclarons une méthode de la façon suivante :

module Capcode
  class Glop < Route '/glop/(.*)', '/glop/code/(.*)'
    def get
      # ...
    end
  end
end

Quand nous appelons Glog.__urls__ nous obtenons le résultat suivant :

[{"/glop/code"=>"(.*)", "/glop"=>"(.*)"}, Capcode::Glop]

Et nous en finissons donc, pour le moment, avec la méthode Route :

module Capcode
  include Rack
 
  class << self
    def Route *u
      Class.new {
        meta_def(:__urls__){
          h = {}
          u.each do |_u|
            m = /\/([^\/]*\(.*)/.match( _u )
            if m.nil?
              h[_u] = ''
            else
              h[m.pre_match] = m.captures[0]
            end
          end
          [h, self]
        }
 
        def params
          @request.params
        end
 
        def env
          @env
        end
 
        def request
          @request
        end
 
        def response
          @response
        end
 
        def call(e) 
          @env = e
          @response = Rack::Response.new
          @request = Rack::Request.new(@env)
 
          @case @env["REQUEST_METHOD"]
            when "GET"
              @response.write get
            when "POST"
              @response.write post
          end
          @response.finish
        end
 
        # ...
      }
    end
  end
end

La méthode json est en fait un helper que nous aurions tout aussi bien pu mettre dans la classe renvoyée par Route. Oui mais cela ne serait pas drôle. De plus, il est intéressant de lui garder son côté helper, car, en effet, nous pourrions très facilement faire autrement. Anyway5 ! Nous allons donc créer un module Capcode::Helpers dans lequel nous mettrons cette méthode :

module Capcode
  module Helpers
    def json( d )
      @response['Content-Type'] = 'application/json'
      d.to_json
    end
  end
 
  include Rack
 
  class << self
    def Route *u
      # ...
    end
  end
end

Comme vous pouvez le voir, cette méthode se contente de positionner la valeur du Content-Type dans l’entête de la réponse et de transformer ce qui est passé en paramètre au format JSON puis de renvoyer le tout.

Il faut maintenant prendre en compte le module Capcode::Helpers. Rien de plus simple. Il suffit de rajouter une include Capcode::Helpers à la fin de la création de la classe renvoyée par Route :

module Capcode
  module Helpers
    def json( d )
      @response['Content-Type'] = 'application/json'
      d.to_json
    end
  end
 
  include Rack
 
  class << self
    def Route *u
      Class.new {
        meta_def(:__urls__){
          h = {}
          u.each do |_u|
            m = /\/([^\/]*\(.*)/.match( _u )
            if m.nil?
              h[_u] = ''
            else
              h[m.pre_match] = m.captures[0]
            end
          end
          [h, self]
        }
 
        def params
          @request.params
        end
 
        def env
          @env
        end
 
        def request
          @request
        end
 
        def response
          @response
        end
 
        def call(e) 
          @env = e
          @response = Rack::Response.new
          @request = Rack::Request.new(@env)
 
          @case @env["REQUEST_METHOD"]
            when "GET"
              @response.write get
            when "POST"
              @response.write post
          end
          @response.finish
        end
 
        include Capcode::Helpers
      }
    end
  end
end

La méthode map est beaucoup plus simple. En fait, ce que nous voulons faire, c’est compléter un hachage comme nous l’avons fait plus haut. Souvenez-vous de ceci :

routes = {
  '/time' => Rack::MyTime.new,
  '/' => Rack::File.new( "." )
}

Et bien dans Capcode, nous allons faire la même chose, à la différence que notre hachage sera ici une variable de classe du module Capcode :

module Capcode
  module Helpers
    def json( d )
      ...
    end
  end
 
  include Rack
  @@__ROUTES = {}
 
  class << self
    def Route *u
      ...
    end
 
    def map( r, &b )
      @@__ROUTES[r] = yield
    end
  end
end

Terminons avec la méthode run. Dans cette méthode nous avons besoin de mettre en place l’application. Pour cela nous avons besoins de retrouver toutes les routes, les classes qui s’y rattachent, puis de mettre à jour le hachage @@__ROUTES qu’il suffira ensuite de passer au constructeur de Rack::URLMap. Pour retrouver l’ensemble les routes d’une classe, nous avons la méthode __urls__. Mais il faut au préalable retrouver les classes. Sachant qu’en Ruby une classe est avant tout une constante, il suffit d’utiliser la méthode Module::constants :

module Capcode
  module Helpers
    def json( d )
      ...
    end
  end
 
  include Rack
  @@__ROUTES = {}
 
  class << self
    def Route *u
      ...
    end
 
    def map( r, &b )
      ...
    end
 
    def run( args )
      # ...
 
      Capcode.constants.each do |k|
        begin
          if eval "Capcode::#{k}.public_methods(true).include?( '__urls__' )"
            u, c = eval "Capcode::#{k}.__urls__"
            u.keys.each do |_u|
              @@__ROUTES[_u] = c.new
            end
          end
        rescue => e
          raise e.message
        end
      end
 
      # ...
    end
  end
end

Dans ce code, nous parcourons toutes les constantes du module Capcode. Pour chacune nous vérifions si elle possède une méthode __urls__. Si c’est le cas, nous appelons cette méthode et nous mettons à jour le hachage @@__ROUTES.

Il ne reste plus qu’à créer l’application avec Rack::URLMap comme nous l’avons fait précédemment. Voici donc le code complet de Capcode (capcode.rb). Je n’entre pas plus dans les détails de la méthode run c’est sensiblement la même chose que ce que nous avons fait plus haut.

require 'rubygems'
require 'rack'
require 'json'
require 'logger'
 
class Object #:nodoc:
  def meta_def(m,&b) #:nodoc:
    (class<<self;self end).send(:define_method,m,&b)
  end
end
 
module Capcode
  module Helpers
    def json( d )
      @response['Content-Type'] = 'application/json'
      d.to_json
    end
  end
 
  include Rack
 
  CAPCOD_VERION="0.1.0"
  @@__ROUTES = {}
 
  class << self
    def map( r, &b )
      @@__ROUTES[r] = yield
    end
 
    def Route *u
      Class.new {
        meta_def(:__urls__){
          h = {}
          u.each do |_u|
            m = /\/([^\/]*\(.*)/.match( _u )
            if m.nil?
              h[_u] = ''
            else
              h[m.pre_match] = m.captures[0]
            end
          end
          [h, self]
        }
 
        def params
          @request.params
        end
 
        def env
          @env
        end
 
        def request
          @request
        end
 
        def response
          @response
        end
 
        def call( e ) #:nodoc:
          @env = e
          @response = Rack::Response.new
          @request = Rack::Request.new(@env)
 
          case @env["REQUEST_METHOD"]
            when "GET"
              @response.write get
            when "POST"
              @response.write post
          end
          @response.finish
        end
 
        include Capcode::Helpers
      }      
    end
 
    def run( args = {} )
      conf = {
        :port => args[:port]||3000, 
        :host => args[:host]||"localhost",
        :server => args[:server]||nil,
        :log => args[:log]||$stdout,
        :session => args[:session]||{}
      }
 
      # Check that mongrel exists 
      if conf[:server].nil? || conf[:server] == "mongrel"
        begin
          require 'mongrel'
          conf[:server] = "mongrel"
        rescue LoadError 
          puts "!! could not load mongrel. Falling back to webrick."
          conf[:server] = "webrick"
        end
      end
 
      Capcode.constants.each do |k|
        begin
          if eval "Capcode::#{k}.public_methods(true).include?( '__urls__' )"
            u, c = eval "Capcode::#{k}.__urls__"
            u.keys.each do |_u|
              @@__ROUTES[_u] = c.new
            end
          end
        rescue => e
          raise e.message
        end
      end
 
      app = Rack::URLMap.new(@@__ROUTES)
      app = Rack::Session::Cookie.new( app, conf[:session] )
      app = Rack::ContentLength.new(app)
      app = Rack::Lint.new(app)
      app = Rack::ShowExceptions.new(app)
      app = Rack::Reloader.new(app) ## -- NE RELOAD QUE capcode.rb -- So !!!
      app = Rack::CommonLogger.new( app, Logger.new(conf[:log]) )
 
 
      case conf[:server]
      when "mongrel"
        puts "** Starting Mongrel on #{conf[:host]}:#{conf[:port]}"
        Rack::Handler::Mongrel.run( app, {:Port => conf[:port], :Host => conf[:host]} ) { |server|
          trap "SIGINT", proc { server.stop }
        }
      when "webrick"
        puts "** Starting WEBrick on #{conf[:host]}:#{conf[:port]}"
        Rack::Handler::WEBrick.run( app, {:Port => conf[:port], :BindAddress => conf[:host]} ) { |server|
          trap "SIGINT", proc { server.shutdown }
        }
      end
    end
  end
end

Si vous y regardez de près, nous ne sommes pas exactement conformes avec ce que nous avons énoncé dans l’exemple de départ. Disons simplement que c’est une v1 et que vous allez attendre patiemment le prochain épisode

1 les premiers exemples sont toujours simple…
2 Si, si, vous souhaitez
3 comment ça vachement inspiré de Camping ?
4 ça y est, j’ai fini par le dire !
5 en Québécois dans le texte.