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 :
1 # routes.yml
2
3 "/hello": { via: "get", to: "hello#index" }
Charger le contenu d’un fichier au format YAML demande peu de travail :
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) :
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 :
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».
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:
1 # config.ru
2
3 require_relative "application"
4 run Application.new
Lancez l’application avec rackup et testez plusieurs routes.
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 :
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» :
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
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 :
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 :
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à !
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.
# 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:
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 :
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
1 <!-- hello.html -->
2 <h1>Un grand bonjour depuis mon extraordinaire application</h1>
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.