Article écrit par Hugo Fabre
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 wizzard 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.