Mehari 0.3

La nouvelle mise-à-jour évènement

Mehari 0.3 est sorti et aujourd’hui nous allons discuter de la prochaine version : Mehari 0.4 (nom de code: prostate lovers)

Pour ceux qui ont cliqué pour la version 0.3, le changelog complet est disponible ci-dessous :

https://github.com/Psi-Prod/Mehari/releases/tag/0.3

N’hésitez pas à faire vos remarques sur l’API de Mehari en commentaire.

Dans l’assimilation parfaite

Pondre une API bien lissée est probablement ce que je préfère faire en programmation (c’est notamment pour cette raison que j’aime OCaml). Pour rendre dépendant des utilisateurs de ma seule volonté et percer dans le OCaml jeu, il vaut mieux écrire des bibliothèques aussi composables que possible. Si le meilleur des pythonista se mettait à écrire une bibliothèque OCaml, il produirait assurément quelque chose de convenable mais pas très agréable à utiliser. Et c’est bien normal car il ne connaît ni les idiomes du langage, ni son écosystème.

Venons-en au point, je n’aime pas trop la façon dont les paramètres de route sont gérés dans Mehari. Je m’explique :

ocaml
let my_route =
  Mehari_lwt_unix.route "/echo/(.*)" ~regex:true (fun req ->
    Mehari.param req 1 |> Mehari.respond_text);

Ici, on définit une route qui match tous les chemins qui commencent par `echo` et on renvoie le reste du chemin en utilisant `Mehari.param`.

Normalement, un truc a dû vous piquer les yeux. Les regex sont écrites en clair au lieu d’utiliser un DSL pur OCaml typesafe ! Et oui, il existe une super librairie – sobrement nommé `ocaml-re` (🗿) – qui permet d’écrire des regex de manière encore plus incompréhensible :

ocaml
Re.(seq [ str "foo="; group (rep1 any) ]) <=> "foo=(.+)

https://github.com/ocaml/ocaml-re

On va donc remplacer le paramètre labellisé booléen `regex` par quelque chose de plus puissant pour choisir si l’on souhaite :

Pour ce faire, deux options s’offrent à nous :

Utiliser un GADT signifierait devoir préciser un paramètre labellisé pour chaque type de route, même lorsqu’on veut gérer une route triviale comme `/articles`, ce qui n’est pas le cas actuellement dans Mehari. En effet, placer un constructeur d’un GADT en tant qu’argument par défaut conduit à fixer le type de ce dernier et interdit logiquement d’utiliser des constructeurs d’un type différent :

ocaml
type _ t = Int : int t | String : string t

let print (type a) ?(typ=Int) (x : a) =
let _ = print ~typ:String ""
                   ^^^^^^^^^
Error: This expression has type string t but an expression was expected of type
         int t
       Type string is not compatible with type int
Si vous êtes intéressé par les GADT, j’en ai déjà parlé moins brièvement dans cet article :

/articles/gadt-mehari.gmi

Que faire

Dans les deux cas présentés, ce serait un breaking change. Mais qui utilise Mehari pour faire tourner son serveur Gemini, sérieusement ?

Explication du design

Maintenant que le sujet est sur la table, je voulais m’expliquer concernant le choix de supporter directement des regex plutôt que d’imposer notre propre système comme Dream le fait. D’un côté, Dream est plus lisible avec ses paramètres nommés (`/foo/:bar`) mais d’un autre côté, les expressions régulières sont plus puissantes, bien que plus « austères ». À cela s’ajoute un argument non négligeable : on n’avait pas envie de se faire chier.

Le cas de Mehari.param

En bidouillant un peu, je me suis aperçu que la sémantique de Mehari.param est assez déplorable. En effet, cette fonction lève la même exception si l’index est négatif et si la paramètre demandé n’est pas présent dans la route. Ci-dessous un extrait de la doc :

Mehari.param req n retrieves the n-th path parameter of req.
Raise Invalid_argument if n is not a positive integer
Raise Invalid_argument if path does not contain any parameters in which case the program is buggy.

C’est donc sémantiquement impossible de distinguer une mauvaise utilisation de la fonction d’une erreur de programmation, même en matchant sur le string contenu dans le constructeur `Invalid_argument` !

ocaml
try Mehari.param req 1 with Invalid_argument msg when String.starts_with ~prefix:"xxx" -> raise "JE SUIS COMPLÈTEMENT FOU 😹"

On va donc prochainement lever `Not_found` lorsque la route ne contiendra aucun paramètre récupérable. Enfin on va peut-être ajouter une fonction équivalente qui renverra soit un `string option` en levant `Invalid_argument`, soit un `(string, [ `NotFound | `NegativeInteger]) result`. Je ne sais pas encore quelle signature est la mieux, dites-moi.

Formater les esprits

Après m’avoir laissé pour le moins perplexe pendant longtemps, j’ai découvert que le module Format était une fesserie. Je me suis ainsi convaincu qu’utiliser cette machinerie dans la fonction `Gemtext.to_string` pouvait potentiellement être une bonne idée.

Je me suis heureusement rendu compte qu’utiliser Format a la fâcheuse tendance de ne plus faire de la fonction `Gemtext.of_string` une bijection réciproque de `Gemtext.to_string` (car Format implémente le soft-wrap des lignes trop longues).

Pour me pardonner de mon impertinence, voici une recette pour obtenir un rendu Gemtext lisible qui rend hommage au beau, au juste et au vrai :

ocaml
let pp_line ppf =
  let open Format in
  function
  | Text t -> pp_print_text ppf t
  | Link { url; name } ->
      fprintf ppf "@[<hov 2>⇒ %s%a@]" url (pp_print_option (fun ppf ->
        fprintf ppf "@ %a" pp_print_text)) name
  | Preformat { alt; text } ->
      fprintf ppf "@[<v>%a@ %s@]" (pp_print_option pp_print_text) alt text
  | Heading (h, t) ->
    let hchar = match h with `H1 -> "" | `H2 -> "" | `H3 -> "" in
    fprintf ppf "@[<hov 2>%s %a@]" hchar pp_print_text t
  | ListItem t ->
    fprintf ppf "@[<hov 2>– %a@]" pp_print_text t
  | Quote t ->
    let out_funs = Format.pp_get_formatter_out_functions ppf () in
    pp_set_formatter_out_functions ppf { out_funs with out_indent = (fun _ -> fprintf ppf "")};
    fprintf ppf "@[<hov 0>█ %a@]" pp_print_text t;
    Format.pp_set_formatter_out_functions ppf out_funs

let pp = Format.pp_print_list ~pp_sep:Format.pp_force_newline pp_line

let gemtext =
  Mehari.Gemtext.[
    heading `H1 "Salut la compagnie";
    newline;
    link "https://heyplzlookat.me/" ~name:"Un super site";
    newline;
    heading `H2 "Passions";
    newline;
    text "Liste des passions :";
    list_item "le langage de prog OCaml";
    list_item "Marx";
    newline;
    quote "De très bonnes passions"
  ]

let () =
  Format.set_margin 20; (* Max 20 caractères par ligne *)
  Format.printf "%a%!" pp gemtext

Imprégnez-vous maintenant d’esthétique, misérables misomuses :

Sortie du programme ci-dessus
Ⅰ Salut la
  compagnie

⇒ https://heyplzlookat.me/
  Un super site

Ⅱ Passions

Liste des
passions :
– le langage de
  prog OCaml
– Marx

█ De très bonnes
█ passions

En vrac

Mehari_mirage.static

Pour encore mieux intégrer mehari-mirage, j’aimerais bien ajouter une fonction pour servir statiquement des fichiers depuis une expression de type `Mirage_kv.RO`.

Un paquet Gemtext

Il faudrait que l’on factorise le module Gemtext de Mehari dans un paquet tierce car on l’utilise également dans Razzia, notre client Gemini secret non publié sur Opam en raison du manque de motivation pour écrire sa documentation (mais qui reste malgré tout utilisable). De plus, cela nous permettrait de pouvoir tester le parser convenablement.

https://github.com/Psi-Prod/Razzia

Dernier recourt

En y repensant, je me disais qu’il faudrait pouvoir personnaliser le comportement en cas de not found avec un handler même si c’est techniquement réalisable avec ce hack :

ocaml
Mehari_lwt_unix.router [
;
  Mehari_lwt_unix.route "/(.*)" ~regex:true (fun _ ->
    Mehari.response not_found "not found !")
]

Retour au gemlog
Retour à l’accueil

Commentaires (0)

Pas encore de commentaires

Écrire un commentaire