2 - Une route + un contrôleur + une vue = une application

Une route

Le premier composant du framework dont je voudrais discuter est le routage, routing en anglais.

Le rôle du routage sera d’aiguiller les requêtes venant de l’extérieur vers la bonne méthode de traitement, contenue quelque part dans l’application.

Toutes les routes de nos applications seront contenues dans le fichier routes.yml.

Il y a de nombreuses manières de spécifier une route, ainsi que de nombreuses techniques possibles pour charger les routes en mémoire. Dans ce livre, comme dans mon travail au quotidien, dès que je le peux je fais le choix de la simplicité. C’est pourquoi j’ai préféré utiliser un fichier de configuration au format YAML, plutôt que de définir un DSL qui aurait eu pour conséquence de trop compliquer les choses dès le début de ce livre. Mais nous reviendrons sur un DSL dans les derniers chapitres.

Voici le contenu de notre premier fichier routes.yml, avec la définition d’une seule route :

Votre première route
1 # routes.yml
2 
3 "/hello": { via: "get", to: "hello#index" }

Charger le contenu d’un fichier au format YAML demande peu de travail :

Charger le fichier routes.yml avec Ruby
require 'yaml'
YAML.load_file("routes.yml")
#=> {
#=>     "/hello" => {
#=>         "via" => "get",
#=>          "to" => "hello#index"
#=>     }
#=> }

Nous récupérons donc un Hash, dont la clé principale est le chemin de la requête. La valeur est elle-même un Hash, avec une première clé "via" qui contient le verbe HTTP de la requête, et une seconde clé "to" qui contient sous une forme un peu cryptique la classe et la méthode qui effectuera le traitement voulu. Nous verrons plus loin la signification réelle de ce "hello#index".

En français une telle route signifie «route le chemin /hello, qui vient du verbe http GET, vers la méthode index».

Notre application va charger toutes les routes lors de son initialisation (même si pour l’instant il n’y en a qu’une) :

En route pour une nouvelle application
1 # application.rb
2 require 'yaml'
3 
4 class Application
5 
6   def initialize
7     @routes = YAML.load_file("routes.yml")
8   end
9 end

Lançons notre application depuis une session irb pour nous assurer qu’elle fonctionne. Notez que l’objet app qui s’affiche automatiquement dans la console contient une représentation compréhensible du Hash @routes :

Voir l’objet au sein d’une session irb
require "./application"
#=> true
Application.new
#=> #<Application:0x00559939fa2a30 @routes={"/hello"=>{"via"=>"get",
#=>                                                    "to"=>"hello#index"}}>

J’ai rabâché plusieurs fois dans le chapitre précédent qu’une application Rack devait avoir une méthode call. Notre classe Application se doit donc de répondre à call. Profitons en pour tester la reconnaissance des routes. Si nous envoyons une requête avec un chemin correspondant à une route nous répondons avec «Ce chemin existe». Si au contraire la requête ne correspond à aucun chemin nous afficherons «Ce chemin n’existe pas».

On ajoute le call qui va bien
 1 # application.rb
 2 require 'yaml'
 3 
 4 class Application
 5 
 6   def initialize
 7     @routes = YAML.load_file("routes.yml")
 8   end
 9 
10   def call(env)
11     if @routes[env["REQUEST_PATH"]]
12       [200, {}, ["Ce chemin existe"]]
13     else
14       [200, {}, ["Ce chemin n'existe pas"]]
15     end
16   end
17 end

Notre application comporte désormais trois fichiers.

$ tree code/ch02/route/
├── application.rb
├── config.ru
└── routes.yml

Le config.ru est réduit à sa plus simple expression. Il se contente de charger le fichier application.rb et de lancer l’application Rack:

En avant !
1 # config.ru
2 
3 require_relative "application"
4 run Application.new

Lancez l’application avec rackup et testez plusieurs routes.

Cette route existe
Cette route existe
Celle ci n'existe pas
Celle ci n’existe pas

Un contrôleur

Nous venons de voir qu’une route devait viser une méthode précise dans notre framework. Plus particulièrement la route définit ci-dessus vise quelque chose que nous avons écrit ainsi : "hello#index". La partie avant le # est le nom d’un contrôleur, tandis que la partie après le # est le nom d’une méthode de ce contrôleur.

Le rôle d’un contrôleur sera de fabriquer la réponse attendue par Rack, à savoir un Array avec code de retour, entêtes et corps.

Sans plus attendre, voici un contrôleur minimal pour notre route :

Un contrôleur minimal, mais alors vraiment minimal
1 # hello_controller.rb
2 
3 class HelloController
4   def index
5     [200, {}, ["Coucou !"]]
6   end
7 end

Vous pouvez constater que le code de ce contrôleur est terriblement simple. Nous avons quand même une convention à l’œuvre ici : un contrôleur “foo” est définit dans une classe FooController. Dans ce contrôleur nous retrouvons bien la méthode index qui est visée par la route "hello#index". Et le code de cette méthode index doit vous sembler familier après avoir lu le chapitre précédent sur Rack.

Nous modifions notre application pour y inclure ce contrôleur. Nous chargeons la classe HelloController avec la ligne require_relative 'hello_controller' et nous modifions le contenu de la méthode call pour qu’elle utilise le contrôleur si une bonne route existe. Si le chemin de la requête ne correspond à aucune route nous exécutons fail et rendons le message d’erreur un peu plus explicite en affichant «No matching route» :

Utilisons ce fameux contrôleur
 1 # application.rb
 2 require 'yaml'
 3 require_relative 'hello_controller'
 4 
 5 class Application
 6 
 7   def initialize
 8     @routes = YAML.load_file("routes.yml")
 9   end
10 
11   def call(env)
12     if route_exists?(env["REQUEST_PATH"])
13       HelloController.new.index
14     else
15       fail "Pas de route correspondante"
16     end
17   end
18 
19   private
20 
21   def route_exists?(path)
22     @routes[path]
23   end
24 end

Nous voici désormais avec un total de quatre fichiers.

$ tree code/ch02/controller/
├── application.rb
├── config.ru
├── hello_controller.rb
└── routes.yml
Quand la route existe
Quand la route existe
Quand la route n'existe pas
Quand la route n’existe pas

Une vue

Le prochain composant que nous allons introduire sera la vue. C’est ce qui nous intéresse généralement en tant qu’utilisateur du Web.

Pour l’instant nous définirons une vue comme étant ce qui s’affiche dans le navigateur.

Voici un contenu simplifié pour la vue hello.html. Ce n’est pas un vrai fichier HTML, complet et tout et tout, mais les navigateurs seront quand même content de l’afficher :

Soyons grandiloquent !
1 <!-- hello.html -->
2 <h1>Un grand bonjour depuis mon extraordinaire application</h1>

Nous devons transformer une vue en une chaîne de caractère, la méthode Ruby File.read est parfaite pour cela :

Lire la vue avec File.read
1 # hello_controller.rb
2 class HelloController
3   def index
4     [200, {}, [File.read("hello.html")]]
5   end
6 end

Avec ce nouveau fichier hello.html nous comptons maintenant cinq fichiers.

$ tree code/ch02/vue/
├── application.rb
├── config.ru
├── hello_controller.rb
├── hello.html
└── routes.yml

Et voilà !

C'est gros, c'est beau
C’est gros, c’est beau

Faire la conversion d’une vue en une chaîne de caractère directement dans la méthode index du contrôleur est ce qu’il y a de plus simple. C’est pourquoi j’ai présenté les choses de cette manière jusqu’ici. Mais ça n’est pas forcement le plus intelligent à faire. Nous avons définit le rôle du contrôleur comme étant celui de façonner la réponse attendue par Rack. Mais nous n’avons pas dit que tout devait obligatoirement se dérouler dans le contrôleur. Nous n’avons pas interdit non plus au contrôleur de se faire aider. Isoler les responsabilités rendra notre code plus facile à tester, à modifier et à raisonner. Voici donc un code plus étoffé, faisant appel à une classe Renderer.

Utilisons une classe pour faire le rendu
# hello_controller.rb
class HelloController
  def index
    status, body = Renderer.new("hello.html").render
    [status, {}, [body]]
  end
end

# renderer.rb
class Renderer
  def initialize(filename)
    @filename = filename
  end

  def render
    if File.exists?(@filename)
      [200, File.read(@filename)]
    else
      [500, "<h1>500</h1><p>No such template: #{@filename}</p>"]
    end
  end
end

Le rendu d’un fichier est ainsi testable de manière isolée, sans avoir à charger ou utiliser un contrôleur dont le nombre de fonctionnalité ne manquera pas de croître. Testons ce renderer dans une session irb:

Testons la classe Renderer dans irb
require "./renderer"
Renderer.new("hello.html").render
#=> [
#=>   200,
#=>   "<h1>Hello from an amazing web application!</h1>\n"
#=> ]
Renderer.new("unknown").render
#=> [
#=>   500,
#=>   "<h1>500</h1><p>No such template: unknown</p>"
#=> ]

Vous aurez remarqué que le code de HelloController#index s’est un peu compliqué. Nous verrons comment le rendre beaucoup plus digeste dans le prochain chapitre.

Une application

Nous voici à la fin de ce chapitre avec une application étalée sur six fichiers. Le code est suffisamment court pour que je puisse me permettre de les reproduire ici en entier :

Le code Ruby de l’application
 1 # application.rb
 2 require 'yaml'
 3 require_relative 'hello_controller'
 4 require_relative 'renderer'
 5 
 6 class Application
 7 
 8   def initialize
 9     @routes = YAML.load_file("routes.yml")
10   end
11 
12   def call(env)
13     if route_exists?(env["REQUEST_PATH"])
14       HelloController.new.index
15     else
16       fail "No matching routes"
17     end
18   end
19 
20   private
21 
22   def route_exists?(path)
23     @routes[path]
24   end
25 end
26 
27 # config.ru
28 require_relative 'application'
29 run Application.new
30 
31 
32 # hello_controller.rb
33 class HelloController
34   def index
35     status, body = Renderer.new("hello.html").render
36     [status, {}, [body]]
37   end
38 end
39 
40 
41 # renderer.rb
42 class Renderer
43   def initialize(filename)
44     @filename = filename
45   end
46 
47   def render
48     if File.exists?(@filename)
49       [200, File.read(@filename)]
50     else
51       [500, "<h1>500</h1><p>No such template: #{@filename}</p>"]
52     end
53   end
54 end
Le code HTML de l’application
1 <!-- hello.html -->
2 <h1>Un grand bonjour depuis mon extraordinaire application</h1>
Le code YAML de l’application
1 # routes.yml
2 "/hello": { via: "get", to: "hello#index" }

Nous ne pouvons pas encore parler de framework. Il s’agit d’un programme. En parlant de ça, il serait bon de définir ce qu’est un framework. La première phrase sur Wikipédia France est une bonne introduction. Je vous invite à lire l’article entier, ainsi que son homologue en anglais.

En programmation informatique, un framework ou structure logicielle est un ensemble cohérent de composants logiciels structurels, qui sert à créer les fondations ainsi que les grandes lignes de tout ou d’une partie d’un logiciel (architecture).
Wikipédia

Une autre définition que j’aime bien, plus générale et qui peut tout à fait s’appliquer à d’autres domaines que la programmation, est celle du MacMillan Dictionary :

a structure that supports something and makes it a particular shape
MacMillan dictionary

Dans le prochain chapitre, nous allons généraliser ce que nous avons appris jusqu’ici pour en faire un framework.