À la découverte de Sinatra #2 : routes et templates

Deuxième article de ma série "à la découverte de Sinatra". Dans le premier article, À la découverte de Sinatra #1, nous avons vu que Sinatra était un framework Ruby simple, léger et avec une communauté conséquente, destiné à faciliter la création d'applications web de taille modeste. Puis nous avons mis en place un projet et affiché une première page. Un bon début, mais je sens que vous étiez restés sur votre faim !

Aujourd'hui, nous irons un peu plus loin. Nous allons enrichir notre (jusque-là) toute petite application et créer un site qui affichera une liste de chansons de Frank Sinatra. Un clic sur le titre de la chanson permettra d'afficher ses paroles, qui seront pour le moment stockées dans de simples fichiers texte. Nous utiliserons HAML comme langage de template : c'est un moteur de template très répandu, que je ne connaissais pas encore. Ce sera l'occasion de le découvrir ensemble !

Tout le code correspondant à ce second article sera disponible sur mon dépôt github pabuisson/blog-decouverte-sinatra déjà utilisé dans le billet précédent, avec le tag partie-2 (oui, c'est d'une folle originalité :)). Pour le récupérer et lancer le projet :

1
2
3
$ git clone git@github.com:pabuisson/blog-decouverte-sinatra.git
$ git checkout -f partie-2
$ cd blog-decouverte-sinatra && bundle && ruby app.rb

Préparation : le Gemfile

Allons-y ! Tout d'abord, notre Gemfile :

1
2
3
4
5
# --- Gemfile ---
source 'https://rubygems.org'

gem 'sinatra'
gem 'haml'

Rien de bien complexe jusque-là, ça doit aller. On ajoute simplement le gem haml. Après ça, lancez la traditionnelle commande $ bundle pour installer les gems, et c'est parti !

Les templates avec HAML

J'ai plutôt l'habitude d'utiliser le langage de template Slim, mais pour ce projet, j'ai eu envie de tester HAML, très répandu dans le monde Ruby et qui me semble proche de Slim dans sa philosophie (quoique très légèrement plus verbeux). Nous avons d'ores et déjà ajouté la gem haml dans notre gemfile.

require "haml"

Vous vous rappelez sans doute que nos routes sont définies dans un fichier application (dans notre cas, app.rb à la racine du projet). C'est là que l'on va décrire le comportement de l'application lorsqu'un utilisateur va accéder à une URL. Pour le moment, l'accès à une route impliquera simplement l'affichage d'une vue, que l'on déclenche avec la syntaxe haml :nom_de_la_vue. Il est donc nécessaire d'ajouter un require "haml" au début du fichier app.rb pour que Ruby comprenne de quoi il s'agit lorsqu'il rencontrera cette instruction haml.

Sinatra : le bloc configure et settings.views

Même si Sinatra n'impose aucune contrainte quant à l'organisation des fichiers, j'ai l'habitude de regrouper le contenu de mon application dans un répertoire app à la racine du projet, à la mode Rails : je mets ainsi les vues dans un répertoire app/views. Or par défaut, Sinatra recherche les vues dans le répertoire /views. Houston, we've got a problem !

Avec Sinatra, vous pouvez regrouper toutes les options de configuration dans un bloc configure dédié à cet effet, lu une fois au démarrage de l'application. En l'occurrence, nous allons ajouter ce bloc de configuration au début de notre fichier app.rb. J'en profite aussi pour créer un setting avec le chemin du répertoire où je stockerai les fichiers contenant les paroles :

1
2
3
4
5
configure do
  # Accessibles ensuite via settings.views et settings.lyrics
  set :views,  File.join(settings.root, "app", "views")
  set :lyrics, File.join(settings.root, "app", "data")
end

À noter que vous pouvez utiliser des blocs différents pour les différents contextes dans lesquels va s'exécuter votre application : en règle générale, j'ai ainsi deux blocs de configuration différents, l'un pour le développement et l'autre pour la production :

1
2
3
4
5
6
7
configure :development do
    # ...
end

configure :production do
    # ...
end

Un layout pour les gouverner tous

Nous avons déjà indiqué à Sinatra d'utiliser HAML comme langage de template. HAML est un moteur de template qui vise à alléger le code de présentation, et à le rendre facile à lire. Pour faciliter la compréhension du code HAML qui suit, vous pouvez aller jeter un œil au tutorial HAML ou à la documentation HAML complète.

Généralement, vous voudrez créer un layout de base qui sera repris par toutes les pages de votre application : par convention, ce fichier doit être placé à la racine du répertoire settings.views (vous vous rappelez que nous l'avons tout juste modifié, c'est désormais le répertoire /app/views pour nous) et devra être nommé layout.erb, layout.haml, layout.slim, selon le langage de template utilisé : layout.haml dans notre cas donc !

En l'occurrence, pour notre petit projet, j'ai créé le layout suivant, rien que de très basique :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
!!! 5
%html
  %head
    %meta{charset: "utf-8"}
    %link{rel: "stylesheet", type: "text/css", href: "/stylesheets/app.css"}

  %body
    %section#content
      = yield

    %footer
      %a{href: "https://blog.pabuisson.com/2014/07/a-la-decouverte-de-sinatra-2-routes-et-templates/"}
        À la découverte de Sinatra #2
      par Pierre-Adrien Buisson

      %br

      Lyrics from
      %a{href: "http://metrolyrics.com", title: "Metro Lyrics"}
        Metro Lyrics

Vous pouvez remarquer l'instruction yield : lorsque vous accéderez à une page utilisant ce layout, c'est là que sera injecté le contenu de la page. Un exemple : si vous chargez le template login.haml, c'est le contenu de ce fichier sera injecté à cet emplacement.

Variables et utilisation dans le template

Notre layout est désormais prêt, il ne nous reste plus qu'à créer les routes dont nous allons avoir besoin. Il va nous falloir simplement 2 pages : l'une affichera la liste des chansons, l'autre les paroles d'une chanson. Nous allons donc créer deux routes :

1
2
3
4
5
6
7
8
9
get '/' do
  ...
end

# Correspond aux URL "/song/my_way", "/song/fly_me_to_the_moon" etc.
# On accède ensuite au paramètre :title avec le hash params
get '/song/:title' do
  ...
end
  1. get '/' est une route tout ce qu'il y a de plus simple : si une requête GET est reçue pour la racine du site, on affiche le template index (fichier /app/views/index.haml).
  2. get '/song/:title' est à peine plus complexe : vous voyez qu'elle comporte un paramètre :title. Elle interceptera tous les accès aux URL de la forme /song/quelquechose. Le paramètre quelquechose sera alors accessible via params[:title], et nous chargerons les paroles de la chanson pour les afficher à l'utilisateur dans le template song.haml.

Dans la route /, je vais charger la liste des chansons existantes pour l'afficher à l'utilisateur. Pour faire ça, je vais lister tous les fichiers texte présents dans le répertoire settings.lyrics, et les enregistrer dans un tableau. Mais comment faire pour que cette variable soit accessible depuis mon template, pour affichage ? Rien de plus simple : il faut les définir comme variable d'instance, en les préfixant avec @. Le résultat est par exemple le suivant pour le chargement de la liste de fichiers et l'affichage dans le template :

1
2
3
4
5
6
7
8
get '/' do
  # On liste les fichiers présents dans le répertoire settings.lyrics :
  # chaque fichier correspond à un titre de chanson
  lyrics_path = File.join(settings.lyrics, "*.txt")
  @lyrics = Dir.glob(lyrics_path)

  haml :index
end
1
2
3
4
5
6
7
8
9
10
11
12
13
/ On n'affiche la liste des paroles que s'il y a effectivement des
/ paroles dans le tableau @lyrics. 1 item du tableau = 1 titre
- unless @lyrics.empty?
  %ul#songlist
    / Pour chaque titre du tableau @lyrics, on affiche un lien
    / vers la page /song/titre_de_la_chanson
    - @lyrics.each do |l|
      / Le titre est le nom de fichier auquel on enlève les
      / 4 derniers caractères
      - title = File.basename(l)[0..-5]
      %li
        %a{href: "song/#{title.url_encode}"}
          = title

Je vous laisse regarder le code source complet pour en savoir plus ;)

Routing et erreurs 404

Erreur 404 : Road not found

Mais que va-t-il se passer si l'utilisateur essaie d'accéder à une chanson qui n'existe pas : par exemple, si j'essaie d'accéder manuellement à l'URL /song/my_favorite_game ? Dans ce cas, je vais essayer de charger un fichier qui n'existe pas. Si l'on ne fait rien, ça ne va pas bien se passer !

Erreur : je charge un fichier inexistant, et Ruby n'aime pas ça

Je vais donc créer un template "erreur 404" vers lequel je redirigerai moi-même l'utilisateur si je me rends compte qu'une chanson n'existe pas. Pour ça, il faut procéder en 3 étapes. Tout d'abord, crétons une route pour gérer les erreurs 404 : ça tombe bien, Sinatra fournit un moyen rapide de faire ça !

1
2
3
not_found do
  haml :err_404
end

Maintenant que la route est en place, il faut créer le template HAML. Celui-ci n'a rien de bien compliqué, il affiche simplement un lien pour revenir à la page d'accueil, et un message indiquant que la page n'existe pas. Vous pouvez être plus créatifs, par exemple en affichant une liste des liens proches de celui recherchés. Le but de ces pages d'erreur est d'aider l'utilisateur à trouver son chemin vers la ressource qu'il souhaitait atteindre au départ. Pour notre petit projet, une 404 simple fera l'affaire :

1
2
3
4
5
%a.back{href: "/", title: "Home"}
  %img{src: "/images/back.png", alt: "Retour"}

%h1 Erreur 404
%p Cette page n'existe pas !

Et enfin, dans le code de ma route /song/:title, je vais vérifier que le fichier existe bien, et si ce n'est pas le cas, je renverrai simplement une erreur 404 : pour ce faire, Sinatra met à notre disposition l'instruction halt :

1
2
3
4
5
6
if File.exists?(lyrics_path)
  @lyrics = readfile(lyrics_path)
  haml :song
else
  halt 404
end

Et voilà ! Si un utilisateur tente d'accéder à une chanson qui n'existe pas, il se verra afficher une jolie erreur 404.

Erreur 404 : c'est plus propre

Conclusion

Et voilà, nous avons désormais une petite application toute simple, qui affiche une liste de chansons, permet à l'utilisateur de cliquer dessus, et en affiche les paroles. S'il essaie d'accéder à une URL pour laquelle aucune route n'a été définie, ou aux paroles d'une chanson inexistante, l'application renvoie vers une page d'erreur 404. C'est un bon début !

Vous pouvez récupérer le code source de cet exemple que j'ai mis à disposition sur mon dépôt github pabuisson/blog-decouverte-sinatra :

1
2
3
$ git clone git@github.com:pabuisson/blog-decouverte-sinatra.git
$ git checkout -f partie-2
$ cd blog-decouverte-sinatra && bundle && ruby app.rb

Si vous avez le moindre problème ou la moindre question sur mon code ou l'article, n'hésitez pas à me le dire ! Tout feedback est également le bienvenu.

Références