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:
structColor{r:u8,
v:u8,
b:u8}structData{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 :
structData{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
fnmain(){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.
structData{value:u8}// On rajoute notre implémentation Deserialize à notre structure Data
impl<'de>Deserialize<'de>forData{fndeserialize<D>(deserializer: D)->Result<Self, D::Error>where D:Deserializer<'de>,
{todo!()}}fnmain(){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 :
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 :
structDataVisitor;impl<'de>Visitor<'de>forDataVisitor{// Nous allons désérialiser vers la structure Data
typeValue= Data;// Défini l'erreur renvoyée en cas de souci
fnexpecting(&self, formatter:&mut Formatter)->std::fmt::Result{ formatter.write_str("Expecting data")}}impl<'de>Deserialize<'de>forData{fndeserialize<D>(deserializer: D)->Result<Self, D::Error>where D:Deserializer<'de>,
{ deserializer.deserialize_map(DataVisitor);}}
Si l'on exécute le codeCode complet
structData{value:u8}impl<'de>Deserialize<'de>forData{fndeserialize<D>(deserializer: D)->Result<Self, D::Error>where D:Deserializer<'de>,
{ deserializer.deserialize_map(DataVisitor);}}structDataVisitor;impl<'de>Visitor<'de>forDataVisitor{// Nous allons désérialiser vers la structure Data
typeValue= Data;// Défini l'erreur renvoyée en cas de souci
fnexpecting(&self, formatter:&mut Formatter)->std::fmt::Result{ formatter.write_str("Expecting data")}}fnmain(){let str_data =r#"value: 12"#;let data =serde_yaml::from_str::<Data>(str_data).expect("Something goes wrong");}
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>forDataVisitor{// Nous allons désérialiser vers la structure Data
typeValue= Data;// Défini l'erreur renvoyée en cas de souci
fnexpecting(&self, formatter:&mut Formatter)->std::fmt::Result{ formatter.write_str("Expecting data")}fnvisit_map<A>(self, mutmap: 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.
Par conséquent, nous devons également faire renvoyer un Result<Data, E> à notre visit_map.Code complet
structData{value:u8}impl<'de>Deserialize<'de>forData{fndeserialize<D>(deserializer: D)->Result<Self, D::Error>where D:Deserializer<'de>,
{ deserializer.deserialize_map(DataVisitor);}}structDataVisitor;impl<'de>Visitor<'de>forDataVisitor{// Nous allons désérialiser vers la structure Data
typeValue= Data;// Défini l'erreur renvoyée en cas de souci
fnexpecting(&self, formatter:&mut Formatter)->std::fmt::Result{ formatter.write_str("Expecting data")}// On défini la variante map pour notre visiteur
fnvisit_map<A>(self, mutmap: A)->Result<Self::Value, A::Error>where A:MapAccess<'de>,
{Ok(Data { value:0})}}fnmain(){let str_data =r#"value: 12"#;let data =serde_yaml::from_str::<Data>(str_data).expect("Something goes wrong");}
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.
fnvisit_map<A>(self, mutmap: 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.
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
structData{value:u8}impl<'de>Deserialize<'de>forData{fndeserialize<D>(deserializer: D)->Result<Self, D::Error>where D:Deserializer<'de>,
{ deserializer.deserialize_map(DataVisitor);}}structDataVisitor;impl<'de>Visitor<'de>forDataVisitor{// Nous allons désérialiser vers la structure Data
typeValue= Data;// Défini l'erreur renvoyée en cas de souci
fnexpecting(&self, formatter:&mut Formatter)->std::fmt::Result{ formatter.write_str("Expecting data")}// On défini la variante map pour notre visiteur
fnvisit_map<A>(self, mutmap: A)->Result<Self::Value, A::Error>where A:MapAccess<'de>,
{// On consomme la clef "value"
map.next_key();// On se permet un unwrap pour l'exemple
let value = map.next_value::<u8>().unwrap();Ok(Data { value
})}}fnmain(){let str_data =r#"value: 12"#;let data =serde_yaml::from_str::<Data>(str_data).expect("Something goes wrong");dbg!(data)}
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 :
structData{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>forDataVisitor{fnvisit_map<A>(self, mutmap: 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})}}fnmain(){let str_data =r#"value: 12`
value2: true"#;let data =serde_yaml::from_str::<Data>(str_data).expect("Something goes wrong");dbg!(data)}
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
structData{value:u8,
value2:bool}impl<'de>Deserialize<'de>forData{fndeserialize<D>(deserializer: D)->Result<Self, D::Error>where D:Deserializer<'de>,
{ deserializer.deserialize_map(DataVisitor);}}structDataVisitor;impl<'de>Visitor<'de>forDataVisitor{// Nous allons désérialiser vers la structure Data
typeValue= Data;// Défini l'erreur renvoyée en cas de souci
fnexpecting(&self, formatter:&mut Formatter)->std::fmt::Result{ formatter.write_str("Expecting data")}// On défini la variante map pour notre visiteur
fnvisit_map<A>(self, mutmap: A)->Result<Self::Value, A::Error>where A:MapAccess<'de>,
{// On consomme la clef "value"
map.next_key();// On se permet un unwrap pour l'exemple
let value = map.next_value::<u8>().unwrap();// On consomme la clef "value2"
map.next_key();// On se permet un unwrap pour l'exemple
let value2 = map.next_value::<bool>().unwrap();Ok(Data { value, value2
})}}fnmain(){let str_data =r#"value: 12`
value2: true"#;let data =serde_yaml::from_str::<Data>(str_data).expect("Something goes wrong");dbg!(data)}
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.
fnmain(){let str_data =r#"value2: true
value: 12"#;let data =serde_yaml::from_str::<Data>(str_data).expect("Something goes wrong");dbg!(data)}
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.
whileletSome(key)=next_key()?{}
À partir de ce moment, on peut définir deux variables mutables.
letmut value =None::<u8>;letmut 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 =ifletSome(x)= value { x
}else{returnErr(Error::missing_field("value"));};let value2 =ifletSome(x)= value2 { x
}else{returnErr(Error::missing_field("value2"));};
Si on fusionne tout
structData{value:u8,
value2:bool}impl<'de>Deserialize<'de>forData{fndeserialize<D>(deserializer: D)->Result<Self, D::Error>where D:Deserializer<'de>,
{ deserializer.deserialize_map(DataVisitor);}}structDataVisitor;impl<'de>Visitor<'de>forDataVisitor{// Nous allons désérialiser vers la structure Data
typeValue= Data;// Défini l'erreur renvoyée en cas de souci
fnexpecting(&self, formatter:&mut Formatter)->std::fmt::Result{ formatter.write_str("Expecting data")}// On défini la variante map pour notre visiteur
fnvisit_map<A>(self, mutmap: A)->Result<Self::Value, A::Error>where A:MapAccess<'de>,
{// on initialise les différents champs
letmut value =None::<u8>;letmut value2 =None::<bool>;// on boucle sur chaque entrées
whileletSome(key)= map.next_key::<&str>()?{match key {"value"=>{ value =Some(map.next_value::<u8>()?);}"value2"=>{ value2 =Some(map.next_value::<bool>()?);}_=>{// on consomme toutes les entrées inconnus
// pour éviter des soucis de désérialisation partielle
let_= map.next_value::<serde::de::IgnoredAny>()?;}}}// On vérifie que les champs ont bien été désérialisés.
let value =ifletSome(x)= value { x
}else{returnErr(Error::missing_field("value"));};let value2 =ifletSome(x)= value2 { x
}else{returnErr(Error::missing_field("value2"));};Ok(Data { value, value2
})}}
Avec cette nouvelle implémentation, à la fois
fnmain(){let str_data =r#"value: 12`
value2: true"#;let data =serde_yaml::from_str::<Data>(str_data).expect("Something goes wrong");}
Et
fnmain(){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 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
impl<'de>Visitor<'de>forPersonVisitor{typeValue= Person;fnexpecting(&self, formatter:&mut Formatter)->std::fmt::Result{ formatter.write_str("expecting ")}fnvisit_map<A>(self, mutmap: A)->Result<Self::Value, A::Error>where A:MapAccess<'de>,
{letmut name =None::<String>;letmut age =None::<u8>;whileletSome(key)= map.next_key::<&str>()?{match key {"name"=> name =Some(map.next_value::<String>()?),"age"=> age =Some(map.next_value::<u8>()?),_=>{ map.next_value::<IgnoredAny>()?;}}}let name =ifletSome(x)= name { x
}else{returnErr(Error::missing_field("name"));};let age =ifletSome(x)= age { x
}else{returnErr(Error::missing_field("age"));};Ok(Person { name, age })}}impl<'de>Deserialize<'de>forPerson{fndeserialize<D>(deserializer: D)->Result<Self, D::Error>where D:Deserializer<'de>,
{ deserializer.deserialize_map(PersonVisitor)}}
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
fnvisit_map<A>(self, mutmap: A)->Result<Self::Value, A::Error>where A:MapAccess<'de>,
{letmut value =None::<u8>;letmut value2 =None::<bool>;letmut person =None::<Person>;whileletSome(key)= map.next_key::<&str>()?{match key {"value"=>{ value =Some(map.next_value::<u8>()?);}"value2"=>{ value2 =Some(map.next_value::<bool>()?);}"person"=>{// On utilise le trait Deserialize de Person
person =Some(map.next_value::<Person>()?);}_=>{let_= map.next_value::<serde::de::IgnoredAny>()?;}}}let value =ifletSome(x)= value { x
}else{returnErr(Error::missing_field("value"));};let value2 =ifletSome(x)= value2 { x
}else{returnErr(Error::missing_field("value2"));};// Le nouveau champ
let person =ifletSome(x)= person { x
}else{returnErr(Error::missing_field("person"));};Ok(Data { value, value2, person,})}
fnvisit_map<A>(self, mutmap: A)->Result<Self::Value, A::Error>where A:MapAccess<'de>,
{letmut name =None::<String>;letmut age =None::<u8>;letmut addresses =None::<Vec<String>>;whileletSome(key)= map.next_key::<&str>()?{match key {"name"=> name =Some(map.next_value::<String>()?),"age"=> age =Some(map.next_value::<u8>()?),"addresses"=>{// On désérialise le tableau
addresses =Some(map.next_value::<Vec<String>>()?);}_=>{ map.next_value::<IgnoredAny>()?;}}}let name =ifletSome(x)= name { x
}else{returnErr(Error::missing_field("name"));};let age =ifletSome(x)= age { x
}else{returnErr(Error::missing_field("age"));};let addresses =ifletSome(x)= addresses { x
}else{returnErr(Error::missing_field("addresses"));};Ok(Person { name, age, addresses,})}
fnmain(){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.
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
fnmain(){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");}
fnmain(){// ...
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.
fnmain(){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.
fnmain(){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é.
fnmain(){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);}
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 🤩
fnmain(){// ...
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.
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.
fnsimple<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();ifletSome(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"))?;returnOk(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.
fncomplex<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);ifletSome(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)?;returnOk(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()))})};}
Puis, on réalise le décodage des composantes et si tout se passe bien, on renvoie la couleur.
fnhex<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);ifletSome(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.
fndeserialize_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 😀
fnmain(){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();
usecrate::{Deserialize, Serialize};useregex::Regex;useserde::de::{Error, Visitor};useserde::Deserializer;usestd::fmt::Formatter;usestd::num::ParseIntError;#[derive(Serialize, Deserialize, Debug)]structColor{r:u8,
b:u8,
g:u8,
}#[derive(Serialize, Deserialize, Debug)]structData{#[serde(deserialize_with ="deserialize_color")]color: Color,
shape: Shape,
}#[derive(Debug, Deserialize, Serialize)]#[serde(tag ="kind", content ="details")]enumShape{ Rect { width:u32, length:u32}, Circle(u32), Point,}// -- Utils
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()))})};}fndecode_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()}// -- Parsing
fncomplex<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);ifletSome(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)?;returnOk(Some(Color { r, b, g }));}Ok(None)}fnsimple<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();ifletSome(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"))?;returnOk(Some(Color { r, b, g }));}Ok(None)}fnhex<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);ifletSome(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)}// -- Visitor custom
structColorDeserializer;impl<'de>Visitor<'de>forColorDeserializer{typeValue= Color;fnexpecting(&self, formatter:&mut Formatter)->std::fmt::Result{ formatter.write_str("Expecting color string")}fnvisit_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"))}}fndeserialize_color<'de, D>(deserializer: D)->Result<Color, D::Error>where D:Deserializer<'de>,
{ deserializer.deserialize_str(ColorDeserializer)}// --- Main
fnmain(){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<Data>>(yml_data_serialized).unwrap();dbg!(yml_data_deserialized);
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 ! ❤️
Auteur: Akanoa
Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.