https://lafor.ge/feed.xml

Serde

2022-09-17

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>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.

{% note(title=""() %} Nous verrons dans une partie suivante comment créer notre propre Deserializer {% end %}

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 :

  • 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
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 :

{% detail(title=) %}

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

{% end %}

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

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:

  • 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
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 ! ❤️

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.