Le web en Crystal

Publié le 27 août 2020 par Hugo Fabre | back - framework

Cet article est publié sous licence CC BY-NC-SA

Crystal, web et Lucky (une intro basique)

Depuis un moment j’ai envie de tester le langage Crystal. Celui-ci s’approchant de sa version 1.0, je me suis dit qu’il était temps de regarder ce qu’il était possible de faire dans le domaine du web.

Après quelques recherches, trois frameworks sortent aujourd’hui du lot :

  • Kemal Un framework Sinatra-like pour crystal
  • Amber Un framework qui semble très inspiré de Rails
  • Lucky Un framework qui semble avoir des inspirations plus variées.

Pour ma part j’ai choisi de faire mes premiers pas dans le web en Crystal avec le framework Lucky. Plusieurs raisons m’ont amené à faire ce choix :

  • C’est un framework complet, on devrait donc arriver à un POC rapidement
  • Il est développé par Paul Smith un ancien développeur chez Thoughtbot, une société très active dans l’environnement Ruby on Rails et Elixir
  • Les promesses mises en avant sur son site sont alléchantes

En effet en arrivant sur la page d’accueil on peut y lire :

Lucky is a web framework written in Crystal. It helps you work quickly, catch bugs at compile time, and deliver blazing fast responses.

(NdT : Lucky est un framework web écrit en Crystal. Il vous aidera à avancer rapidement, repérer la plupart des bugs durant la phase compilation et servira des réponses étonnamment rapides)

Quoi de mieux pour nous donner envie. Si vous n’êtes toujours pas convaincu, je vous invite à lire la page d’introduction dédiée beaucoup plus complète que la mienne mais en anglais uniquement.

Installation

Pour une question de simplicité je vous invite à suivre les instructions détaillées fournies par la documentation. Il faudra également prévoir une installation fonctionnelle de PostgreSQL en local ou via Docker.

Démarrons le projet

Pour toutes informations complémentaires je vous invite à lire la documentation sur laquelle je m’appuie pour cette partie.

Le projet en question sera une API web toute simple : Des utilisateurs et une table centralisée de bookmarks dans laquelle les utilisateurs pourront enregistrer des liens et aller les retrouver plus tard.
On commence donc par créer le projet via le CLI Lucky :

lucky init

On suit le wizard qui nous propose différentes configurations par défaut :

Project name?: bookmarks
API only or full support for HTML and Webpack? (api/full): api
Generate authentication? (y/n): y

On configure ensuite notre base de données via le fichier config/database.cr. Le format est assez simple et la configuration de base satisfaisante dans la plupart des situations. De mon côté, la configuration par défaut était suffisante pour que je n’aie qu’à lancer un container PostgreSQL :

docker run --rm --name postgres -p 5432:5432 -e POSTGRES_PASSWORD="postgres" -d postgres

On note dans le fichier les références à Avram qui est tout simplement l’ORM de Lucky.

Il faut ensuite lancer le script de mise en place :

script/setup

Et enfin si tout s’est bien déroulé on peut démarrer notre application

lucky dev

Voilà le résultat avec un appel

curl --request GET --url http://localhost:5000/
{
  "hello": "Hello World from Home::Index"
}

On explore

Authentification

Le modèle User et le système d’authentification ayant déjà été créés par le wizard nous allons voir comment utiliser ce qui a été généré. Pour créer un utilisateur :

curl --request POST \
  --url http://localhost:5000/api/sign_ups \
  --header 'content-type: application/json' \
  --data '{
	"user": {
		"email":"test@example.org",
		"password":"password",
		"password_confirmation": "password"
	}
}'

Vous devriez avoir une réponse contenant le jeton d’authentification :

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.y12ClIjF7Mk8NLa1VwHO0MhsrUtpvEIti4PwkjuYnLs"
}

Et pour générer un autre jeton d d’authentification il suffit de se connecter :

curl --request POST \
  --url http://localhost:5000/api/sign_ins \
  --header 'content-type: application/json' \
  --data '{
	"user": {
		"email": "test@example.org",
		"password": "password"
	}
}'

Et nos marque-pages alors ?

Nous allons nous attaquer au Bookmark. Pour simplifier le processus Lucky, tout comme Rails, nous propose des générateurs :

lucky gen.model Bookmark

Nous avons quatre fichiers qui ont étés générés par cette tâche, deux assez classiques :

  • Le fichier de migration
  • Le fichier du modèle

Et enfin deux qui sortent un peu de l’ordinaire pour un développeur Rails :

  • Un fichier d’opération
  • Un fichier de requête

Pour comprendre ce que sont ces fichiers et à quoi ils servent il faut se plonger un peu dans la philosophie de Lucky, qui comme d’autres frameworks a fait le choix de découpler au plus possible la logique métier d’une application de ces différentes entrées/sorties (en général dans le cadre du web on parle de requêtes HTTP et de bases de données). Ces deux fichiers sont donc liés directement à l’ORM (Avram) plutôt qu’au framework web (Lucky). Ici on a donc un fichier d’opération, qui contiendra les opérations nécessitant d’écrire en base (création et mise à jour) et un fichier de requête dans lequel nous pourrons écrire les différentes requêtes (SQL ici) liées à notre modèle en lecture uniquement.

Pour nos Bookmarks, voilà la migration à écrire (comme toujours, pour plus d’information sur les migrations je vous invite à lire la documentation dédiée :

# db/migrations/20200713134247_create_bookmarks.cr
class CreateBookmarks::V20200713134247 < Avram::Migrator::Migration::V1
  def migrate
    # Learn about migrations at: https://luckyframework.org/guides/database/migrations
    create table_for(Bookmark) do
      primary_key id : Int64
      add link : String, unique: true
      add description : String?
      add_timestamps
    end
  end

  def rollback
    drop table_for(Bookmark)
  end
end

Pour information le type String? vient de Crystal et représente une chaine de caractères nullable. Ici ça nous permettra donc de spécifier à la base de données qu’on accepte la valeur null dans ce champs.

Sans oublier de signaler ces attributs à notre nouveau modèle (à priori pas d’inférence possible ici) :

# src/models/bookmark.cr
class Bookmark < BaseModel
  table do
    column link : String
    column description : String?
  end
end

Nous pouvons maintenant jouer la migration et relancer le serveur

lucky db.migrate && lucky dev

Pour pouvoir manipuler nos marque-pages, il va falloir passer par le concept d’action. En Lucky une action est l’équivalent d’une route et de son action dans le contrôleur. La route elle-même est inférée depuis le nom de la classe de l’action si on la nomme selon les conventions du framework (applicable uniquement pour les actions REST) (documentation). Encore une fois nous avons un générateur à disposition :

lucky gen.action.api Api::Bookmarks::Index

Et dans notre action :

# src/actions/api/bookmarks/index.cr
class Api::Bookmarks::Index < ApiAction
  route do
    # BookmarkQuery.new est un raccourci pour lire tous les Bookmarks, voir https://luckyframework.org/guides/database/querying-records#select-shortcuts
    # BaseSerializer.for_collection est un raccourci pour sérialiser une collection, voir https://luckyframework.org/guides/json-and-apis/rendering-json#rendering-a-collection-with-serializers
    json(BookmarkSerializer.for_collection(BookmarkQuery.new))
  end
end

Pour finir nous devons définir notre sérialiseur :

# src/serializers/bookmark_serializer.cr
class BookmarkSerializer < BaseSerializer
  def initialize(@bookmark : Bookmark)
  end

  def render
    {link: @bookmark.link, description: @bookmark.description}
  end
end

Toutes les routes étant authentifiées par défaut on n’oublie pas son jeton et ça donne :

# Il faudra bien sur changer le jeton pour le vôtre
curl --request GET \
  --url http://localhost:5000/api/bookmarks \
  --header 'authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.y12ClIjF7Mk8NLa1VwHO0MhsrUtpvEIti4PwkjuYnLs'

Et on reçoit bien un tableau vide parce qu’on ne sait pas encore créer des marque-pages.

[]

Que le marque-page soit

Comme pour l’index, on passe par le générateur :

lucky gen.action.api Api::Bookmarks::Create

Et voilà le code :

class Api::Bookmarks::Create < ApiAction
  route do
    # SaveModel est l'opération créée par défaut à la création d'un modèle. 
    # Il suffit ensuite d'en hériter pour customiser, voir: https://luckyframework.org/guides/database/validating-saving
    bookmark = SaveBookmark.create!(params)
    json(BookmarkSerializer.new(bookmark))
  end
end

Et enfin pour tester on crée un marque-page :

# Encore une fois on change son jeton !
curl --request POST \
  --url http://localhost:5000/api/bookmarks \
  --header 'authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.y12ClIjF7Mk8NLa1VwHO0MhsrUtpvEIti4PwkjuYnLs' \
  --header 'content-type: application/json' \
  --data '{
  "bookmark": {
    "link": "https://synbioz.com",
    "description": "Synbioz"
  }
}'

Et on peut relancer notre requête d’index pour vérifier que tout fonctionne :

# C'est la dernière fois que je le dis, on oublie pas de changer son jeton !
curl --request GET \
  --url http://localhost:5000/api/bookmarks \
  --header 'authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.y12ClIjF7Mk8NLa1VwHO0MhsrUtpvEIti4PwkjuYnLs'

Et on reçoit bien :

[
  {
    "link": "https://synbioz.com",
    "description": "Synbioz"
  }
]

Et le marque-page fut (conclusion)

Globalement même si j’ai eu un peu de mal à me mettre dedans, je me suis rapidement fait aux différents concepts mis en avant par le framework. Je suis agréablement surpris par l’outillage mis à disposition (lucky help) depuis le CLI et la marche d’approche qui finalement n’est pas si grande (entre autres grâce à Crystal et sa syntaxe proche du Ruby, mais pas que !). C’était attendu aussi mais les temps de réponses m’ont semblé plus que correct : sur les différentes requêtes que j’ai lancées je suis resté entre 2 et 20 millisecondes. Après il faut bien-sûr relativiser il n’y a rien de vraiment complexe dans notre application. Je voudrais aussi donner un bon point à la documentation qui, même si elle n’est pas au niveau des guides Rails, est plus que correcte et largement suffisante pour débuter (je me suis servi uniquement de celle-ci pour écrire cet article).

En revanche le point négatif inhérent à Crystal, la compilation. À la moindre commande il faut compiler. C’est un peu frustrant quand on vient de Ruby, je ne sais pas du tout comment est géré l’outillage web autour des autres langages compilés à la mode (Go, Rust, …) mais c’est quand même énervant de devoir attendre la compilation sur un simple lucky routes (équivalent de rails routes). Pour relativiser, j’imagine quand même que sur un projet d’envergure, la différence doit être moins flagrante vu que Rails prend aussi beaucoup de temps à charger.
J’ai aussi trouvé le processus d’installation un peu compliqué, mais au final je pense que quelqu’un qui découvre Ruby aujourd’hui dira la même chose, c’est une question d’habitude.

De mon côté je vais prendre le temps de continuer cette petite application pour la rendre plus complexe et pour mieux tester les différentes facettes de Lucky, et quand je serai plus à l’aise j’espère revenir vers vous avec un nouvel article plus poussé et différents exemples de points positifs/négatifs.


L’équipe Synbioz.
Libres d’être ensemble.