Sérialisation
Dans le chapitre précédent, nous avons vu comment la bibliothèque clap
est
capable de générer automatiquement le code d’une interface ligne de commande
très complète à partir de la définition des paramètres qu’on veut
contrôler via cette ligne de commande.
Plus généralement, comme la génération de code est quelque chose de très accessible en Rust, on peut s’en servir pour automatiser toutes sortes de tâches de manipulation de données rébarbatives. Ici, nous allons l’appliquer à une autre opération qui donne beaucoup de fil à retordre en C++, la sérialisation des données. Ou comment transformer ses structs et enum en JSON, YAML, CSV, etc.
Contexte
Dans la plupart des langages, quand on sérialise des données, on se “marie” à un format de données en utilisant une bibliothèque très spécifique à ce format. C’est un problème pour deux raisons.
D’abord, au moment où on fait un premier choix de format de données, l’application n’est pas forcément assez mature pour qu’on fasse un choix éclairé, et donc c’est important de pouvoir essayer autre chose sans réécrire beaucoup de code. Particulièrement au niveau des formats de sortie ou d’entrée, qui opèrent sous des contraintes de performances et se comportent plus ou moins bien selon la manière dont le code les utilise.
Ensuite, les formats de données sont sujets à des effets de mode, particulièrement dans le domaine de la configuration. A l’heure où ces lignes sont écrites, il y a une mode du YAML et du TOML, avec encore pas mal de JSON ici et là. Il y a une dizaine d’années la mode était aux langages à balise comme XML, alors qu’aujourd’hui ce format est considéré comme has been et les utilisateurs détestent configurer quelque chose avec. Et je suis prêt à parier que si vous relisez ce cours dans dix ans, quelque chose d’autre aura remplacé le YAML et son utilisation sera universellement honnie.
Tout ça pour dire que si votre application est destinée à vivre longtemps, vous devez être prêt à changer de format de fichier de configuration au fil de son cycle de maintenance, et dépendre d’un format précis est une mauvaise idée.
En Rust, la bibliothèque serde
essaie donc de répondre à
deux problèmes :
- Rendre la sérialisation plus simple en générant un maximum de code automatiquement, de façon bien intégrée au langage.
- Rendre la sérialisation plus flexible en permettant d’émettre et recevoir des données dans de très nombreux formats de données, du Avro au YAML.
Utilisation
Comme précédemment, commencez par ajouter serde
comme dépendance, en activant
le support de la dérivation de traits :
cargo add serde --features derive
Ensuite, définissez un type de données avec ce que vous voulez dedans :
#![allow(unused)] fn main() { struct MonType { abc: u32, def: String, ghi: Vec<f32>, } }
Puis ramenez les traits de serde
dans le scope pour plus de confort, et
activez le support de la sérialisation pour ce type en quelques dérivations :
use serde::{Serialize, Deserialize};
#[derive(Debug, Deserialize, Serialize)]
struct MonType {
abc: u32,
def: String,
ghi: Vec<f32>,
}
Et voilà, les données de ce type peuvent maintenant être sérialisées et
désérialisées depuis et vers tous les formats supportés par
serde
.
Pour être plus concret, on va donner un exemple en JSON. Ajoutons la
bibliothèque serde_json
pour ce format en dépendance…
cargo add serde_json
…et en une paire d’appels de fonction, on est maintenant capables d’émettre du JSON et de reconstruire des données du type choisi depuis le JSON :
fn main() {
// Valeur test
let entree = MonType {
abc: 42,
def: "hello".to_string(),
ghi: vec![1.2, 3.4, 5.6, 7.8],
};
println!("Valeur d'entrée : {entree:#?}");
// Transformation en JSON
let json = serde_json::to_string(&entree)
.expect("Failed to encode JSON");
println!("JSON : {}", json);
// Décodage du JSON
let sortie: MonType = serde_json::from_str(&json)
.expect("Failed to decode JSON");
println!("Valeur reconstruite : {sortie:#?}");
}
On le voit, serde
rend le cas simple extrêmement simple d’utilisation, ce qui
est très important. Pour les cas plus complexe, on peut utiliser des directives
spécifiques à serde, comme dans l’exemple clap
du chapitre précédent.
Cela permet notamment de contrôler finement la façon dont les types énumérés sont sérialisés : comme ces types ne sont pas supportés nativement par la plupart des formats de données, il y a plusieurs manières de les retranscrire, et c’est important de choisir la bonne quand on doit décoder des données générées par un autre programme.