TD 11 - Mustache
Moteur de modélisation Mustache
Mustache est un moteur de modélisation performant qui permet de séparer un modèle des données qui y sont représentées. Une documentation plus complète se trouve ici
1. Un moteur de modélisation simple
Pour l’instant, on va considérer un modèle simplifié. Les variables seront contenues dans un fichier JSON, que l’on parsera avec ce qu’on a fait au TD 8.
Rappel du modèle
Pour rappel, on considère que le JSON parsé sera représenté par le modèle suivant :
On va aussi considérer qu’un modèle (un Template
) sera une liste
d’éléments, qui pourront être soit des variables (à remplacer dans le
document final), soit du texte simple.
Une variable est constituée d’un nom (donc une chaîne de caractères), du texte sera juste une chaîne de caractères également.
On a donc le modèle suivant :
Parser le modèle - version simple
Pour parser les variables, il faut qu’on utilise notre parseur. Les règles pour les variables sont qu’elles sont représentées par un nom (une chaîne de caractères) compris dans une paire de doubles accolades.
On aura alors par exemple :
Ici, on va utiliser les variables nom
et age
.
Ce qui nous donne le parseur suivant :
Pour parser du texte, on peut juste estimer qu’on va parser n’importe quel caractère excepté des accolades.
On peut donc faire une version simple comme :
Récupérer le modèle complet consiste donc à simplement répéter plusieurs fois la reconnaissance de variables ou de texte, soit :
Instancier le modèle
Maintenant qu’on sait récupérer un modèle, on va s’attacher à remplacer les variables par leurs valeurs.
On va vouloir transformer les valeurs JSON en chaînes de caractères. Pour ça, on va écrire une petite fonction utile pour se simplifier le travail plus tard.
Pour instancier un modèle, il faut pouvoir instancier un élément. Dans le cas des éléments de type texte, c’est facile. Pour les variables, il va falloir aller regarder dans notre environnement (un objet JSON) et récupérer la valeur de la variable.
On va encore une fois s’écrire une fonction utile pour faire le travail de recherche.
La fonction getValue
va prendre en argument un objet JSON et une
chaîne de caractères (la clé), et va renvoyer la valeur de la clé.
Ici, puisque les objets dans notre modèle JSON sont des listes
associatives, on va utiliser la fonction
lookup :: Eq a => a -> [(a, b)] -> Maybe b
, qui fonctionne
comme ceci :
On a donc simplement :
On va donc pouvoir instancier un élément facilement maintenant.
La fonction instElement
va être une fonction qui va prendre
un environnement JSON, un élément et qui va renvoyer une chaîne de
caractères. Elle échouera cependant si la clé d’une variable n’est
pas présente dans l’environnement.
Heureusement, on travaille avec Maybe
, qui est un foncteur
applicatif ! On peut donc tout simplement écrire :
L’instanciation d’un modèle complet est un peu plus technique.
On va vouloir instancier chaque élément, et concaténer les résultats.
Si on map
l’instanciation des éléments, on aura une liste de
type [Maybe String]
. Il faudra alors sortir le Maybe
de la liste
et concaténer les résultats.
Une autre solution est simplement de faire un pli de la liste. On commence avec une chaîne vide et on replie, en concaténant au fur et à mesure.
Pour chaque élément qu’on a, on va donc devoir l’instancier, et concaténer avec l’accumulateur.
Ce qui nous donne :
On utilise ici liftA2
pour concaténer le contenu des deux Maybe
(l’accumulateur et l’élément).
2. Ajouter des fonctionnalités
On va ajouter quelques petites fonctionnalités pour rendre notre moteur de modélisation encore plus utile et pratique.
Utiliser des variables sous la forme d’objets
Souvent dans les objets JSON, on a des objets imbriqués. Par exemple,
on pourrait avoir une variable personne
constituée comme ceci :
Dans ce cas, on va vouloir accéder au champ prenom
en utilisant
la notation personne.prenom
et à son code postal avec
personne.adresse.cp
.
Pour ça, on a deux solutions :
- soit, comme c’est le cas jusqu’à présent, on est capable de parser des noms de variables qui contiennent des points, et dans ce cas, il faut séparer la chaîne de caractères en liste de clés successives ;
- soit on modifie le modèle pour que la clé d’une variable soit
une liste de chaînes et on change le parseur pour reconnaître
le motif de l’expression régulière suivante :
"\s+(\.\s+)*"
(c’est-à-dire une chaîne, suivie d’une répétition d’un point puis d’une chaîne).
On va se concentrer sur la seconde solution ici.
Il faut donc changer le modèle, qui devient alors :
Ensuite, il va falloir changer notre fonction getValue
, qui
va chercher la valeur d’une clé dans l’objet.
Maintenant, il faut que l’on ne renvoie un objet que si la liste
n’a plus qu’un seul élément.
Si on en a plus, il faut qu’on aille récursivement chercher les clés
dans les objets imbriqués.
Sur l’objet défini ci-dessus (qu’on va appeler obj
), on aurait alors :
et ça échouerait toujours dans les cas où les clés ne sont pas trouvées etc.
Le code de la fonction getValue
devient alors :
Dans ce cas, comme le nom de variable est déjà une liste, le code
de la fonction instElement
ne change pas.
Faire des sections conditionnelles
Un des autres avantages de Mustache, c’est qu’on peut décider d’afficher ou non du contenu en fonction de la valeur d’une variable booléenne. On va ajouter cette fonctionnalité à notre moteur de templating.
Par exemple, si on considère le modèle suivant :
Alors, si la clé valide
vaut true
, on aura :
et sinon :
Comme on peut le voir, les définitions de section ressemblent
fortement aux définitions de variables.
Cependant, le nom de la section est précédé par un #
au début
et par un /
à la fin.
À l’intérieur d’une section, on peut trouver un modèle complet.
On peut donc modifier le modèle pour qu’il reconnaisse également les sections de la manière suivante :
On peut ensuite écrire un parseur qui reconnaît une section :
Ce parseur peut sembler compliqué, mais la seule difficulté réside dans le fait de vérifier à la fin que le nom de fermeture de section est le même qu’à l’ouverture.
On peut ensuite simplement modifier la fonction instElement
pour
qu’elle instancie correctement une section.
Répéter du contenu
L’autre avantage des sections, c’est que si elles référencent une valeur sous la forme d’une liste, on peut les utiliser pour répéter du contenu.
Par exemple, si on a :
Alors on peut écrire facilement une liste avec le modèle suivant (ici au format HTML) :
Qui renverrait :
Le format des sections n’a pas changé par rapport à tout à l’heure. On peut donc conserver tout ce qu’on a écrit.
Par contre, il va falloir se méfier au moment de modifier la fonction
instElement
.
On va commencer par instancier chaque élément de la liste.
Si on applique juste un map
à la liste des éléments instanciés,
on se retrouve avec une liste de Maybe String
.
Il faut donc que l’on puisse sortir de la liste le Maybe
, de
manière à pouvoir concaténer les résultats et à obtenir une
seule chaîne de caractères.
Pour cela, on peut utiliser l’opérateur sequence
, qui peut
s’implémenter comme ceci :
On peut ensuite intégrer ceci dans notre code :
On a maintenant un modèle de modélisation plus complet !
3. Implémenter l’application
Un exemple de structure d’application est disponible ici