đŸ’Ÿ Archived View for heyplzlookat.me â€ș articles â€ș mehari-0-3.gmi captured on 2024-08-18 at 17:25:26. Gemini links have been rewritten to link to archived content

View Raw

More Information

âŹ…ïž Previous capture (2024-03-21)

-=-=-=-=-=-=-

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 :

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 :

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 :

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` !

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 :

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 :

Ⅰ 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 :

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