Serde

Lecture 15 min. ‱


Table des matiĂšres

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
struct Data {
    value: u8
}

impl<'de> Deserialize<'de> for Data {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_map(DataVisitor);
    }
}

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")
    }    
}

fn main() {
    let str_data = r#"value: 12"#;
    let data = serde_yaml::from_str::<Data>(str_data)
        .expect("Something goes wrong");
}

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
struct Data {
    value: u8
}

impl<'de> Deserialize<'de> for Data {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_map(DataVisitor);
    }
}

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")
    }
    
    // On défini la variante map pour notre visiteur
    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
    where
        A: MapAccess<'de>,
    {
        Ok(Data {
            value: 0
        })
    }
}

fn main() {
    let str_data = r#"value: 12"#;
    let data = serde_yaml::from_str::<Data>(str_data)
        .expect("Something goes wrong");
}

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
struct Data {
    value: u8
}

impl<'de> Deserialize<'de> for Data {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_map(DataVisitor);
    }
}

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")
    }
    
    // On défini la variante map pour notre visiteur
    fn visit_map<A>(self, mut map: 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
        })
    }
}

fn main() {
    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 :

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 :

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
struct Data {
    value: u8,
    value2: bool
}

impl<'de> Deserialize<'de> for Data {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_map(DataVisitor);
    }
}

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")
    }
    
    // On défini la variante map pour notre visiteur
    fn visit_map<A>(self, mut map: 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
        })
    }
}

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)
}

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
struct Data {
    value: u8,
    value2: bool
}

impl<'de> Deserialize<'de> for Data {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_map(DataVisitor);
    }
}

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")
    }
    
    // On défini la variante map pour notre visiteur
    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
    where
        A: MapAccess<'de>,
    {
        // on initialise les différents champs
        let mut value = None::<u8>;
        let mut value2 = None::<bool>;

        // on boucle sur chaque entrées
        while let Some(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 = 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"));
        };

        Ok(Data {
            value,
            value2
        })
    }
}

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
impl<'de> Visitor<'de> for PersonVisitor {
    type Value = Person;

    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
        formatter.write_str("expecting ")
    }

    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
    where
        A: MapAccess<'de>,
    {
        let mut name = None::<String>;
        let mut age = None::<u8>;

        while let Some(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 = if let Some(x) = name {
            x
        } else {
            return Err(Error::missing_field("name"));
        };
        let age = if let Some(x) = age {
            x
        } else {
            return Err(Error::missing_field("age"));
        };

        Ok(Person { name, age })
    }
}

impl<'de> Deserialize<'de> for Person {
    fn deserialize<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
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
    A: MapAccess<'de>,
{
    let mut value = None::<u8>;
    let mut value2 = None::<bool>;
    let mut person = None::<Person>;

    while let Some(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 = 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"));
    };
    // Le nouveau champ
    let person = if let Some(x) = person {
        x
    } else {
        return Err(Error::missing_field("person"));
    };

    Ok(Data {
        value,
        value2,
        person,
    })
}

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
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
    A: MapAccess<'de>,
{
    let mut name = None::<String>;
    let mut age = None::<u8>;
    let mut addresses = None::<Vec<String>>;

    while let Some(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 = if let Some(x) = name {
        x
    } else {
        return Err(Error::missing_field("name"));
    };
    let age = if let Some(x) = age {
        x
    } else {
        return Err(Error::missing_field("age"));
    };
    let addresses = if let Some(x) = addresses {
        x
    } else {
        return Err(Error::missing_field("addresses"));
    };

    Ok(Person {
        name,
        age,
        addresses,
    })
}

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
use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
use serde::{Deserialize, Deserializer};
use std::fmt::Formatter;

#[derive(Debug)]
struct Person {
    name: String,
    age: u8,
    addresses: Vec<String>,
}

#[derive(Debug)]
struct Data {
    person: Person,
}

struct DataVisitor;
struct PersonVisitor;

impl<'de> Visitor<'de> for PersonVisitor {
    type Value = Person;

    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
        formatter.write_str("expecting ")
    }

    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
    where
        A: MapAccess<'de>,
    {
        let mut name = None::<String>;
        let mut age = None::<u8>;
        let mut addresses = None::<Vec<String>>;

        while let Some(key) = map.next_key::<&str>()? {
            match key {
                "name" => name = Some(map.next_value::<String>()?),
                "age" => age = Some(map.next_value::<u8>()?),
                "addresses" => {
                    addresses = Some(map.next_value::<Vec<String>>()?);
                }
                _ => {
                    map.next_value::<IgnoredAny>()?;
                }
            }
        }

        let name = if let Some(x) = name {
            x
        } else {
            return Err(Error::missing_field("name"));
        };
        let age = if let Some(x) = age {
            x
        } else {
            return Err(Error::missing_field("age"));
        };
        let addresses = if let Some(x) = addresses {
            x
        } else {
            return Err(Error::missing_field("addresses"));
        };

        Ok(Person {
            name,
            age,
            addresses,
        })
    }
}

impl<'de> Visitor<'de> for DataVisitor {
    type Value = Data;

    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>,
    {

        let mut value = None::<u8>;
        let mut value2 = None::<bool>;
        let mut person = None::<Person>;

        while let Some(key) = map.next_key::<&str>()? {
            match key {
                "person" => person = Some(map.next_value::<Person>()?),
                _ => {
                    let _ = map.next_value::<IgnoredAny>()?;
                }
            }
        }


        let person = if let Some(x) = person {
            x
        } else {
            return Err(Error::missing_field("person"));
        };

        Ok(Data {
            person,
        })
    }
}

impl<'de> Deserialize<'de> for Person {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_map(PersonVisitor)
    }
}

impl<'de> Deserialize<'de> for Data {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_map(DataVisitor)
    }
}

pub 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);

    let str_data = r#"{
    "person": {
        "name" : "Bob",
        "age" : 42,
        "addresses": [
            "test",
            "test2"
        ]
    }
}"#;
    let data = serde_json::from_str::<Data>(str_data).expect("Something goes wrong");
    dbg!(data);
}

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 cas visit_str et visit_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 :

Cliquez pour dérouler
use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
use serde::{Deserialize, Deserializer};
use std::fmt::Formatter;

#[derive(Debug)]
enum Housing {
    House,
    Flat,
    Other(String),
}

#[derive(Debug)]
struct Data {
    housing: Housing,
}

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")
    }

    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)),
        }
    }

    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"))
        }
    }
}

struct DataVisitor;

impl<'de> Visitor<'de> for DataVisitor {
    type Value = Data;

    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>,
    {
        let mut housing = None::<Housing>;
        while let Some(key) = map.next_key()? {
            match key {
                "housing" => housing = Some(map.next_value::<Housing>()?),
                _ => {
                    map.next_value::<IgnoredAny>()?;
                }
            }
        }

        let housing = if let Some(x) = housing {
            x
        } else {
            return Err(Error::missing_field("housing"));
        };

        Ok(Data { housing })
    }
}

impl<'de> Deserialize<'de> for Data {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_map(DataVisitor)
    }
}

impl<'de> Deserialize<'de> for Housing {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_any(HousingVisitor)
    }
}

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

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

Code Rust
use serde::ser::{SerializeMap, SerializeTupleVariant};
use serde::{Serialize, Serializer};

enum Housing {
    House,
    Flat,
    Other(String),
}

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()
    }
}

struct Person {
    name: String,
    age: u8,
    housings: Vec<Housing>,
}

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()
    }
}

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());
}

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:

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
use crate::{Deserialize, Serialize};
use regex::Regex;
use serde::de::{Error, Visitor};
use serde::Deserializer;
use std::fmt::Formatter;
use std::num::ParseIntError;

#[derive(Serialize, Deserialize, Debug)]
struct Color {
    r: u8,
    b: u8,
    g: u8,
}

#[derive(Serialize, Deserialize, Debug)]
struct Data {
    #[serde(deserialize_with = "deserialize_color")]
    color: Color,
    shape: Shape,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "kind", content = "details")]
enum Shape {
    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()))
            })
    };
}

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()
}

// -- Parsing

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)
}

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)
}

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)
}

// -- Visitor custom
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"))
    }
}

fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
where
    D: Deserializer<'de>,
{
    deserializer.deserialize_str(ColorDeserializer)
}

// --- Main

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<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 ! ❀