戻る Retour au journal

Quand Ruby résout un vieux problème d'informatique ?

Imaginez un formulaire de site web où quelqu'un renseigne une date de naissance. Ce qui arrive au serveur, ce n'est jamais une « vraie » date : c'est une chaîne de caractères, du texte brut, du type "2024-01-01". Le programme doit la comprendre, vérifier qu'elle est valide, puis la transformer en un objet date qu'il peut comparer, trier, ou utiliser dans un calcul.

Jusque-là, rien d'original, n'importe quel langage sait « parser » une date. Mais que se passe-t-il quand ce même programme doit renvoyer cette date à un autre système, ou la ré-afficher dans un formulaire ? Il faut refaire le chemin inverse : reprendre l'objet, et le réécrire en texte, dans le bon format.

En apparence anodin, ce va-et-vient devient vite un casse-tête dès qu'on a affaire à des dizaines de champs différents (dates, identifiants uniques, adresses web, montants d'argent, valeurs vrai/faux…), et à plusieurs formats de destination (JSON pour une API, formulaire HTML, fichier CSV…). Dans la plupart des projets, chaque traduction, aller et retour, est écrite à la main, dispersée dans le code, et personne ne garantit qu'elles restent cohérentes entre elles au fil du temps.

ruby
# Sens 1 : le texte reçu devient une vraie date
date = Date.parse("2024-01-01")

# Sens 2 (ailleurs dans le code, parfois écrit par quelqu'un d'autre,
# des mois plus tard) : la date redevient du texte
texte = date.strftime("%Y-%m-%d")

Plumb est une bibliothèque Ruby conçue pour valider et transformer des données. Sa particularité est de traiter les règles de validation comme des petites pièces qu'on peut assembler entre elles, un peu comme des tuyaux qu'on raccorde (d'où son nom) : « accepte une chaîne de caractères », « puis vérifie que c'est bien un email », « puis transforme-la en majuscules », etc. Ces pièces s'enchaînent avec des opérateurs très simples : « et ensuite » (>>), « ou bien » (|).

Sur cette base, les auteurs de Plumb ont ajouté un mécanisme spécifiquement pensé pour le problème du va-et-vient décrit plus haut. Il repose sur deux idées.

La première : l'encodeur. Un encodeur, c'est une règle de traduction déclarée une seule fois, dans les deux sens. On lui dit : « voici comment lire ce format » et « voici comment l'écrire », et Plumb se charge du reste.

La seconde : le codec. Un codec, c'est un ensemble d'encodeurs qu'on applique d'un coup à toute une structure de données, un formulaire entier, un enregistrement complet avec vingt champs de types différents, plutôt qu'à un seul champ à la fois.

ruby
EncodeurDeDate = Plumb::Encoder.new(
  decode: ->(texte) { Date.parse(texte) },   # texte → date
  encode: ->(date)  { date.strftime("%Y-%m-%d") } # date → texte
)

Ce qui rend l'approche élégante, c'est que le développeur n'a presque jamais besoin de préciser « je veux lire » ou « je veux écrire ». Le système le déduit tout seul, en regardant ce qui se trouve juste avant dans la chaîne de traitement.

Prenons un exemple concret : une plage de dates (un « du » et un « au »), qui peut se présenter sous deux formes, un vrai intervalle de dates, ou une structure toute simple { de: ..., à: ... } où les dates sont encore du texte.

Si le texte arrive en premier dans la chaîne, Plumb comprend qu'il faut décoder : transformer le texte en dates utilisables. Si, à l'inverse, c'est l'intervalle de dates déjà construit qui arrive en premier, Plumb comprend qu'il faut faire l'inverse : encoder, c'est-à-dire retransformer les dates en texte. Une seule définition de la règle de traduction suffit à couvrir les deux cas, c'est le contexte qui décide, pas une option qu'il faudrait se souvenir de préciser à chaque fois.

ruby
# Le texte arrive en premier → Plumb décode
"2024-01-01" >> EncodeurDeDate
# => #<Date: 2024-01-01>

# La date arrive en premier → Plumb encode (sens inverse)
Date.parse("2024-01-01") >> EncodeurDeDate
# => "2024-01-01"

Traduire un seul champ, c'est utile. Mais les vraies données sont rarement aussi simples : un formulaire d'inscription, un enregistrement client, contiennent souvent une dizaine de champs de nature différente, parfois imbriqués les uns dans les autres (une adresse à l'intérieur d'une fiche client, elle-même dans une liste de commandes…).

C'est là qu'intervient le codec. Plutôt que d'appliquer un encodeur champ par champ, on décrit une fois pour toutes la structure complète attendue, « voici à quoi ressemble une fiche client, avec tel champ texte, telle date, tel montant », et on la confie au codec. Celui-ci parcourt automatiquement toute la structure, y compris ses parties imbriquées, et applique le bon encodeur à chaque champ qui en a besoin, en laissant intacts les champs qui n'ont pas besoin de traduction (un simple texte reste un simple texte).

Un détail de conception mérite d'être souligné : ce travail de repérage, quel encodeur va où, n'est fait qu'une seule fois, au démarrage du programme, et non à chaque donnée traitée. Une fois la structure « comprise », les traductions elles-mêmes ne coûtent quasiment rien : c'est une différence importante dans une application qui traite des milliers, voire des millions de messages par seconde.

Autre garantie appréciable : si un champ de la structure ne correspond à aucune règle de traduction connue, Plumb refuse de démarrer et signale précisément quel champ pose problème, plutôt que de laisser ce problème dormir jusqu'à ce qu'il provoque un plantage en production, au pire moment possible.

ruby
FicheClient = Plumb::Codec.new(
  nom:            String,
  date_naissance: EncodeurDeDate,
  budget:         EncodeurDeMontant
)

# Un seul appel traduit TOUTE la structure, texte → objets utilisables
FicheClient.decode(
  "nom" => "Alice", "date_naissance" => "1990-05-12", "budget" => "199.90"
)
# => { nom: "Alice", date_naissance: #<Date: 1990-05-12>, budget: #<BigDecimal: 199.90> }

La bibliothèque livre deux traducteurs prêts à l'emploi, qui illustrent bien à quel point les formats de données peuvent différer.

Le premier vise le JSON, format d'échange le plus courant du web. En JSON, beaucoup de types existent déjà nativement (texte, nombres, vrai/faux) : pas besoin de règle spéciale pour eux. En revanche, tout ce que JSON ne sait pas représenter nativement, une date, un identifiant unique, un montant d'argent avec des centimes exacts, reçoit une règle de traduction dédiée.

Le second vise les formulaires, les données qui arrivent, par exemple, quand on remplit et soumet un formulaire HTML. Ici, la situation est presque inversée : tout, absolument tout, arrive sous forme de texte, y compris ce qu'on pourrait croire être « déjà » un nombre ou une case cochée. Un nombre doit ressembler très précisément à une suite de chiffres, une case cochée est reconnue par des conventions comme "1" ou "true", et une chaîne vide est comprise comme « rien » (l'équivalent d'une case laissée vierge). Cette rigueur n'est pas un excès de prudence : elle reflète une réalité incontournable des formulaires, où tout ce qui arrive au serveur est, par nature, du texte brut, jamais un type déjà construit.

ruby
# Côté JSON : le nombre arrive déjà comme un nombre
CodecJSON.decode({ "age" => 34 })          # => { age: 34 }

# Côté formulaire : tout arrive en texte, y compris les nombres
CodecFormulaire.decode({ "age" => "34" })  # => { age: 34 }
CodecFormulaire.decode({ "age" => "" })    # => { age: nil }   (case vide = rien)
CodecFormulaire.decode({ "age" => "abc" }) # => erreur de validation

On pourrait croire qu'il s'agit d'un simple gain de confort pour les développeurs, ce qui serait déjà appréciable. Mais l'apport est plus profond : en détectant les incohérences avant que le programme ne tourne, plutôt qu'en cours de route, Plumb déplace une catégorie entière de bugs, le champ oublié, la règle de traduction manquante, l'incompatibilité silencieuse entre deux types, du terrain de la production, où ils coûtent cher et sont difficiles à diagnostiquer, vers celui du démarrage de l'application, où ils sont immédiats, explicites, et faciles à corriger.

Cette philosophie, préférer une erreur bruyante et précoce à un dysfonctionnement discret et tardif, est un principe bien connu des langages de programmation dits « à typage statique » (comme Rust ou Haskell), où le programme refuse de compiler s'il détecte une incohérence. Ce qui est notable ici, c'est que Plumb obtient une bonne partie de ce bénéfice dans Ruby, un langage réputé pour sa souplesse plutôt que pour sa rigueur, la preuve qu'une discipline de ce type peut s'ajouter par-dessus un langage souple, sans avoir à en changer.

Le mécanisme n'est pas magique partout. Quand le contexte ne donne aucun indice sur le sens à prendre, par exemple lorsqu'une donnée est traitée de façon isolée, sans rien avant elle dans la chaîne, le développeur doit préciser lui-même s'il veut lire ou écrire. Et certaines transformations, si elles sont mal conçues (par exemple une transformation qui ne serait pas rigoureusement réversible), pourraient en théorie s'appliquer deux fois par erreur lors du décodage d'une structure complexe, un point que la documentation elle-même signale comme un piège à éviter plutôt que comme un problème résolu.

Le système d'encodeurs et de codecs de Plumb transforme un problème habituellement traité à la main, champ par champ, sens par sens, en une règle unique, déclarée une fois, applicable dans les deux directions, et vérifiée avant même que le programme ne traite sa première donnée. Ce n'est pas une révolution conceptuelle — l'idée de traductions réversibles existe depuis longtemps en informatique — mais une mise en œuvre particulièrement soignée et cohérente, qui rend cette idée accessible dans un langage grand public, sans sacrifier la rigueur qu'on attend habituellement des systèmes de types plus stricts.