Serde
Bonjour à toutes et à tous 😀
Une fois n'est pas coutume, nous allons partir d'un cahier des charges.
Je dispose de 3 fichiers:
Un YAML
color: "r:66,v:38,b:128"
Un JSON
{
"color": "66,38,128"
}
Un TOML
color = "#422d80"
Je désire obtenir mes données dans la structure Rust suivante:
struct Color {
r: u8,
v: u8,
b: u8
}
struct Data {
color: Color
}
De quelle manière peut-on réaliser cela en Rust ?
C'est ce que l'on va voir dans la suite.
C'est parti !
Sérialisation / Désérialisation
Lorsque vous jouez aux jeux-vidéos à moins que vous soyez sur un jeu datant d'avant le Déluge, vous avez la possibilité de sauvegarder votre partie.
Celle-ci sera stockée dans la mémoire sous la forme d'un fichier.
Exemple : Vous êtes au niveau 40, votre inventaire est composé d'une épée en bois et d'une gourde, vous avez 10 pv.
Ces informations peuvent-être stockées de manière textuelle ainsi :
lvl=40
pv=10
inventory=wood_sword,flask
Ce fichier sera alors lu et interprété pour reconstituer l'état du jeu avant sauvegarde.
On vient de réaliser un processus de sérialisation/désérialisation.
flowchart LR Jeu-- "Sérialisation"-->Sauvegarde Sauvegarde -- "Désérialisation"-->Jeu
Le format de sérialisation est libre, vous pouvez tout aussi bien définir un protocole qui vous est propre.
Par exemple la sauvegarde du dessus qui est relativement explicite, pourrait être plus cryptique :
40;10;4,9
Les données sont les mêmes, mais ne sont pas représentées dans la sauvegarde de la même manière.
Le processus de sérialisation consiste donc à transformer une structure de données en un format pouvant être stocké et transmis.
La principale caractéristique d'une bonne sérialisation, c'est qu'elle est réversible via le processus inverse de désérialisation.
flowchart LR A[Structure de Données] B[Données sérialisées] A-- "Sérialisation"-->B B-- "Désérialisation"-->A
Remarque
Écrire du code en programmation, c'est sérialiser la pensée humaine 🤯
Introduction à Serde
Lorsque l'on débute dans le monde de la sérialisation/désérialisation en Rust, on se retrouve très vite à entendre parler de serde
.
A comprendre SERialisationDEserialization
Serde est une crate qui fourni à la fois une série de traits mais aussi une batterie de fonctions utilitaires, ainsi que de macros.
Nous allons voir dans la suite de l'article comment nous en servir.
Implémentation du trait Deserialize
En recherchant un peu sur internet, on finit par découvrir la crate serde_yaml
. Celle-ci comme son nom l'indique, permet de sérialiser et de désérialiser des structures de données au format YAML.
On installe celle-ci
cargo add serde_yaml
Exemple le fichier suivant
value:12
Nous voulons la structure suivante :
struct Data {
value: u8
}
En creusant dans la documentation de la crate serde_yaml, nous trouvons une fonction qui prend en paramètre une chaîne de caractères.
Celle-ci renvoie un résultat sous la forme d'un Result<T>
où T
implémente le trait Deserialize
.
En l'absence d'indices pour le compilateur, vous devez spécifier le type de T
fn main() {
let str_data = r#"value: 12"#;
let data = serde_yaml::from_str(str_data)
.expect("Something goes wrong");
}
Sinon vous recevrez cette erreur :
error[E0282]: type annotations needed
|
| let data = serde_yaml::from_str(str_data)
| ^^^^ consider giving `data` a type
Que vous pouvez corriger ainsi
let data : Data = serde_yaml::from_str(str_data)
.expect("Something goes wrong");
ou
let data = serde_yaml::from_str::<Data>(str_data)
.expect("Something goes wrong");
Mais cela conduit à une seconde erreur:
error[E0277]: the trait bound `Data: Deserialize<'_>` is not satisfied
|
| let data = serde_yaml::from_str::<Data>(str_data)
| ^^^^
the trait `Deserialize<'_>` is not implemented for `Data`
Ce trait Deserialize
est notre premier pas dans l'utilisation de la crate serde
. En effet se trait fait partie du module serde::de::Deserialize
.
Nous devons donc pour rendre compatible notre structure Data
en venant implémenter le trait serde::de::Deserialize
.
struct Data {
value: u8
}
// On rajoute notre implémentation Deserialize à notre structure Data
impl<'de> Deserialize<'de> for Data {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
todo!()
}
}
fn main() {
let str_data = r#"value: 12"#;
let data = serde_yaml::from_str::<Data>(str_data)
.expect("Something goes wrong");
}
Cela va bien évidemment provoquer une erreur de code non implémenté.
Ceci peut être fait en renvoyant une structure Data
par défaut :
impl<'de> Deserialize<'de> for Data {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(Data {
value: 0,
value2: false,
})
}
}
Ce code compile, mais ne renverra bien sûr pas de résultat qui varie en fonction de l'entrée.
Pour ce faire, nous devons utiliser une autre partie de la crate serde
.
Utilisation d'un Deserializers
Il s'agit des Deserializers.
Ça tombe bien la méthode deserialize
nous fourni ce Deserializer
, ou plutôt serde_yaml
nous en fourni un.
Nous verrons dans une partie suivante comment créer notre propre
Deserializer
Ce désérialiser implémente le trait Deserializer
, celui-ci nous fourni une série de fonctions.
La méthode qui va nous intéresser se nomme deserialize_map.
Le trait Visitor
Comme l'indique la documentation la méthode deserialize_map
prend un paramètre qui implémente un trait serde::de::Visitor
Si vous ne connaissez pas le pattern visitor, vous pouvez vous y former ici.
Ce trait Visitor
possèdent une fonction et un type qui doivent être définis.
Nous pouvons alors modifier notre code en conséquence :
struct DataVisitor;
impl<'de> Visitor<'de> for DataVisitor {
// Nous allons désérialiser vers la structure Data
type Value = Data;
// Défini l'erreur renvoyée en cas de souci
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("Expecting data")
}
}
impl<'de> Deserialize<'de> for Data {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(DataVisitor);
}
}
Si l'on exécute le code
Code complet
Celui-ci nous renvoie une erreur :
Error("invalid type: map, expected Expecting data", line: 1, column: 1)
Ce qu'il nous dit énigmatiquement, c'est qu'il a tenté de visiter une map
mais que le visiteur n'est pas capable de le faire.
Nous allons donc devoir implémenter la méthode visit_map sur notre DataVisitor
.
impl<'de> Visitor<'de> for DataVisitor {
// Nous allons désérialiser vers la structure Data
type Value = Data;
// Défini l'erreur renvoyée en cas de souci
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("Expecting data")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
unimplemented!()
}
}
Si l'on relance, on arrive dans notre fonction avec un joli panic!
thread 'main' panicked at 'not implemented
Ça échoue avec succès, c'est parfait !
Le trait MapAccess
Si l'on analyse un peu plus en détails la signature de la méthode visit_map
. On voit que celle-ci renvoie un
Result<Self::Value, E>
Ce Self::Value
est en fait le type Value
de notre DataVisitor
.
impl<'de> Visitor<'de> for DataVisitor {
type Value = Data;
}
Par conséquent, nous devons également faire renvoyer un Result<Data, E>
à notre visit_map
.
Code complet
Ce qui conduit à encore une erreur :
Error("invalid length 1, expected map containing 0 entries", line: 1, column: 1)
Ok! Là, c'est un peu plus énigmatique ! 😕
Pour comprendre, il faut analyser le paramètre d'entrée de la fonction visit_map
, il nous fournit une structure qui implémente le trait MapAccess.
Dans les méthodes implémentées par ce trait, deux d'entre-elles vont nous intéresser.
Elles vont nous permettre de désérialiser respectivement la clef et la valeur de chaque entrée de la map qui représente notre structure de données Data
.
Cette map à une particularité : les clefs sont toujours des chaînes de caractères.
Map<&str, T:Deserialize>
Nous allons pouvoir récupérer nos données.
Notre structrure ne possédant qu'un seul champ. Si l'on exécute next_value
nous sommes certains d'accéder à la valeur du champ value
.
Ce champ étant un u8
, nous demandons à next_value
de désérialiser vers une valeur de u8
.
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let value = map.next_value::<u8>();
Ok(Data {
value
})
}
Une erreur survient
thread 'main' panicked at 'unexpected end of mapping'
Cette erreur est due à la manière dont la map est consommée par le deserializer: il vient d'abord analyser la clef de l'entrée puis passe au séparateur et enfin atteint la valeur.
┌-- clef
| ┌-- valeur
↓ ↓
value : 12
^^^^ séparateur
Nous devons donc consommer la clef avant de pouvoir le faire avec la valeur. Comme nous savons que la clef est toujours du type &str
. Nous pouvons utiliser la désérialisation de la clef vers une chaîne de caractères.
Code complet
data = Ok(
Data {
value: 12,
},
)
Cette fois-ci c'est bon. 😀
Désérialisation d'une structure de plus d'un champ (approche naïve)
C'est cool, mais pour l'instant, il n'y a qu'une entrée dans notre structure.
Que se passe-t-il si la structure ressemble plutôt à ceci :
struct Data {
value: u8,
value2: bool
}
Si l'on exécute à nouveau notre code précédent, mais avec des données plus complexes qui comportent les deux champs :
- value
- value2
impl<'de> Visitor<'de> for DataVisitor {
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
map.next_key();
let value = map.next_value::<u8>().unwrap();
Ok(Data {
value,
// on force la vaeur pour passer la compilation
value2: false
})
}
}
fn main() {
let str_data = r#"value: 12`
value2: true"#;
let data = serde_yaml::from_str::<Data>(str_data)
.expect("Something goes wrong");
dbg!(data)
}
Cela provoque une erreur :
Error("invalid length 2, expected map containing 1 entry", line: 1, column: 1)
Maintenant que nous parlons la langue des deserializer, nous comprenons que nous ne consommons qu'une seule entrée sur les 2 existantes dans la map fourni par le serde_yaml.
Code complet
Cette fois-ci ça fonctionne ! 😀
Désérialisation d'une structure de plus d'un champ (approche systématique)
Bon, nous avons réparé notre désérialiser.
Par contre, nous avons posé une hypothèse en présumant de l'ordre des entrées lors de la désérialisation du yaml.
fn main() {
let str_data = r#"value2: true
value: 12"#;
let data = serde_yaml::from_str::<Data>(str_data)
.expect("Something goes wrong");
dbg!(data)
}
Cette fois-ci l'erreur est claire !
Error("value2: invalid type: boolean `true`, expected u8", line: 1, column: 9)
Nous avons supposé l'ordre d'apparition de value
et value2
et nous nous sommes trompés !
Il est temps d'utiliser le next_key::<&str>
.
Celui-ci va nous renvoyer un résultat de type Result<Option<&str>>
.
Tant qu'il existe une clef qui n'a pas été consommée, un Some(&str)
est renvoyé. Si la dernière clef a déjà été consommée, c'est un None
.
On peut utiliser la structure while let
pour déstructurer et boucler sur les résultats de notre next_key
.
On utilise également la syntaxe ?
pour nous débarrasser de l'éventuelle erreur de désérialisation de la clef.
while let Some(key) = next_key()? {
}
À partir de ce moment, on peut définir deux variables mutables.
let mut value = None::<u8>;
let mut value2 = None::<bool>;
On peut alors venir matcher la clef. Et désérialiser selon le bon type.
match key {
"value" => {
value = Some(map.next_value::<u8>()?);
}
"value2" => {
value2 = Some(map.next_value::<bool>()?);
}
_ => {
let _ = map.next_value::<serde::de::IgnoredAny>()?;
}
}
Finalement, une fois que toutes les entrées ont été désérialisées, l'on peut détecter si les champs de la structure Data
,ont correctement été désérialisés.
Si l'un des champs n'a pas été trouvé lors de la désérialisation, on renvoie une erreur.
let value = if let Some(x) = value {
x
} else {
return Err(Error::missing_field("value"));
};
let value2 = if let Some(x) = value2 {
x
} else {
return Err(Error::missing_field("value2"));
};
Si on fusionne tout
Avec cette nouvelle implémentation, à la fois
fn main() {
let str_data = r#"value: 12`
value2: true"#;
let data = serde_yaml::from_str::<Data>(str_data)
.expect("Something goes wrong");
}
Et
fn main() {
let str_data = r#"value2: true`
value: 12"#;
let data = serde_yaml::from_str::<Data>(str_data)
.expect("Something goes wrong");
}
Fonctionnent correctement. 😁
Désérialisation depuis d'autres formats
En bonus, on peut aussi désérialiser du JSON sans avoir à changer notre trait Deserialize
.
On installe la crate serde_json et sa méthode from_str.
cargo add serde_json
fn main() {
let str_data = r#"{
"value" : 12,
"value2": true
}"#;
let data = serde_json::from_str::<Data>(str_data)
.expect("Something goes wrong");
}
C'est la grande force de serde
, à partir du moment où l'on définit le trait Deserialize
on peut l'utiliser avec, n'importe quel Deserializer
.
Composition de structures
Imaginons que l'on souhaite désérialiser vers une structure qui a plutôt cette forme :
struct Person {
name: String,
age: u8,
}
struct Data {
value: u8,
value2: bool,
person: Person,
}
On se retrouve avec un champ "person" qui doit également être désérialisé.
Pour cela on implémente le trait Deserialize
pour la structure Person
.
Deserialize for Person
Je ne détaille pas plus, on est dans la même situation que pour la structure Data
.
Puis on modifie le visit_map
du trait Visitor
du DataVisitor
:
visit_map
On peut alors l'utiliser
pub fn main() {
let str_data = r#"value2: true
value: 12
person:
age: 78
name: "Jane""#;
let data = serde_yaml::from_str::<Data>(str_data);
dbg!(data);
let str_data = r#"{
"value" : 13,
"value2": true,
"person": {
"name" : "Bob",
"age" : 42
}
}"#;
let data = serde_json::from_str::<Data>(str_data).expect("Something goes wrong");
dbg!(data);
}
Désérialiser un tableau
Maintenant, nous sommes rodés, on peut commencer à désérialiser ce que l'on veut ! Par exemple, on peut automatiquement désérialiser tout
Vec<T : Deserialize>
Ce qui nous permet de pouvoir faire :
struct Person {
name: String,
age: u8,
addresses: Vec<String>,
}
On modifie le visit_map
en conséquence.
visit_map
Que l'on utilise ainsi :
fn main() {
let str_data = r#"
person:
age: 78
name: "Jane"
addresses:
- "test"
- test2
"#;
let data = serde_yaml::from_str::<Data>(str_data);
dbg!(data);
Code complet de la partie
Désérialiser une énumération
Une énumération en Rust est un objet complexe, il peut prendre plusieurs formes :
Nous allons tenter de désérialiser la structure suivante :
Elle possède des champs qui se désérialisent sous la forme d'une énumération.
Cette énumération a pour particularité d'avoir une variante possédant un paramètre de type chaîne de caractères.
enum Housing {
House,
Flat,
Other(String),
}
struct Data {
housing: Housing,
}
Nous allons définir deux formats valides, permettant la sérialisation de ce champ housing
.
La première défini cette énumération comme une chaîne de caractère.
housing: house
---
housing: flat
---
housing: other
La seconde sous la forme d'une structure.
housing:
type: house
---
housing:
type: flat
---
housing:
type: other
details: van
Je passe l'implémentation du trait Deserialize
pour la structure Data
.
Et on s'attaque tout de suite à l'implémentation de notre trait Deserialize
pour l'énumération Housing
.
impl<'de> Deserialize<'de> for Housing {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(HousingVisitor)
}
}
On utilise
deserialize_any
pour gérer le casvisit_str
etvisit_map
.
Puis, on s'attaque à l'implémentation du HousingVisitor
struct HousingVisitor;
impl<'de> Visitor<'de> for HousingVisitor {
type Value = Housing;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("expecting housing")
}
}
On définit d'abord la méthode visit_str
:
fn visit_str<E>(self, kind: &str) -> Result<Self::Value, E>
where
E: Error,
{
const VARIANTS: &[&str] = &["house", "flat", "other"];
match kind.to_lowercase().as_str() {
"house" => Ok(Housing::House),
"flat" => Ok(Housing::Flat),
"other" => Ok(Housing::Other("".to_string())),
_ => Err(Error::unknown_variant(kind, VARIANTS)),
}
}
Elle vient réaliser une correspondance entre la chaîne de caractères désérialisée et une variante de notre énumération Housing
.
La fonction visit_map
ressemble beaucoup à ce que l'on a déjà fait : on vient désérialiser les entrées kind
et details
.
La différence, c'est que le champ details n'est utilisé que pour la variante Other
.
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut kind = None::<&str>;
let mut details = None::<String>;
while let Some(key) = map.next_key::<&str>()? {
match key {
"kind" => kind = Some(map.next_value::<&str>()?),
"details" => details = map.next_value::<Option<String>>()?,
_ => {
map.next_value::<IgnoredAny>()?;
}
}
}
const VARIANTS: &[&str] = &["house", "flat", "other"];
if let Some(kind) = kind {
match kind.to_lowercase().as_str() {
"house" => Ok(Housing::House),
"flat" => Ok(Housing::Flat),
"other" => Ok(Housing::Other(details.unwrap_or_default())),
_ => Err(Error::unknown_variant(kind, VARIANTS)),
}
} else {
Err(Error::missing_field("kind"))
}
}
En rassemblant le tout :
Plus de détails
On peut alors désérialiser nos structures :
fn main() {
let str_data = r#"
housing: House
"#;
let data = serde_yaml::from_str::<Data>(str_data);
dbg!(data);
let str_data = r#"
housing:
kind: other
details: "van"
"#;
let data = serde_yaml::from_str::<Data>(str_data);
dbg!(data);
}
Implémentation du trait Serialize
Ce trait est le pendant du trait Deserialize
.
Il est nécessaire pour la transformation d'une structure de données Rust en chaîne de caractères en format JSON, TOML, ou YAML par exemple.
On le retrouve dès que l'on souhaite utiliser les méthodes to_string
des différents serializers:
Ces trois méthodes possèdent la même signature qui prend en paramètre d'entrée, une référence vers structure de données implémentant le trait Serialize.
Celui-ci nous impose d'implémenter la méthode serialize.
Si on possède une structure comme celle-ci :
enum Housing {
House,
Flat,
Other(String),
}
struct Person {
name: String,
age: u8,
housings: Vec<Housing>,
}
Comme énoncé dans la documentation, il est facile d'implémenter le sérialiseur de notre structure.
impl Serialize for Person {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map_serializer = serializer.serialize_map(Some(3_usize))?;
map_serializer.serialize_entry("name", &self.name)?;
map_serializer.serialize_entry("age", &self.age)?;
map_serializer.serialize_entry("housings", &self.housings)?;
map_serializer.end()
}
}
Par contre, le serialize_entry
pour "housings" pose souci. En effet, la value doit implémenter Serialize
, or l'énumération Housing
n'implémente pas ce trait.
Ce qui provoque cette erreur :
the trait bound `Housing: Serialize` is not satisfied
|
| map_serializer.serialize_entry("housings", &self.housings)?;
the trait `Serialize` is not implemented for `Housing ^^^^^^^^^^^^^^
Nous allons remédier à cela.
Pour sérialiser notre énumération Housing
. Nous allons en faire une structure possédant 2 champs
- kind : sera rempli avec la dénomination sous forme de chaîne de caractères de la variante
- details : il sera seulement rempli dans le cas de la variante
Housing::Other
impl Serialize for Housing {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let (kind, details) = match self {
Housing::House => ("House", ""),
Housing::Flat => ("Flat", ""),
Housing::Other(details) => ("Other", details.as_str()),
};
let mut map_serializer = serializer.serialize_map(Some(2))?;
map_serializer.serialize_entry("kind", kind)?;
map_serializer.serialize_entry("details", details)?;
map_serializer.end()
}
}
Ceci fait, nous pouvons alors utiliser notre structure Person
dans n'importe quel sérialiseur !
On installe tout le monde
cargo add serde_json serde_yaml toml
Puis, on définit notre structure à sérialiser et on l'utilise dans les sérialiseurs. 😀
fn main() {
let bob = Person {
name: "Bob".to_string(),
age: 36,
housings: vec![
Housing::House,
Housing::Other("van".to_string()),
Housing::Flat,
Housing::Other("travel trailer".to_string()),
],
};
let str_yaml = serde_yaml::to_string(&bob);
println!("{}", str_yaml.unwrap());
let str_yaml = serde_json::to_string(&bob);
println!("{}", str_yaml.unwrap());
println!();
let str_yaml = toml::to_string(&bob);
println!("{}", str_yaml.unwrap());
}
Le code complet
Plus de détails
Les dérivations
Je vous ai un peu menti. 😅
Tout ce qu'on a fait au-dessus est faisable bien plus facilement.
Il existe en Rust ce que l'on appelle les dérivations.
Des dérivations, vous en avez déjà utilisé.
Par exemple :
struct Toto;
fn main() {
let toto = Toto;
println("toto:?");
}
Ceci provoque une erreur, mais que le compilateur vous permet de résoudre simplement.
error[E0277]: `Toto` doesn't implement `Debug`
| println!("{toto:?}")
| ^^^^ `Toto` cannot be formatted using `{:?}`
help: consider annotating `Toto` with `#[derive(Debug)]`
|
| #[derive(Debug)]
En obéissant docilement à cargo.
#[derive(Debug)]
struct Toto;
fn main() {
let toto = Toto;
println("{toto:?}");
}
Cette fois-ci ça fonctionne. ✅
Le
#[derive()
est un appel à une macro. Il vient générer du code qui sera compilé par la suite.#[derive(Debug)] struct Toto { val: u8, val2: bool, } // --- équivalant à struct Toto { val: u8, val2: bool, } impl Debug for Toto { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("Toto") .field("val", &self.val) .field("val2", &self.val2) .finish() } }
Ces dérivations nous permettent à la fois de gagner du temps et d'avoir une écriture plus compacte.
serde
nous propose le même genre de dérivations.
Que l'on doit activer sous la forme de feature.
car add serde --features derive
Cette feature nous permet d'utiliser deux dérivations venant de [serde_derive]:
On peut alors reprendre notre exemple de la section précédente.
#[derive(Debug, Serialize, Deserialize)]
enum Housing {
House,
Flat,
Other(String),
}
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
age: u8,
}
#[derive(Debug, Serialize, Deserialize)]
struct Data {
housings: Vec<Housing>,
person: Person,
}
Utilisons notre nouveau jouet ! 😁
fn main() {
let original_data = Data {
housing: vec![
Housing::Flat,
Housing::Other("van".to_string())
],
person: Person {
name: "Bob".to_string(),
age: 41,
},
};
let json_string = serde_json::to_string(&original_data)
.expect("Unable to serialize to JSON");
let json_data =
serde_json::from_str::<Data>(&json_string)
.expect("Unable to deserialize from JSON");
}
Magie ! 🧙♂️
json_string = "{\"housing\":[\"Flat\",{\"Other\":\"van\"}],\"person\":{\"name\":\"Bob\",\"age\":41}}"
json_data = Data {
housing: [
Flat,
Other(
"van",
),
],
person: Person {
name: "Bob",
age: 41,
},
}
On récupère nos données avec 0 effort ou presque.
Essayons avec YAML
fn main() {
// ...
let yaml_string = toml::to_string(&original_data)
.expect("Unable to serialize to YAML");
let yaml_data = toml::from_str::<Data>(&yaml_string)
.expect("Unable to deserialize from YAML");
dbg!(yaml_string);
dbg!(yaml_data);
}
Ah 😥
thread 'main' panicked at 'Unable to serialize to YAML: UnsupportedType'
Essayons de décomposer les choses.
fn main() {
let original_data = Data {
housing: vec![
Housing::Flat,
],
person: Person {
name: "Bob".to_string(),
age: 41,
},
};
let yaml_string = toml::to_string(&original_data)
.expect("Unable to serialize to YAML");
println!("{}", yaml_string);
}
Le TOML généré ressemble à :
housing = ["Flat"]
[person]
name = "Bob"
age = 41
Ok, et avec la variante Housing::Other
.
fn main() {
let original_data = Data {
housing: vec![
Housing::Other("van".to_string()),
],
person: Person {
name: "Bob".to_string(),
age: 41,
},
};
let yaml_string = toml::to_string(&original_data)
.expect("Unable to serialize to YAML");
println!("{}", yaml_string);
}
Bim! Il se prend les pieds dans le tapis !
thread 'main' panicked at 'Unable to serialize to YAML: UnsupportedType'
Il ne sait pas comment gérer la variante Housing::Other
.
Il va falloir lui donner un coup de pouce. ^^
Heureusement, les concepteurs de serde_derive
ont tout prévu. Il est possible d'annoter une énumération pour définir comment la variante et son contenu doit être sérialisé.
Pour cela l'on modifie notre énumération Housing
.
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
enum Housing {
House,
Flat,
Other(String),
}
Si l'on relance la fonction, cela se passe beaucoup mieux. 😀
[[housing]]
type = "Other"
value = "van"
[person]
name = "Bob"
age = 41
On réactive notre tableau :
fn main() {
let original_data = Data {
housing: vec![
Housing::Flat,
Housing::Other("van".to_string())
],
person: Person {
name: "Bob".to_string(),
age: 41,
},
};
let yaml_string = toml::to_string(&original_data)
.expect("Unable to serialize to YAML");
let yaml_data = toml::from_str::<Data>(&yaml_string)
.expect("Unable to deserialize from YAML");
println!("{}", yaml_string);
dbg!(yaml_data);
}
La sérialisation se passe bien :
[[housing]]
type = "Flat"
[[housing]]
type = "Other"
value = "van"
[person]
name = "Bob"
age = 41
Ainsi que la désérialisation
yaml_data = Data {
housing: [
Flat,
Other(
"van",
),
],
person: Person {
name: "Bob",
age: 41,
},
}
On peut alors faire quelque chose de fondamentalement inutile mais follement amusant 🤩
fn main() {
// ...
let json_string = serde_json::to_string(&original_data)
.expect("Unable to serialize to JSON");
let json_data =
serde_json::from_str::<Data>(&json_string)
.expect("Unable to deserialize from JSON");
let yaml_string = toml::to_string(&json_data)
.expect("Unable to serialize to YAML");
let yaml_data = toml::from_str::<Data>(&yaml_string)
.expect("Unable to deserialize from YAML");
let toml_string = toml::to_string(&yaml_data)
.expect("Unable to serialize to TOML");
let toml_data = toml::from_str::<Data>(&toml_string)
.expect("Unable to deserialize from TOML");
dbg!(toml_data);
}
On sérialise puis désérialise successivement dans plusieurs formats.
flowchart LR A[Data] B1[JSON String] B2[YAML String] B3[TOML String] A-- "ser JSON"-->B1 B1-- "de JSON"-->A A-- "ser YAML"-->B2 B2-- "de YAML"-->A A-- "ser TOML"-->B3 B3-- "de TOML"-->A
Pour finalement afficher le résultat.
toml_data = Data {
housing: [
Flat,
Other(
"van",
),
],
person: Person {
name: "Bob",
age: 41,
},
}
Désérialisation personnalisée
Parfois, on ne maîtrise pas le format que l'on souhaite désérialiser.
Par exemple la structure suivante :
struct Color {
r: u8,
b: u8,
g: u8,
}
enum Shape {
Rect{width: u8, length: u8},
Circle(u8),
Point
}
struct Data {
color: Color,
shape: Shape
}
Sera sérialisée en YAML ainsi :
- color: 66,128,45
shape:
kind: Point
- color: 66,128,45
shape:
kind: Rect
details:
width: 600
length: 400
- color: 66,128,45
shape:
kind: Circle
details: 45
On voit ici que le champ color
est de type String
.
En utilisant les dérivations cela nous donne:
#[derive(Deserialize)]
struct Color {
r: u8,
b: u8,
g: u8,
}
#[derive(Deserialize)]
#[serde(tag="kind", content="details")]
enum Shape {
Rect{width: u8, length: u8},
Circle(u8),
Point
}
#[derive(Deserialize)]
struct DataColorString {
color: String,
shape: Shape
}
On se retrouve donc avec une chaîne de caractères qu'il va falloir parser.
On a 3 formats différents à parser:
- simple : des nombres séparés par une virgule
- complex: le format
r:0,g:0,b:0
- hex :
#000000
Format simple
Une chaîne valide est composée de 3 nombres séparés par une virgule.
Une fois la chaîne split sur la virgule, l'on convertit en u8
chaque composante.
Si une erreur survient, on s'arrête et retourne l'erreur.
Puis, on récupère les différentes valeurs pour composer notre Color
.
fn simple<E: Error>(value: &str) -> Result<Option<Color>, E> {
let values = value
.split(',')
.map(|v| v.parse::<u8>().map_err(|err| <E>::custom(err)))
.collect::<Result<Vec<u8>, E>>()
.ok();
if let Some(values) = values {
let r = *values
.first()
.ok_or_else(|| <E>::custom("Unable to get r component"))?;
let g = *values
.get(1)
.ok_or_else(|| <E>::custom("Unable to get g component"))?;
let b = *values
.get(2)
.ok_or_else(|| <E>::custom("Unable to get b component"))?;
return Ok(Some(Color { r, b, g }));
}
Ok(None)
}
E
n'est pas encore défini.
Format complexe
Le format complexe, impose une certaine manière de définir l'entrée.
Le moyen le plus simple est d'utiliser une regex que l'on installe.
cargo add regex
Ce qui nous donne le code suivant.
fn complex<E: Error>(value: &str) -> Result<Option<Color>, E> {
let regex_rgb =
Regex::new(r#"r:(\d+),g:(\d+),b:(\d+)"#).map_err(|err| E::custom(err.to_string()))?;
let caps = regex_rgb.captures(value);
if let Some(caps) = caps {
let r = parse_component!(1, E, "r", caps)?;
let g = parse_component!(2, E, "g", caps)?;
let b = parse_component!(3, E, "b", caps)?;
return Ok(Some(Color { r, b, g }));
}
Ok(None)
}
On utilise une macro pour diminuer le code à écrire.
macro_rules! parse_component { ($index: literal, $E: ty, $field:literal, $caps: expr) => { $caps .get($index) .ok_or(<$E>::custom(format! {"Unable to get {} component", $field})) .and_then(|m| { m.as_str() .parse::<u8>() .map_err(|err| E::custom(err.to_string())) }) }; }
Le rappel sur les macros est ici. 😀
Format hexadécimal
Le format hexadécimal doit commencer par un "#" et être suivi de 3 nombres hexadécimaux.
Pour transformer ces nombres héxa en u8
.
On définit une fonction qui va réaliser ce décodage.
fn decode_hex(s: &str) -> Result<Vec<u8>, ParseIntError> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16))
.collect()
}
Puis, on réalise le décodage des composantes et si tout se passe bien, on renvoie la couleur.
fn hex<E: Error>(value: &str) -> Result<Option<Color>, E> {
let regex_rgb =
Regex::new(r#"#([a-z0-9]{6})+"#).map_err(|err| E::custom(err.to_string()))?;
let caps = regex_rgb.captures(value);
if let Some(caps) = caps {
return caps
.get(1)
.ok_or_else(|| <E>::custom("Unable to get hex value string"))
.and_then(|m| {
let string_data = m.as_str();
let values = decode_hex(string_data).map_err(|err| <E>::custom(err))?;
let r = *values
.first()
.ok_or_else(|| <E>::custom("Unable to get r component"))?;
let g = *values
.get(1)
.ok_or_else(|| <E>::custom("Unable to get g component"))?;
let b = *values
.get(2)
.ok_or_else(|| <E>::custom("Unable to get b component"))?;
Ok(Some(Color { r, b, g }))
});
}
Ok(None)
}
On fusionne tout
Nous allons maintenant transformer notre Vec<DataColorString>
en Vec<Data>
.
Pour cela, on définit une fonction qui permet de désérialiser successivement chaque format et de s'arrêter dès que l'un correspond.
fn deserialize_color_from_str<E: Error>(v: &str) -> Result<Color, E> {
simple::<E>(v)?
.map_or_else(|| complex::<E>(v), |color| Ok(Some(color)))?
.map_or_else(|| hex::<E>(v), |color| Ok(Some(color)))?
.ok_or_else(|| <E>::custom("Unable to deserialize color field"))
}
Et on fait rouler l'algo 😀
fn main() {
let yml_data_serialized = r#"
- color: "r:66,g:128,b:45"
shape:
kind: Point
- color: "66,128,45"
shape:
kind: Circle
details: 45
- color: "\\#422d80"
shape:
kind: Rect
details:
width: 600
length: 400
"#;
let yml_data_deserialized =
serde_yaml::from_str::<Vec<DataColorString>>(yml_data_serialized).unwrap();
let result = yml_data_deserialized
.into_iter()
.map(|data| {
let color = deserialize_color_from_str::<serde_yaml::Error>(&data.color)?;
Ok(Data {
color,
shape: data.shape,
})
})
.collect::<Vec<Result<Data, serde_yaml::Error>>>();
dbg!(result);
}
Ce qui nous donne :
result = [
Ok(
Data {
color: Color {
r: 66,
b: 45,
g: 128,
},
shape: Point,
},
),
Ok(
Data {
color: Color {
r: 66,
b: 45,
g: 128,
},
shape: Circle(
45,
),
},
),
Ok(
Data {
color: Color {
r: 66,
b: 128,
g: 45,
},
shape: Rect {
width: 600,
length: 400,
},
},
),
]
Serialize with
C'est sympa mais, ce passage de DataColorString
vers Data
est de trop.
Moi, je voudrais
let yml_data_deserialized =
serde_yaml::from_str::<Vec<Data>>(yml_data_serialized).unwrap();
Et bien, c'est possible ! 😄
Serde propose une annotation.
Cette annotation va pouvoir être défini sur le champ color
.
enum Data {
#[serde(deserialize_with = "deserialize_color")]
color: Color,
shape: Shape
}
On doit alors définir une fonction avec la signature suivante :
fn f<'de, D>(deserializer: D) -> Result<Color, D::Error> where D: Deserializer<'de>
C'est parti:
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(....)
}
Oh mais, on connait ça; c'est notre pote le Visitor. On est donc à la maison.
Plus qu'à implémenter le visiteur.
struct ColorDeserializer;
impl<'de> Visitor<'de> for ColorDeserializer {
type Value = Color;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("Expecting color string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
simple::<E>(v)?
.map_or_else(|| complex::<E>(v), |color| Ok(Some(color)))?
.map_or_else(|| hex::<E>(v), |color| Ok(Some(color)))?
.ok_or_else(|| <E>::custom("Unable to deserialize color field"))
}
}
Que l'on rebranche dans notre deserialize_color
.
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(ColorDeserializer)
}
Et maintenant, on fusionne tout !
Code complet
yml_data_deserialized = [
Data {
color: Color {
r: 66,
b: 45,
g: 128,
},
shape: Point,
},
Data {
color: Color {
r: 66,
b: 45,
g: 128,
},
shape: Circle(
45,
),
},
Data {
color: Color {
r: 66,
b: 128,
g: 45,
},
shape: Rect {
width: 600,
length: 400,
},
},
]
Conclusion
Oups ! 😏 l'article devait être court ça n'a pas été le cas. 😅
J'espère qu'il vous a plu.
Je vous remercie pour votre lecture et je vous dis à la prochaine ! ❤️