https://lafor.ge/feed.xml

Builder Rust

2022-07-13

Bonjour à toutes et tous :)

Pour ceux qui ne me connaissent pas, je suis à la fois fainéant et travailleur lorsque cela me permet de ne rien faire plus tard. 😂

Aujourd’hui je vais vous conter la merveilleuse épopée d’un développement qui m’a pris 3 jours pour quelque chose qui nécessitait que 30min. 🙃

Builder

Dans de très nombreux langages de programmation et en particulier ceux qui possèdent un typage statique, il est possible de définir des structures de données possédant plusieurs champs.

Pour générer cette structure, il faut généralement passer par un constructeur qui vient réaliser les opérations de définition des différents champs.

À tout hasard en Rust 😄

#[derive(Debug, PartialEq)]
struct Foo {
  a: u8,
  b: f32
}

impl Foo {
  pub fn new(a: u8, b: f32) -> Self {
    Foo {
        a,
        b 
    }   
  }
}

#[test]
fn test() {
  let foo : Foo = Foo::new(12, 45.5);
  let expected = Foo {
    a: 12,
    b: 45.5
  };
  assert(foo, expected);
}

Si l’on veut rajouter un champ c: bool à notre structure nous allons être obligés de modifier le code en conséquence:

struct Foo {
  a: u8,
  b: f32,
  c: bool
}

impl Foo {
  pub fn new(a: u8, b: f32, c: bool) -> Self {
    Foo {
        a,
        b,
        c
    }   
  }
}

Bref, ce n’est pas très passionnant. 😑

Pourquoi ne mettrions-nous pas en place une syntaxe proche des builders de classes comme en Java ?

let foo = Foo::builder()
            .a(12)
            .b(45.5)
            .c(true)
            .build();

Ceci s’appelle le Design Pattern: Builder

Cahier des charges

Ok! Nous avons défini notre API, mais quelles contraintes veut-on pour notre système ?

Obliger la définition des champs

Contrainte 1

Tous les champs doivent être définis avant que l’appel à la méthode build ne soit possible.

Dans notre cas cela signifie que l’on peut soit réaliser le trajet:

  flowchart LR
    builder-->a-->b-->build

ou

  flowchart LR
    builder-->b-->a-->build

Mais celui-ci:

  flowchart LR
    builder-->a-->build

Car cela signifierait que le champ b n’a pas de valeur définie !!

Permettre l’existence de champs optionels

Contrainte 2

Notre structure peut posséder des champs optionnels.

struct Foo {
  a: u8,
  b: f32,
  optional: Vec<i8>
}

Ici le champ optional est optionel, il n’est pas obligatoire de le définir si l’on le désire.

Ce qui signifie qu’aussi bien:

let foo = Foo::builder()
            .a(12)
            .b(45.5)
            .build();

que:

let foo = Foo::builder()
            .a(12)
            .b(45.5)
            .optional(vec![15, -45])
            .build();

sont valides.

De même

let foo = Foo::builder()
            .a(12)
            .optional(vec![15, -45])
            .b(45.5)
            .build();

ou

let foo = Foo::builder()
            .b(45.5)
            .optional(vec![15, -45])
            .a(12)
            .build();

ou des trucs absurdes mais justes, comme:

let foo = Foo::builder()
            .optional(vec![15, -45])
            .optional(vec![15, -45, 33])
            .a(12)
            .b(45.5)
            .build();

et

let foo = Foo::builder()
            .optional(vec![15, -45])
            .a(12)
            .optional(vec![15, -45, 33])
            .b(45.5)
            .build();

le sont aussi !

Notre graphe devient un poil plus complexe:

  flowchart LR

    optional_builder[optional]

    builder-->optional_builder
    optional_builder-->optional_builder
    optional_builder-->a

    optional_a[optional]

    a-->optional_a
    optional_a-->optional_a
    optional_a-->b
    
    optional-->optional
    optional-->build
    b-->optional

    builder-->b
    b-.->a
    a-->build

    builder-->a
    a-.->b
    b-->build

Du fait que notre appel à optional peut-être n’importe où entre les appels des méthodes builder et build.

Et encore nous n’avons qu’un seul champ optionel pour le moment. 😋

Vous sentez la combinatoire bien énervée qui s’approche ? 😈

Mais il y a un problème: le cycle en pointillé.

Dans les faits, il permet de faire des choses comme:

  flowchart LR
    builder-->a-->build

ou

  flowchart LR
    builder-->b-->build

Ce qui est interdit par notre première règle!

L’ordre d’appel des champ obligatoire est fixe

Nous allons donc nous imposer une 3ème règle.

Contrainte 3

Les champs obligatoires doivent être définis dans un ordre précis.

Nous voulons forcer le graphe suivant:

  flowchart LR
    builder-->a-->b-->build

et interdire:

  flowchart LR
    builder-->b-->a-->build

Ce qui nous donne un graphe des enfers un peu moins infernal

  flowchart LR

    optional_builder[optional]

    builder-->optional_builder
    optional_builder-->optional_builder
    optional_builder-->a

    optional_a[optional]

    a-->optional_a
    optional_a-->optional_a
    optional_a-->b
    
    optional-->optional
    optional-->build
    b-->optional

    builder-->a
    a-->b
    b-->build

qui respecte nos deux règles 🥳

Et juste pour rire, je vous montre un graphe avec l’introduction d’un champ optional2.

  flowchart LR

    optional_builder[optional]

    builder-->optional_builder
    optional_builder-->optional_builder
    optional_builder-->a

    optional_builder2[optional2]
    optional_a[optional]

    a-->optional_a
    optional_a-->optional_a
    optional_a-->b
    
    optional-->optional
    optional-->build
    b-->optional

    builder-->optional_builder2
    optional_builder2-->optional_builder2
    optional_builder2-->a
    optional_builder-->optional_builder2
    optional_builder2-->optional_builder

    optional_a2[optional2]

    a-->optional_a2
    optional_a2-->optional_a2
    optional_a2-->b


    
    optional2-->optional2
    optional2-->build
    b-->optional2

    optional_a-->optional_a2
    optional_a2-->optional_a




    optional-->optional2
    optional2-->optional


    builder-->a
    a-->b
    b-->build

C’est harmonieux non ? 😍

Doit échouer à la compilation pas au runtime

Contraite 4

Un processus de build incorrect ou inachevé ne doit pas permettre de compiler

Il est essentiel d’être averti le plus rapidement possible lorsqu’un code est incorrect.

Ainsi à la différence de certain builders qui imposent d’unwrap le résultat.

Nous désirons que ce soit à l’étape de compilation que nous soyons avertis, lorsqu’un champ obligatoire n’a pas, ou a été incorrectement défini.

Implémentation naïve

Implémentons à la main le cas le plus simple:

J’aime bien travailler en utilisant le TDD, on va donc développer avec lui. 😁

#[derive(Debug, PartialEq, Default)]
struct Foo {
  a: u8,
  b: f32
}

#[test]
fn basic() {
  let foo = Foo::builder()
            .a(12)
            .b(45.5)
            .build();

  let expected = Foo {
    a: 12,
    b: 45.5
  };
  assert_eq!(foo, expected);
}

La première chose c’est de doter notre struct Foo d’une méthode statique builder. Le plus facile. ^^

impl Foo {
  fn builder() {}
}

Bon Ok, ça nous avance pas à grand chose. 😛

Définissons maintenant notre Builder.

#[derive(Default)]
struct Builder;
impl Foo {
  fn builder() -> Builder {
    Builder::default()
  }
}

Nos appels à a(), b() et build() ne sont toujours pas disponibles.

Remédions à ça:

#[derive(Default)]
struct Builder {
  a: u8,
  b: f32
}

impl Builder {
  
  pub fn a(self, _a: u8) -> Self {
    self
  } 

  pub fn b(self, _b: f32) -> Self {
    self
  }
  
  pub fn build(self) -> Foo {
    Foo::default()
  }
}

Pas sûr que cela passe les tests unitaires, mais maintenant, ça compile. 😎

'assertion failed: `(left == right)
Left:  Foo { a: 0, b: 0.0 }
Right: Foo { a: 12, b: 45.5 }

Et en effet non.

On doit donc propager les valeurs jusqu’au build de Foo.

La méthode la plus simple est de réaliser quelque chose comme:

impl Builder {
  pub fn a(self, a: u8) -> Self {
    Builder {
      a,
      b: self.b
    }
  }
}

Le self.b permet de propager la valeur dans le Builder suivant.

On peut faire pareil pour l’autre méthode b.

impl Builder {
  pub fn b(self, b: f32) -> Self {
    Builder {
      a: self.a,
      b
    }
  }
}

Et maintenant nous pouvons implémenter notre build.

impl Builder {
  pub fn build(self) -> Foo {
    Foo {
      a: self.a,
      b: self.b
    }
  }
}

On résume

Code Rust
#[derive(Debug, PartialEq, Default)]
struct Foo {
    a: u8,
    b: f32,
}

impl Foo {
    fn builder() -> Builder {
        Builder::default()
    }
}

#[derive(Default)]
struct Builder {
    a: u8,
    b: f32,
}

impl Builder {
    pub fn a(self, a: u8) -> Self {
        Builder { a, b: self.b }
    }

    pub fn b(self, b: f32) -> Self {
        Builder { a: self.a, b }
    }

    pub fn build(self) -> Foo {
        Foo {
            a: self.a,
            b: self.b,
        }
    }
}

#[test]
fn basic() {
    let foo = Foo::builder().a(12).b(45.5).build();

    let expected = Foo { a: 12, b: 45.5 };
    assert_eq!(foo, expected);
}

Bon le test est au vert ✅

Par contre

Certains de nos tests fonctionnent alors qu’ils ne le devraient pas et inversement.

// compilation réussi, mais ne devrait pas être possible ❌
// le seul order possible devrait être builder->a->b->build
// contrainte 3 non respectée
#[test]
fn inverted() {
    let foo = Foo::builder().b(45.5).a(12).build();

    let expected = Foo { a: 12, b: 45.5 };
    assert_eq!(foo, expected);
}

// compile mais fail ❌
// la compilation réussi alors qu'il manque l'appel à b()
// contrainte 1 non respectée
#[test]
fn without_b() {
    let foo = Foo::builder().a(12).build();

    let expected = Foo { a: 12, b: 45.5 };
    assert_eq!(foo, expected);
}

// compile mais fail ❌
// la compilation réussi alors qu'il manque l'appel à a()
// contrainte 1 non respectée
// contrainte 3 non respectée
#[test]
fn without_a() {
    let foo = Foo::builder().b(45.5).build();

    let expected = Foo { a: 12, b: 45.5 };
    assert_eq!(foo, expected);
}

Nous sommes dans le cas

  flowchart LR
    builder-->a
    builder-->b
    a-->b
    b-->a
    b-->build
    a-->build

Alors que l’on veut

  flowchart LR
builder-->a-->b-->build

Nous devons contraindre nos états.

Rage on the State Machine

Cette notion d’états et de transitions se nomme une Machine à états.

En Rust, nous allons matérialiser nos états par des structures. Et les transitions par des appels aux méthodes de ces structures.

  flowchart LR
  fin([Foo construit])
  start([Foo à construire])
  start-->|"builder()"|Init
  Init[Init ou Builder]-->|"a()"|WithA
  WithA-->|"b()"|Buildable
  Buildable-->|"build()"|fin

Déclarons nos structures:

struct WithA {
  a: u8,
  b: f32,
}

struct Buildable {
  a: u8,
  b: f32,
}

Notre Init est le Builder en lui-même, nous pouvons donc l’ignorer (pour l’instant 🤐).

Et maintenant, au tour des transitions !

impl Builder {
    pub fn a(self, a: u8) -> WithA {
        WithA { a, b: self.b }
    }
}

impl WithA {
    pub fn b(self, b: f32) -> Buildable {
        Buildable { a: self.a, b }
    }
}

impl Buildable {
  pub fn build(self) -> Foo {
        Foo {
            a: self.a,
            b: self.b,
        }
    }
}

Bon mieux 😀

// compile et réussi ✅
#[test]
fn basic() {}

// ne compile plus ✅
#[test]
fn inverted() {}

// ne compile plus ✅
#[test]
fn without_b() {}

// ne compile plus ✅
#[test]
fn without_a() {}

Nous venons de contraindre notre système !

Récapitulons:

Code Rust
#[derive(Debug, PartialEq, Default)]
struct Foo {
    a: u8,
    b: f32,
}

impl Foo {
    fn builder() -> Builder {
        Builder::default()
    }
}

#[derive(Default)]
struct Builder {
    a: u8,
    b: f32,
}

struct WithA {
    a: u8,
    b: f32,
}

struct Buildable {
    a: u8,
    b: f32,
}

impl Builder {
    pub fn a(self, a: u8) -> WithA {
        WithA { a, b: self.b }
    }
}

impl WithA {
    pub fn b(self, b: f32) -> Buildable {
        Buildable { a: self.a, b }
    }
}

impl Buildable {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a,
            b: self.b,
        }
    }
}

#[test]
fn basic() {
    let foo = Foo::builder().a(12).b(45.5).build();

    let expected = Foo { a: 12, b: 45.5 };
    assert_eq!(foo, expected);
}

La répétition c’est le mal!

Dans la programmation, s’il y a bien une chose qui est insupportable, c’est la répétion.

Imaginez, ici nous n’avons que 2 champs, mais avec trois champs, ça demande d’écrire quelque chose comme ceci:

#[derive(Default)]
struct Builder {
    a: u8,
    b: f32,
    c: bool,
}

struct WithA {
    a: u8,
    b: f32,
    c: bool,
}

struct WithB {
    a: u8,
    b: f32,
    c: bool,
}

struct Buildable {
    a: u8,
    b: f32,
}

Ça devient fastidieux! Et rappellez vous, le maître mot qui nous guide durant notre épopée est la fainéantise. 😅

Qu’utilise-t-on en Rust lorsque l’on manipule des structures avec des champs qui peuvent avoir des types variables?

Les génériques bien sûr !!

Réécrivons notre code dans ce sens:


struct Init;
struct WithA;
struct Buildable;

#[derive(Default)]
struct Builder<T> {
    a: u8,
    b: f32,
}

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        Builder::<WithA> { a, b: self.b }
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<Buildable> {
        Builder::<Buildable> { a: self.a, b }
    }
}

impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a,
            b: self.b,
        }
    }
}

Et oui! Init est de retour, je vous avais bien dit qu’on allait s’en occuper plus tard ^^.

Compilons !

| struct Builder<T> {
|                ^ unused parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`
= help: if you intended `T` to be a const parameter, use `const T: usize` instead

Hum presque 😥

Mais heureusement le compilateur nous met sur la voie. Il faut que nous rajoutions un champ supplémentaire qui va tenir l’état de la structure.

Ce type un peu spécial, se nomme un PhantomData et ne sert que de marqueur permettant de tenir notre <T>.

#[derive(Default)]
struct Builder<T> {
    a: u8,
    b: f32,
    state: PhantomData<T>,
}

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        Builder::<WithA> { 
            a, 
            b: 
            self.b, state: Default::default() 
        }
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<Buildable> {
        Builder::<Buildable> { 
            a: self.a, 
            b, 
            state: Default::default() 
        }
    }
}

impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a,
            b: self.b,
        }
    }
}

Bon, cela commence à ressembler à quelque chose sans trop de répétitions.

Comme de coutume, un nouveau milestone !

Code Rust
use std::marker::PhantomData;

#[derive(Debug, PartialEq, Default)]
struct Foo {
    a: u8,
    b: f32,
}

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

#[derive(Default)]
struct Init;
struct WithA;
struct WithB;
struct Buildable;

#[derive(Default)]
struct Builder<T> {
    a: u8,
    b: f32,
    state: PhantomData<T>,
}

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        Builder::<WithA> {
            a,
            b: self.b,
            state: Default::default(),
        }
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<Buildable> {
        Builder::<Buildable> {
            a: self.a,
            b,
            state: Default::default(),
        }
    }
}

impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a,
            b: self.b,
        }
    }
}

#[test]
fn basic() {
    let foo = Foo::builder()
    .a(12)
    .b(45.5)
    .build();

    let expected = Foo { 
        a: 12, 
        b: 45.5 
    };
    assert_eq!(foo, expected);
}

Vous reprendrez bien un peu d’options ?

Maintenant que les critères 1 & 3 de notre cahier des charges sont remplis (échec à la compilation et tous les champs obligatoires doivent avoir été appellés).

Nous pouvons réécrire notre machine à états pour y inclure notre appel à optional(), pour chaque état de la machine.

  flowchart LR
  
  fin([Foo construit])
  start([Foo à construire])

  start-->|"builder()"|Init
  Init[Init ou Builder]-->|"a()"|WithA
  WithA-->|"b()"|Buildable
  Buildable-->|"build()"|fin

  Init-->|"optional()"|Init
  WithA-->|"optional()"|WithA
  Buildable-->|"optional()"|Buildable

Établissons nos tests, nous sommes en TDD, ne perdons pas les bonnes habitudes. 😉

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .optional(vec![45, -78])
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![45, -78],
    };
    assert_eq!(foo, expected);
}

Pour y parvenir, nous allons profiter d’avoir une structure générique.

En effet si l’on écrit:


struct Builder<T> {
    a: u8,
    b: f32,
    optional : Vec<i8>,
    state: PhantomData<T>
}

impl Builder<T> {
    pub fn optional(optional: Vec<i8>) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            optional
        }
    }
}

On crée une fonction optional() qui :

  • si elle est appellée par Builder<Init>, renverra un nouveau Builder<Init>
  • si elle est appellée par Builder<WithA>, renverra un nouveau Builder<WithA>
  • et de manière générale, si elle est appellée par Builder<T>, elle renverra un nouveau Builder<T>

Réécrivons notre code:

use std::marker::PhantomData;

#[derive(Debug, PartialEq, Default)]
struct Foo {
    a: u8,
    b: f32,
    optional: Vec<i8>,
}

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

#[derive(Default)]
struct Init;
struct WithA;
struct WithB;
struct Buildable;

#[derive(Default)]
struct Builder<T> {
    a: u8,
    b: f32,
    optional: Vec<i8>,
    state: PhantomData<T>,
}

impl<T> Builder<T> {
    pub fn optional(self, optional: Vec<i8>) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            optional,
            state: Default::default(),
        }
    }
}

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        Builder::<WithA> {
            a,
            b: self.b,
            optional: self.optional,
            state: Default::default(),
        }
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<Buildable> {
        Builder::<Buildable> {
            a: self.a,
            b,
            optional: self.optional,
            state: Default::default(),
        }
    }
}

impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a,
            b: self.b,
            optional: self.optional,
        }
    }
}

#[test]
fn basic() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![],
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .optional(vec![45, -78])
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![45, -78],
    };
    assert_eq!(foo, expected);
}

Nos deux tests sont au verts ! ✅

Essayons de voir si notre méthode optional() peut être placée n’importe où.

Code Rust
#[test]
fn with_optional_before_a() {
    let foo = Foo::builder()
        .a(12)
        .optional(vec![45, -78])
        .b(45.5)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![45, -78],
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_optional_before_builder() {
    let foo = Foo::builder()
        .optional(vec![45, -78])
        .a(12)
        .b(45.5)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![45, -78],
    };
    assert_eq!(foo, expected);
}

#[test]
// On vérifie que le dernier appel à optional est celui qui détermine
// la valeur du champ final
fn with_optional_duplicated() {
    let foo = Foo::builder()
        .optional(vec![45, -78])
        .a(12)
        .b(45.5)
        .optional(vec![45, -78, -1])
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![45, -78, -1],
    };
    assert_eq!(foo, expected);
}

Tout nos tests sont au verts ! ✅

Et si nous avons plus d’un champ optionel ?

Même combat, si on a à la fois optional et optional2, notre machine à état devient:

  flowchart LR
  
  fin([Foo construit])
  start([Foo à construire])

  start-->|"builder()"|Init
  Init[Init ou Builder]-->|"a()"|WithA
  WithA-->|"b()"|Buildable
  Buildable-->|"build()"|fin

  Init-->|"optional()"|Init
  WithA-->|"optional()"|WithA
  Buildable-->|"optional()"|Buildable

  Init-->|"optional2()"|Init
  WithA-->|"optional2()"|WithA
  Buildable-->|"optional2()"|Buildable

Ce qui nous donne l’implémentation suivante:

use std::marker::PhantomData;

#[derive(Debug, PartialEq, Default)]
struct Foo {
    a: u8,
    b: f32,
    optional: Vec<i8>,
    optional2: bool,
}

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

#[derive(Default)]
struct Init;
struct WithA;
struct WithB;
struct Buildable;

#[derive(Default)]
struct Builder<T> {
    a: u8,
    b: f32,
    optional: Vec<i8>,
    optional2: bool,
    state: PhantomData<T>,
}

impl<T> Builder<T> {
    pub fn optional(self, optional: Vec<i8>) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }

    pub fn optional2(self, optional2: bool) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            optional: self.optional,
            optional2,
            state: Default::default(),
        }
    }
}

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        Builder::<WithA> {
            a,
            b: self.b,
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<Buildable> {
        Builder::<Buildable> {
            a: self.a,
            b,
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a,
            b: self.b,
            optional: self.optional,
            optional2: self.optional2,
        }
    }
}

#[test]
fn basic() {
    let foo = Foo::builder().a(12).b(45.5).build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .optional(vec![45, -78])
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_optional_before_a() {
    let foo = Foo::builder()
        .a(12)
        .optional(vec![45, -78])
        .b(45.5)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_optional_before_builder() {
    let foo = Foo::builder()
        .optional(vec![45, -78])
        .a(12)
        .b(45.5)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_optional_duplicated() {
    let foo = Foo::builder()
        .optional(vec![45, -78])
        .a(12)
        .b(45.5)
        .optional(vec![45, -78, -1])
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![45, -78, -1],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_more_than_one_optional_field() {
    let foo = Foo::builder()
        .a(12)
        .optional2(true)
        .b(45.5)
        .optional(vec![45, -78])
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        optional: vec![45, -78],
        optional2: true,
    };
    assert_eq!(foo, expected);
}

Je vous passe la phase de TDD, mais oui c’est du vert aussi. 😛

Petite amélioration

Nous avons supposé que tous les champs de notre structure implémentaient le trait Default.

Mais ce n’est pas forcément vrai.

Code Rust
use std::marker::PhantomData;


#[derive(Debug, PartialEq)]
struct Bar;

#[derive(Debug, PartialEq)]
struct Foo {
    a: u8,
    b: f32,
    c: Bar,
    optional: Vec<i8>,
    optional2: bool,
}

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

#[derive(Default)]
struct Init;
struct WithA;
struct WithB;
struct Buildable;

#[derive(Default)]
struct Builder<T> {
    a: u8,
    b: f32,
    c: Bar,
    optional: Vec<i8>,
    optional2: bool,
    state: PhantomData<T>,
}

impl<T> Builder<T> {
    pub fn optional(self, optional: Vec<i8>) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            c: self.c,
            optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }

    pub fn optional2(self, optional2: bool) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            c: self.c,
            optional: self.optional,
            optional2,
            state: Default::default(),
        }
    }
}

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        Builder::<WithA> {
            a,
            b: self.b,
            c: self.c,
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<WithB> {
        Builder::<WithB> {
            a: self.a,
            b,
            c: self.c,
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<WithB> {
    pub fn c(self, c: Bar) -> Builder<Buildable> {
        Builder::<Buildable> {
            a: self.a,
            b: self.b,
            c,
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a,
            b: self.b,
            c: self.c,
            optional: self.optional,
            optional2: self.optional2,
        }
    }
}

Ce code ne compile pas:

28 | #[derive(Default)]
   |          ------- in this derive macro expansion
...
32 |     c: Bar,
   |     ^^^^^^ the trait `Default` is not implemented for `Bar`
   |
   = note: this error originates in the derive macro `Default` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Bar` with `#[derive(Default)]`

Cargo n’est pas content. 😥

Soit nous imposons que tous les champs implémentent Default, et du coup on impose une contrainte supplémentaire et non nécessaire.

Ou plus malin, on s’appuie sur les Optional et on réécrit les choses ainsi:

Code Rust
use std::marker::PhantomData;


#[derive(Debug, PartialEq)]
struct Bar;

#[derive(Debug, PartialEq)]
struct Foo {
    a: u8,
    b: f32,
    c: Bar,
    optional: Vec<i8>,
    optional2: bool,
}

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

#[derive(Default)]
struct Init;
struct WithA;
struct WithB;
struct Buildable;

#[derive(Default)]
struct Builder<T> {
    a: Option<u8>,
    b: Option<f32>,
    c: Option<Bar>,
    optional: Vec<i8>,
    optional2: bool,
    state: PhantomData<T>,
}

impl<T> Builder<T> {
    pub fn optional(self, optional: Vec<i8>) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            c: self.c,
            optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }

    pub fn optional2(self, optional2: bool) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            c: self.c,
            optional: self.optional,
            optional2,
            state: Default::default(),
        }
    }
}

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        Builder::<WithA> {
            a: Some(a),
            b: self.b,
            c: self.c,
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<WithB> {
        Builder::<WithB> {
            a: self.a,
            b: Some(b),
            c: self.c,
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<WithB> {
    pub fn c(self, c: Bar) -> Builder<Buildable> {
        Builder::<Buildable> {
            a: self.a,
            b: self.b,
            c: Some(c),
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a.unwrap(),
            b: self.b.unwrap(),
            c: self.c.unwrap(),
            optional: self.optional,
            optional2: self.optional2,
        }
    }
}

#[test]
fn basic() {
    let foo = Foo::builder().a(12).b(45.5).c(Bar).build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .c(Bar)
        .optional(vec![45, -78])
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_optional_before_a() {
    let foo = Foo::builder()
        .a(12)
        .optional(vec![45, -78])
        .b(45.5)
        .c(Bar)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_optional_before_builder() {
    let foo = Foo::builder()
        .optional(vec![45, -78])
        .a(12)
        .b(45.5)
        .c(Bar)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_optional_duplicated() {
    let foo = Foo::builder()
        .optional(vec![45, -78])
        .a(12)
        .b(45.5)
        .c(Bar)
        .optional(vec![45, -78, -1])
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78, -1],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

#[test]
fn with_more_than_one_optional_field() {
    let foo = Foo::builder()
        .a(12)
        .optional2(true)
        .b(45.5)
        .c(Bar)
        .optional(vec![45, -78])
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: true,
    };
    assert_eq!(foo, expected);
}

Ça compile et en plus nous n’obligeons plus l’implémentation du trait Default sur Bar. Tout est beau dans le meilleur des mondes !

Macros

Je sais pas vous, mais moi je trouve que ce code se répète un peu beaucoup. 🤔

Rajouter un ou plusieurs champs est encore plus fastidieux !

Nous devons trouver un moyen de nous faciliter la tâche.

Ce moyen est tout trouvé en Rust et il se nomme les macros.

J’ai d’ailleurs fait un autre article dessus, si vous voulez découvrir ou vous rafraîchir la mémoire sur ce concept.

Automatiser la création des champs des structures builders

Nous allons y aller pas à pas pour construire notre automatisation.

La première chose que l’on remarque est que les implémentations se ressemblent et se répètent à quelques différences près.

impl<T> Builder<T> {
    pub fn optional(self, optional: Vec<i8>) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            c: self.c,
            optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }

    pub fn optional2(self, optional2: bool) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            c: self.c,
            optional: self.optional,
            optional2,
            state: Default::default(),
        }
    }
}

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        Builder::<WithA> {
            a: Some(a),
            b: self.b,
            c: self.c,
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<Buildable> {
        Builder::<Buildable> {
            a: self.a,
            b: Some(b),
            c: self.c,
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

On peut déjà sortir deux cas:

Si l’on traite des champs obligatoires, nous avons deux possibilités:

  • champ : self.champ : cas où l’on désire transmettre la valeur du champ de l’étape précédente vers l’étape suivante sans modification
  • champ: Some(champ) : cas où l’on désire configurer la valeur du champ pour l’étape suivante avec la valeur en entrée.

Si l’on traite des champs optionels, nous avons aussi deux possibilités:

  • champ : self.champ : même principe que pour les champs optionnels
  • champ : on réalise un remplacement de la valeur pour la suite des opérations

En Rust, il est possible d’omettre le nom du champ si la variable s’appelle pareil.

Nous pouvons construire une macro qui vient réaliser ce travail à notre place.

La première chose à se demander: comment discriminer les deux cas ?

Le critère est le nom du champ, si le nom du champ est égal au nom du paramètre, alors on doit procéder au remplacement de la valeur, sinon à sa propagation.

Première tentative (ça marchera pas ^^’)

Pour notre première tentative, nous allons essayer d’utiliser une condition pour arriver au résultat voulu.

La macro stringify! permet de transformer un ident en string et ainsi de pouvoir l’afficher dans un println!, ou de la comparer avec une autre String.

macro_rules! fill_field {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {
        if stringify!($field_name) == stringify!($target) {
             Some($value)
        } else {
            $self.$field_name
        }
    };
}

#[test]
fn test_field_name_macro() {

    struct A {
        a: Option<u8>,
        b: Option<f32>
    }

    impl A {
        fn set_a(&self, a: u8 ) -> A {
            A {
                a: fill_field!(self, a, a, a),
                b: fill_field!(self, b, a, a),
            }
        }
    }

}

Ce code ne compile pas à cause de la ligne 23.

error[E0308]: mismatched types
   --> src\steps\part2.rs:134:44
    |
23  |                 b: fill_field!(self, b, a, a),
    |                                            ^ expected `f32`, found `u8`
    |
help: you can convert a `u8` to an `f32`, producing the floating point representation of the integer
    |
23  |                 b: fill_field!(self, b, a, a.into()),
    |                                             +++++++

En effet, si on étend cette macro, nous avons les résultats suivants:

Pour le champ a:

if stringify!( a ) == stringify!( a ) {
    Some(a) // type Option<u8>
} else {
    self.a // type Option<u8>
}

Pour le champ b:

if stringify!( b ) == stringify!( a ) {
    Some(a) // type Option<u8>
} else {
    self.b // type Option<f32>
}

Et là ça coince car le code ainsi généré n’est pas valide et ne peut donc pas compiler!

En en effet, notre if faisant réellement partie du code généré, il doit être également analysé et donc ses branches doivent retourner le même type.

Nous sommes plutôt bloqués. Notre seul discriminant étant la correspondance entre le nom de la variable et le champ.

Il nous faut un autre moyen pour ne pas générer tout le code, mais seulement la partie de la condition qui nous intéresse.

Deuxième tentative, une pincée de proc_macro (la bonne)

Le dicton dit bien:

Dicton éculé mais vrai

Si votre seul outil est un marteau, tous vos problèmes seront des clous.

Ici notre marteau représente les macros et les macros sont difficiles à utiliser pour faire des conditions.

Heureusement, il existe un autre type de macro, les proc_macro.

Celles-ci sont du Rust pour écrire du Rust et donc on vient compiler du Rust pour générer du Rust qui lui même sera compilé.

Il va sans dire que les proc_macros peuvent elles mêmes contenir des macros! 😵

Pour réaliser notre génération conditionnelle de code, nous allons utiliser les crates appellées tt-call et tt-equal.

Pour les utiliser, vous devez rajouter à votre cargo.toml les dépendances suivantes:

[dependencies]
tt-call = "1.0.8"
tt-equal = "0.1.2"

Ou si vous êtes en rust > 1.62, vous pouvez les commandes:

cargo add tt-call tt-equal

Maintenant nous pouvons utiliser des macros plus avancées pour faire ce que l’on désire.

La crate tt-call nous fournira une macro tt_if! qui possède une syntaxe spécifique.

tt_if! {
    input = [<A> <B>]
    condition = [{condition}]
    true =  [{...}]
    false = [{...}]
}

La macro prend plusieurs paramètres:

  • <A> et <B> : les expressions qui vont être comparées.
  • condition : quel opération renvoyant un booléen sera appliqué à <A> et <B>.
  • true : code à générer en cas de succès
  • false : code à générer en cas d’échec

La condition que nous allons appliquer est elle-même une macro tt_equal, provenant de la crate tt_equal.

Remplaçons par nos paramètres:

tt_if! {
    input = [$field_name $target]
    condition = [{tt_equal}]
    true =  [{...}]
    false = [{...}]
}

Dans notre cas, nous allons comparer l’égalité entre $field_name:ident et $target:ident.

Pour les branches de notre condition.

Nous avons d’une part:

true = [{Some($value)}]

Et d’autre part:

false = [{$self.$field_name}]

Si on regroupe le tout:

macro_rules! fill_field {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                Some($value)
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

Et si on veut écrire un test pour s’en assurer:

Code Rust
use tt_call::tt_if;
use tt_equal::tt_equal;


macro_rules! fill_field {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                Some($value)
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

#[test]
fn test_field_name_macro() {

    #[derive(Debug, PartialEq)]
    struct A {
        a: Option<u8>,
        b: Option<f32>
    }

    impl A {
        fn set_a(&self, a: u8 ) -> A {
            A {
                a: fill_field!(self, a, a, a),
                b: fill_field!(self, b, a, a),
            }
        }
    }

    let a = A {
        a: None,
        b: None
    };

    let a = a.set_a(42);

    assert_eq!(a, A {
        a: Some(42),
        b: None
    })

}

Nous avons besoin d’une deuxième macro pour gérer le cas des champs optionnels:

macro_rules! fill_field_optional {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                $value
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

Maintenant, au tour de notre implémentation de builder

Code Rust
use std::marker::PhantomData;
use tt_call::tt_if;
use tt_equal::tt_equal;

macro_rules! fill_field {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                Some($value)
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_field_optional {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                $value
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}


#[derive(Debug, PartialEq)]
struct Bar;

#[derive(Debug, PartialEq)]
struct Foo {
    a: u8,
    b: f32,
    c: Bar,
    optional: Vec<i8>,
    optional2: bool,
}

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

#[derive(Default)]
struct Init;
struct WithA;
struct WithB;
struct Buildable;

#[derive(Default)]
struct Builder<T> {
    a: Option<u8>,
    b: Option<f32>,
    c: Option<Bar>,
    optional: Vec<i8>,
    optional2: bool,
    state: PhantomData<T>,
}

impl<T> Builder<T> {
    pub fn optional(self, optional: Vec<i8>) -> Builder<T> {
        Builder::<T> {
            a: fill_field!(self, a, optional, optional),
            b: fill_field!(self, b, optional, optional),
            c: fill_field!(self, c, optional, optional),
            optional: fill_field_optional!(self, optional, optional, optional),
            optional2: fill_field_optional!(self, optional2, optional, optional),
            state: Default::default(),
        }
    }

    pub fn optional2(self, optional2: bool) -> Builder<T> {
        Builder::<T> {
            a: fill_field!(self, a, optional2, optional2),
            b: fill_field!(self, b, optional2, optional2),
            c: fill_field!(self, c, optional2, optional2),
            optional: fill_field_optional!(self, optional, optional2, optional2),
            optional2: fill_field_optional!(self, optional2, optional2, optional2),
            state: Default::default(),
        }
    }
}

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        Builder::<WithA> {
            a: fill_field!(self, a, a, a),
            b: fill_field!(self, b, a, a),
            c: fill_field!(self, c, a, a),
            optional: fill_field_optional!(self, optional, a, a),
            optional2: fill_field_optional!(self, optional2, a, a),
            state: Default::default(),
        }
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<WithB> {
        Builder::<WithB> {
            a: fill_field!(self, a, b, b),
            b: fill_field!(self, b, b, b),
            c: fill_field!(self, c, b, b),
            optional: fill_field_optional!(self, optional, b, b),
            optional2: fill_field_optional!(self, optional2, b, b),
            state: Default::default(),
        }
    }
}

impl Builder<WithB> {
    pub fn c(self, c: Bar) -> Builder<Buildable> {
        Builder::<Buildable> {
            a: fill_field!(self, a, c, c),
            b: fill_field!(self, b, c, c),
            c: fill_field!(self, c, c, c),
            optional: fill_field_optional!(self, optional, c, c),
            optional2: fill_field_optional!(self, optional2, c, c),
            state: Default::default(),
        }
    }
}

impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a.unwrap(),
            b: self.b.unwrap(),
            c: self.c.unwrap(),
            optional: self.optional,
            optional2: self.optional2,
        }
    }
}

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .optional(vec![45, -78])
        .c(Bar)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

Automatiser la création de la stucture elle-même

Il semblerait que notre macro ne nous nous épargne pas l’écriture de beaucoup de lignes. 😕

J’aurai même tendance à penser qu’elle fait l’inverse et nous donne de travail qu’avant.

Si l’Automatisation ne marche pas, c’est qu’on utilise pas suffisamment d’automatisation!!

On va donc appliquer ce grand précepte et écrire une macro qui réalise le travail à notre place ^^

Comme toujours lorsque l’on conçoit une macro, l’idée est de repérer les différences et similitudes entre les codes pour trouver des règles de génération permettant de nous éviter le maximum d’efforts.

Regardons de plus près les structures du corp de notre machine à états.

Code Rust
    pub fn optional(self, optional: Vec<i8>) -> Builder<T> {
        Builder::<T> {
            a: fill_field!(self, a, optional, optional),
            b: fill_field!(self, b, optional, optional),
            c: fill_field!(self, c, optional, optional),
            optional: fill_field_optional!(self, optional, optional, optional),
            optional2: fill_field_optional!(self, optional2, optional, optional),
            state: Default::default(),
        }
    }

    pub fn optional2(self, optional2: bool) -> Builder<T> {
        Builder::<T> {
            a: fill_field!(self, a, optional2, optional2),
            b: fill_field!(self, b, optional2, optional2),
            c: fill_field!(self, c, optional2, optional2),
            optional: fill_field_optional!(self, optional, optional2, optional2),
            optional2: fill_field_optional!(self, optional2, optional2, optional2),
            state: Default::default(),
        }
    }

    pub fn a(self, a: u8) -> Builder<WithA> {
        Builder::<WithA> {
            a: fill_field!(self, a, a, a),
            b: fill_field!(self, b, a, a),
            c: fill_field!(self, c, a, a),
            optional: fill_field_optional!(self, optional, a, a),
            optional2: fill_field_optional!(self, optional2, a, a),
            state: Default::default(),
        }
    }

    pub fn b(self, b: f32) -> Builder<WithB> {
        Builder::<WithB> {
            a: fill_field!(self, a, b, b),
            b: fill_field!(self, b, b, b),
            c: fill_field!(self, c, b, b),
            optional: fill_field_optional!(self, optional, b, b),
            optional2: fill_field_optional!(self, optional2, b, b),
            state: Default::default(),
        }
    }

Comparons le type de retour de optional, optional2, d’une part, et a etb, d’autre part.

Les premières fonctions renvoient systématiquement un Builder<T>.

Tandis que les secondes ont comme type de retour un type différent : ici, respectivement Builder<WithA>, Builder<WithB>.

Maintenant que nous avons mis en lumière les différences, nous allons donc devoir écrire deux macros,

La première, pour les champs obligatoires, prendra un paramètre supplémentaire de type ty, pour variabiliser le type de la structure de sortie.

Pour le corps des deux macros, nous allons réutiliser la macro précédente fill_field!, pour nous faciliter la vie.

Cela nous donne les macros suivantes:

macro_rules! fill_struct {
    ($self:ident, $target:ident, $value:expr, $new_state:ty) => {
        Builder::<$new_state> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

macro_rules! fill_struct_optional {
    ($self:ident, $target:ident, $value:expr) => {
        Builder::<T> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

Code utilisant la macro fill_struct!:

Code Rust
use std::marker::PhantomData;
use tt_call::tt_if;
use tt_equal::tt_equal;

macro_rules! fill_field {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                Some($value)
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_field_optional {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                $value
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_struct {
    ($self:ident, $target:ident, $value:expr, $new_state:ty) => {
        Builder::<$new_state> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

macro_rules! fill_struct_optional {
    ($self:ident, $target:ident, $value:expr) => {
        Builder::<T> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

#[derive(Debug, PartialEq)]
struct Bar;

#[derive(Debug, PartialEq)]
struct Foo {
    a: u8,
    b: f32,
    c: Bar,
    optional: Vec<i8>,
    optional2: bool,
}

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

#[derive(Default)]
struct Init;
struct WithA;
struct WithB;
struct Buildable;

#[derive(Default)]
struct Builder<T> {
    a: Option<u8>,
    b: Option<f32>,
    c: Option<Bar>,
    optional: Vec<i8>,
    optional2: bool,
    state: PhantomData<T>,
}

impl<T> Builder<T> {
    pub fn optional(self, optional: Vec<i8>) -> Builder<T> {
        fill_struct_optional!(self, optional, optional)
    }

    pub fn optional2(self, optional2: bool) -> Builder<T> {
        fill_struct_optional!(self, optional2, optional2)
    }
}

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        fill_struct!(self, a, a, WithA)
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<WithB> {
        fill_struct!(self, b, b, WithB)
    }
}

impl Builder<WithB> {
    pub fn c(self, c: Bar) -> Builder<Buildable> {
        fill_struct!(self, c, c, Buildable)
    }
}

impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a.unwrap(),
            b: self.b.unwrap(),
            c: self.c.unwrap(),
            optional: self.optional,
            optional2: self.optional2,
        }
    }
}

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .optional(vec![45, -78])
        .c(Bar)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

Automatiser les champs de la structure

Notre système commence à être un minimum automatisé, mais la problématique reste entière, si l’on veut rajouter un nouveau champ (optionel ou obligatoire), nous devons écrire les implémentations de la machine à états, ce qui passablement agaçant. 😑

Nous devons touver un moyen de réaliser l’introspection des différents champs de la structure que nous désirons construire.

Introspection des champs de la structure

L’instrospection seule ne nous permettra pas de tout faire, mais à minima de nous donner une base de réflexion pour la suite des opérations.

Pour ce faire nous allons nous inspirer de cet exemple.

Le résultat que l’on tente d’atteindre est le suivant:

#[derive(Default)]
struct Builder<T> {
    a: Option<u8>,
    b: Option<f32>,
    c: Option<Bar>,
    optional: Vec<i8>,
    optional2: bool,
    state: PhantomData<T>,
}

Nous devons aussi distinguer deux cas, les champs optionnels et les champs obligatoires.

Nous avons donc en quelque sorte, deux macros à écrire:

  • La première, qui génère des champs en champ: Option<type de champ>
  • La seconde : champ : type de champ

Pour les distinguer, nous allons rajouter un discriminant permettant à la macro de comprendre ce qu’elle analyse.

Notre macro va se nommer build_builder!. Et aura pour motif de match:

Premier essai de motif de match

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident{
        $($field_name:ident : $field_type:ty,)*
        $($field_name_optional:ident : $field_type_optional:ty, [optional])*
    } ) => {
        println!("la structure {}", stringify!($name));
        $(println!("\ta le champ obligatoire {} a pour type {}", stringify!($field_name), stringify!($field_type)));*;
        $(println!("\ta le champ optionnel {} a pour type {}", stringify!($field_name_optional), stringify!($field_type_optional)));*;
    };
}

#[test]
fn main() {
    build_builder! {
        struct Foo {
            a: u8,
            b: f32,
            c: Bar,
            optional: Vec<i8>, [optional]
            optional2: bool,   [optional]
        }
    }
}

Ce premier essai ne compilera pas et nous provoquera une erreur.

error: local ambiguity when calling macro `build_builder`: 
multiple parsing options: built-in NTs ident ('field_name') or ident ('field_name_optional')

Car les motifs sont trop semblables et le moteur de macro n’est pas capable de faire la distinction entre les champs optionels et obligatoires.

Nous allons devoir rajouter un séparateur entre nos champs obligatoires et optionnels.

Deuxième essai via un séparateur

Prenons par exemple ---.

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident{
        $($field_name:ident : $field_type:ty,)*
        ---
        $($field_name_optional:ident : $field_type_optional:ty, [optional])*
    } ) => {
        println!("la structure {}", stringify!($name));
        $(println!("\ta le champ obligatoire {} a pour type {}", stringify!($field_name), stringify!($field_type)));*;
        $(println!("\ta le champ optionnel {} a pour type {}", stringify!($field_name_optional), stringify!($field_type_optional)));*;
    };
}

#[test]
fn main() {
    build_builder! {
        struct Foo {
            a: u8,
            b: f32,
            c: Bar,
            ---
            optional: Vec<i8>, [optional]
            optional2: bool,   [optional]
        }
    }
}

Cette fois-ci, notre code compile et nous affiche:

la structure Foo
	a le champ obligatoire a a pour type u8
	a le champ obligatoire b a pour type f32
	a le champ obligatoire c a pour type Bar
	a le champ optionnel optional a pour type Vec<i8>
	a le champ optionnel optional2 a pour type bool

On peut même se dispenser des [optional] et remplacer le --- par #[optional fields] pour plus de clarté.

On en profite aussi pour permettre de rajouter optionnellement une virgule au dernier champ de la structure.

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident{
        $($field_name:ident : $field_type:ty,)*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {
        println!("la structure {}", stringify!($name));
        $(println!("\ta le champ obligatoire {} a pour type {}", stringify!($field_name), stringify!($field_type)));*;
        $(println!("\ta le champ optionnel {} a pour type {}", stringify!($field_name_optional), stringify!($field_type_optional)));*;
    };
}

#[test]
fn main() {
    build_builder! {
        struct Foo {
            a: u8,
            b: f32,
            c: Bar,
            #[optional fields]
            optional: Vec<i8>,
            optional2: bool
        }
    }
}

Pas mal, nous sommes désormais capable de réaliser l’introspection des champs de notre structure! 🥳

Génération de la machine à état pour les champs optionnels

Nous avons la stucture générale de macro, nous pouvons maintenant passer à son implémentation.

Pour commencer, nous allons nous attaquer aux champs optionnels.

Le code que nous allons devoir générer est le suivant:

impl<T> Builder<T> {
    pub fn optional(self, optional: Vec<i8>) -> Builder<T> {
        fill_struct_optional!(self, optional, optional)
    }

    pub fn optional2(self, optional2: bool) -> Builder<T> {
        fill_struct_optional!(self, optional2, optional2)
    }
}

Nous avons plusieurs choses à réaliser:

Tout d’abord, nous devons déclarer le impl Builder<T>

Puis, pour chacun des champs optionnels, nous devons générer les fonctions associées.

Pour cela, nous allons là aussi utiliser le principe de répétition. Mais cette fois-ci nous allons répéter le motif de la déclaration de la fonction de définition, et pour chaque champ optionnel.

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident{
        $($field_name:ident : $field_type:ty,)*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {
        impl<T> Builder<T> {

            $(
            pub fn $field_name_optional(self, $field_name_optional: $field_type_optional) -> Builder<T> {
                fill_struct_optional!(self, $field_name_optional, $field_name_optional)
            }
            )*
        }
    };
}

Génération de la machine à état pour les champs obligatoires

Pour pouvoir générer les méthodes permettant de passer d’un état obligatoire à un autre, nous devons connaître l’état courant, ainsi que l’état suivant.

Pour ce faire, nous allons devoir introduire un DSL spécifique, qui permettra de pouvoir déterminer ces états lors de la génération de nos implémentations.

Pour rappel, ces implémentations sont les suivantes:

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        fill_struct!(self, a, a, WithA)
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<WithB> {
        fill_struct!(self, b, b, WithB)
    }
}

impl Builder<WithB> {
    pub fn c(self, c: Bar) -> Builder<Buildable> {
        fill_struct!(self, c, c, Buildable)
    }
}

Notre DSL ressemblera à:

build_builder! {
    struct Foo {
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    }
}

Nous avons choisi une notation en [ État courant => État suivant]. L’idée est de former un graph amenant de l’état Init jusqu’à l’état Buildable.

Tout d’abord, nous venons matcher les états pour chaque champ:

[ $current_state:ident => $next_state:ident ]

Puis pour chacun de ceux-ci, implémenter les différents états de la machine.

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident{
        $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {
            // ... code précédent
            
            $(
                impl Builder<$current_state> {
                    fn $field_name(self, $field_name: $field_type) -> Builder<$next_state> {
                        fill_struct!(self, $field_name, $field_name, $next_state)
                    }
                }
            )*
}

Si on résume:

Code Rust
use std::marker::PhantomData;
use tt_call::tt_if;
use tt_equal::tt_equal;

macro_rules! fill_field {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                Some($value)
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_field_optional {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                $value
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_struct {
    ($self:ident, $target:ident, $value:expr, $new_state:ty) => {
        Builder::<$new_state> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

macro_rules! fill_struct_optional {
    ($self:ident, $target:ident, $value:expr) => {
        Builder::<T> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident{
        $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {
        impl<T> Builder<T> {

            $(
            pub fn $field_name_optional(self, $field_name_optional: $field_type_optional) -> Builder<T> {
                fill_struct_optional!(self, $field_name_optional, $field_name_optional)
            }
            )*

        }

        $(
            impl Builder<$current_state> {
                fn $field_name(self, $field_name: $field_type) -> Builder<$next_state> {
                    fill_struct!(self, $field_name, $field_name, $next_state)
                }
            }
        )*
    };
}

#[derive(Debug, PartialEq)]
struct Bar;

#[derive(Debug, PartialEq)]
struct Foo {
    a: u8,
    b: f32,
    c: Bar,
    optional: Vec<i8>,
    optional2: bool,
}

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

#[derive(Default)]
struct Init;
struct WithA;
struct WithB;
struct Buildable;

#[derive(Default)]
struct Builder<T> {
    a: Option<u8>,
    b: Option<f32>,
    c: Option<Bar>,
    optional: Vec<i8>,
    optional2: bool,
    state: PhantomData<T>,
}



build_builder! {
    struct Foo {
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    }
}


impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a.unwrap(),
            b: self.b.unwrap(),
            c: self.c.unwrap(),
            optional: self.optional,
            optional2: self.optional2,
        }
    }
}

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .optional(vec![45, -78])
        .c(Bar)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

On génère maintenant l’intégralité des transitions de la machine, que ce soit les transitions obligatoires ou optionnelles !!! 😎

Automatiser la construction du Builder en lui-même

Il nous reste encore pas mal de boulot, en effet bien que la structure du builder soit générique pour le moment, nous sommes dans l’obligation de l’écrire par nous même.

Nous allons remédier à cette situation intôlérable ! 😁

Le code que nous allons devoir générer est le suivant:

#[derive(Default)]
struct Builder<T> {
    a: Option<u8>,
    b: Option<f32>,
    c: Option<Bar>,
    optional: Vec<i8>,
    optional2: bool,
    state: PhantomData<T>,
}

Pour ce faire, nous allons continuer à nous appuyer sur la macro build_builder!, celle-ci réalisant l’introspection des noms des champs de la structure, ainsi que leur type.

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident{
        $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {

    #[derive(Default)]
    struct Builder<T> {
        $($field_name: Option<$field_type>),*,
        $($field_name_optional: $field_type_optional),*,
        state: PhantomData<T>,
    }
}

Toujours plus d’automatisation !

Code Rust
use std::marker::PhantomData;
use tt_call::tt_if;
use tt_equal::tt_equal;

macro_rules! fill_field {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                Some($value)
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_field_optional {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                $value
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_struct {
    ($self:ident, $target:ident, $value:expr, $new_state:ty) => {
        Builder::<$new_state> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

macro_rules! fill_struct_optional {
    ($self:ident, $target:ident, $value:expr) => {
        Builder::<T> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident{
        $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {

        #[derive(Default)]
        struct Builder<T> {
            $($field_name: Option<$field_type>),*,
            $($field_name_optional: $field_type_optional),*,
            state: PhantomData<T>,
        }

        impl<T> Builder<T> {

            $(
            pub fn $field_name_optional(self, $field_name_optional: $field_type_optional) -> Builder<T> {
                fill_struct_optional!(self, $field_name_optional, $field_name_optional)
            }
            )*

        }

        $(
            impl Builder<$current_state> {
                fn $field_name(self, $field_name: $field_type) -> Builder<$next_state> {
                    fill_struct!(self, $field_name, $field_name, $next_state)
                }
            }
        )*
    };
}

#[derive(Debug, PartialEq)]
struct Bar;

#[derive(Debug, PartialEq)]
struct Foo {
    a: u8,
    b: f32,
    c: Bar,
    optional: Vec<i8>,
    optional2: bool,
}

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

#[derive(Default)]
struct Init;
struct WithA;
struct WithB;
struct Buildable;


build_builder! {
    struct Foo {
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    }
}


impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a.unwrap(),
            b: self.b.unwrap(),
            c: self.c.unwrap(),
            optional: self.optional,
            optional2: self.optional2,
        }
    }
}

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .optional(vec![45, -78])
        .c(Bar)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

Bon, ça commence à se dégrossir !

Génération des états de la machine

Maintenant que nous avons les transitions, au tour des états.

Pour cela, nous allons devoir enrichir notre DSL pour prendre en compte cette nouvelle problématique.

Le code que nous devons générer est le suivant :

#[derive(Default)]
struct Init;
struct WithA;
struct WithB;
struct Buildable;

Deux états sont fixes pour toutes les implémentations, il s’agit de Init et Buildable. Init doit aussi implémenter le trait Default.

Le DSL que je vous propose est le suivant: nous définissons entre [ et ] après de la structure, tous les états de notre machine, excepté Init et Build.

build_builder! {
    struct Foo [
        WithA,
        WithB
    ]{
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    }
}

Nous allons donc générer les WithA et WithB.

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident [
        $($state:ident),*$(,)*
    ]{
        $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {


        #[derive(Default)]
        struct Init;
        $(struct $state);*;
        struct Buildable;
}

Nous avons désormais intégré les états dans notre DSL.

Code Rust
use std::marker::PhantomData;
use tt_call::tt_if;
use tt_equal::tt_equal;

macro_rules! fill_field {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                Some($value)
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_field_optional {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                $value
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_struct {
    ($self:ident, $target:ident, $value:expr, $new_state:ty) => {
        Builder::<$new_state> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

macro_rules! fill_struct_optional {
    ($self:ident, $target:ident, $value:expr) => {
        Builder::<T> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident [
        $($state:ident),*$(,)*
    ]{
        $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {


        #[derive(Default)]
        struct Init;
        $(struct $state);*;
        struct Buildable;


        #[derive(Default)]
        struct Builder<T> {
            $($field_name: Option<$field_type>),*,
            $($field_name_optional: $field_type_optional),*,
            state: PhantomData<T>,
        }



        impl<T> Builder<T> {

            $(
            pub fn $field_name_optional(self, $field_name_optional: $field_type_optional) -> Builder<T> {
                fill_struct_optional!(self, $field_name_optional, $field_name_optional)
            }
            )*

        }

        $(
            impl Builder<$current_state> {
                fn $field_name(self, $field_name: $field_type) -> Builder<$next_state> {
                    fill_struct!(self, $field_name, $field_name, $next_state)
                }
            }
        )*
    };
}

#[derive(Debug, PartialEq)]
struct Bar;

#[derive(Debug, PartialEq)]
struct Foo {
    a: u8,
    b: f32,
    c: Bar,
    optional: Vec<i8>,
    optional2: bool,
}

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

build_builder! {
    struct Foo [
        WithA,
        WithB
    ]{
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    }
}


impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a.unwrap(),
            b: self.b.unwrap(),
            c: self.c.unwrap(),
            optional: self.optional,
            optional2: self.optional2,
        }
    }
}

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .optional(vec![45, -78])
        .c(Bar)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

Occupons nous de la méthode build

Continuons notre nettoyage de printemps!

La méthode build, permettant de réaliser la construction de la structure finale, doit être encore implémententée manuellement.

Nous allons améliorer les choses en demandant à notre macro de réaliser ce travail à notre place.

Pour rappel, celle-ci a pour définition :

impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a.unwrap(),
            b: self.b.unwrap(),
            c: self.c.unwrap(),
            optional: self.optional,
            optional2: self.optional2,
        }
    }
}

Nous allons utiliser une macro pour automatiser sa création.

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident [
        $($state:ident),*$(,)*
    ]{
        $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {

    impl Builder<Buildable> {
        pub fn build(self) -> $name {
            $name {
                $($field_name: self.$field_name.unwrap()),*,
                $($field_name_optional: self.$field_name_optional),*,
            }
        }
    }    
    
}

Ainsi, nous n’aurons plus besoin d’implémenter la méthode build quelque soit le nombre de champs.

Automatiser les macros fill_struct!

Parlant d’automatisation de champs, nous avons encore des champs a, b, c qui trainent dans les macros fill_struct! et fill_struct_optional!.

macro_rules! fill_struct {
    ($self:ident, $target:ident, $value:expr, $new_state:ty) => {
        Builder::<$new_state> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

macro_rules! fill_struct_optional {
    ($self:ident, $target:ident, $value:expr) => {
        Builder::<T> {
            a: fill_field!($self, a, $target, $value),
            b: fill_field!($self, b, $target, $value),
            c: fill_field!($self, c, $target, $value),
            optional: fill_field_optional!($self, optional, $target, $value),
            optional2: fill_field_optional!($self, optional2, $target, $value),
            state: Default::default(),
        }
    };
}

Nous allons avoir besoin de l’instrospection des champs de la structure. Ce qui implique que nous allons devoir déclarer ces macros à l’intérieur de notre macro build_builder!.

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident [
        $($state:ident),*$(,)*
    ]{
        $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {


        macro_rules! fill_struct {
            ($self:ident, $target:ident, $value:expr, $new_state:ty) => {
                Builder::<$new_state> {
                    $($field_name: fill_field!($self, $field_name, $target, $value)),*,
                    $($field_name_optional: fill_field_optional!($self, $field_name_optional, $target, $value)),*,
                    state: Default::default(),
                }
            };
        }

        macro_rules! fill_struct_optional {
            ($self:ident, $target:ident, $value:expr) => {
                Builder::<T> {
                    $($field_name: fill_field!($self, $field_name, $target, $value)),*,
                    $($field_name_optional: fill_field_optional!($self, $field_name_optional, $target, $value)),*,
                    state: Default::default(),
                }
            };
        }

}

Et là on s’aperçoit du potentiel quasi illimité des macros et des métavariables qui peuvent être utilisées dans le corps d’autres macros imbriquées pour créer des comportements très complexes.

Il ne reste plus grand chose en dehors de la macro build_builder!, à part une dernière chose…

Générer la fonction Builder

Notre méthode builder est très simple et donc très facile à générer.

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

Ce qui donne sous la forme de macro:

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident [
        $($state:ident),*$(,)*
    ]{
        $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {

    impl $name {
        fn builder() -> Builder<Init> {
            Builder::default()
        }
    }
}

Générer la structure à partir du DSL de la macro

Un truc m’ennuie, nous avons une répétition dans la définition des champs:

Une pour la structure à créer:

#[derive(Debug, PartialEq)]
struct Foo {
    a: u8,
    b: f32,
    c: Bar,
    optional: Vec<i8>,
    optional2: bool,
}

Et une deuxième dans la déclaration du DSL:

build_builder! {
    struct Foo [
        WithA,
        WithB
    ]{
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    }
}

Pourquoi ne pas générer la structure du dessus directement depuis notre DSL ?

Il faut par contre prendre en compte les attributs de la structure:

#[derive(Debug, PartialEq)]

Notre macro devra donc être capable de comprendre et de générer les attributs au-dessus de la structure finale.

En mettant les attributs en dur, cela donne quelque chose comme ça:

macro_rules! build_builder {
    ($struct_vis:vis struct $name:ident [
        $($state:ident),*$(,)*
    ]{
        $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
        #[optional fields]
        $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
    } ) => {
    
        #[derive(Debug, PartialEq)]
        $struct_vis struct $name {
            $($field_name: $field_type),*,
            $($field_name_optional: $field_type_optional),*,
        }
}

Si on veut aussi les variabiliser, on doit modifier le pattern de match pour prendre en compte cette nouvelle contrainte.

macro_rules! build_builder {
    (
        $(#[$attr:meta])*    
        $struct_vis:vis struct $name:ident [
            $($state:ident),*$(,)*
        ]{
            $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
            #[optional fields]
            $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
        } 
    ) => {

        #[$($attr)*]
        $struct_vis struct $name {
            $($field_name: $field_type),*,
            $($field_name_optional: $field_type_optional),*,
        }
}

Tout la magie se passe à la ligne 3. Elle capture les attributs et les recopies dans la structure générée.

$(#[$attr:meta])*  

Cette ligne permet de capturer tous les attributs qui sont associés à la structure.

Notre nouveau DSL a maintenant cette tête:

build_builder! {
    #[derive(Debug, PartialEq)]
    struct Foo [
        WithA,
        WithB
    ]{
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    }
}

On récapitule:

Code Rust
use std::marker::PhantomData;
use tt_call::tt_if;
use tt_equal::tt_equal;

macro_rules! fill_field {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                Some($value)
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_field_optional {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                $value
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! build_builder {
    (
        $(#[$attr:meta])*
        $struct_vis:vis struct $name:ident [
            $($state:ident),*$(,)*
        ]{
            $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
            #[optional fields]
            $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
        }
    ) => {

        #[$($attr)*]
        $struct_vis struct $name {
            $($field_name: $field_type),*,
            $($field_name_optional: $field_type_optional),*,
        }

        impl $name {
            fn builder() -> Builder<Init> {
                Builder::default()
            }
        }

        #[derive(Default)]
        struct Init;
        $(struct $state);*;
        struct Buildable;


        #[derive(Default)]
        struct Builder<T> {
            $($field_name: Option<$field_type>),*,
            $($field_name_optional: $field_type_optional),*,
            state: PhantomData<T>,
        }

        macro_rules! fill_struct {
            ($self:ident, $target:ident, $value:expr, $new_state:ty) => {
                Builder::<$new_state> {
                    $($field_name: fill_field!($self, $field_name, $target, $value)),*,
                    $($field_name_optional: fill_field_optional!($self, $field_name_optional, $target, $value)),*,
                    state: Default::default(),
                }
            };
        }

        macro_rules! fill_struct_optional {
            ($self:ident, $target:ident, $value:expr) => {
                Builder::<T> {
                    $($field_name: fill_field!($self, $field_name, $target, $value)),*,
                    $($field_name_optional: fill_field_optional!($self, $field_name_optional, $target, $value)),*,
                    state: Default::default(),
                }
            };
        }

        impl<T> Builder<T> {

            $(
            pub fn $field_name_optional(self, $field_name_optional: $field_type_optional) -> Builder<T> {
                fill_struct_optional!(self, $field_name_optional, $field_name_optional)
            }
            )*

        }

        $(
            impl Builder<$current_state> {
                fn $field_name(self, $field_name: $field_type) -> Builder<$next_state> {
                    fill_struct!(self, $field_name, $field_name, $next_state)
                }
            }
        )*

        impl Builder<Buildable> {
            pub fn build(self) -> $name {
                $name {
                    $($field_name: self.$field_name.unwrap()),*,
                    $($field_name_optional: self.$field_name_optional),*,
                }
            }
        }
    };
}

#[derive(Debug, PartialEq)]
struct Bar;

build_builder! {
    #[derive(Debug, PartialEq)]
    struct Foo [
        WithA,
        WithB
    ]{
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    }
}

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .optional(vec![45, -78])
        .c(Bar)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

Et bien tout est dans la macro !!! 🥳🥳🥳

On arrive au bout ! Encore quelques améliorations et on aura enfin fini, et on pourra ENFIN en profiter !

Pouvoir renommer le nom du Builder

Il est possible que le nom du builder soit déjà utilisé quelque part dans le module, et donc que le nom Builder soit en conflit avec une autre structure ou énumération, déjà existante.

Nous allons permettre à l’utilisateur de pouvoir en configurer le nom et même le chemin vers ce builder.

En effet, nous allons aussi envelopper notre mécanique de Builder à l’intérieur d’un module, pour éviter des modifications externes et ainsi n’exposer que les éléments essentiels.

Nous allons introduire, une nouvelle notation dans notre DSL.

Il s’agira d’un clef-valeur. Par exemple :

[builder_name => builder::Builder ]

Notre macro créera un module interne builder à l’emplacement de l’invocation de la macro.

Ceci nous permettra de sceller les états de la machine.

Voici notre nouveau DSL.

build_builder! {
    #[derive(Debug, PartialEq)]
    struct Foo [
        WithA,
        WithB
    ]{
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    };
    [builder_name => builder::BuilderFoo]
}

Cet appel à la macro doit créer une structure telle que:

mod builder {
    struct Builder {...}
}

Et voici, notre nouvelle macro avec le nom du Builder variabilisé.

use std::marker::PhantomData;
use tt_call::tt_if;
use tt_equal::tt_equal;

macro_rules! fill_field {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                Some($value)
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_field_optional {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                $value
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! build_builder {
    (
        $(#[$attr:meta])*
        $struct_vis:vis struct $name:ident [
            $($state:ident),*$(,)*
        ]{
            $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
            #[optional fields]
            $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
        };
        [builder_name => $mod_name:ident::$builder_name:ident]
    ) => {

        #[$($attr)*]
        $struct_vis struct $name {
            $($field_name: $field_type),*,
            $($field_name_optional: $field_type_optional),*,
        }

        impl $name {
            fn builder() -> $builder_name<Init> {
                $builder_name::default()
            }
        }

        #[derive(Default)]
        struct Init;
        $(struct $state);*;
        struct Buildable;


        #[derive(Default)]
        struct $builder_name<T> {
            $($field_name: Option<$field_type>),*,
            $($field_name_optional: $field_type_optional),*,
            state: PhantomData<T>,
        }

        macro_rules! fill_struct {
            ($self:ident, $target:ident, $value:expr, $new_state:ty) => {
                $builder_name::<$new_state> {
                    $($field_name: fill_field!($self, $field_name, $target, $value)),*,
                    $($field_name_optional: fill_field_optional!($self, $field_name_optional, $target, $value)),*,
                    state: Default::default(),
                }
            };
        }

        macro_rules! fill_struct_optional {
            ($self:ident, $target:ident, $value:expr) => {
                $builder_name::<T> {
                    $($field_name: fill_field!($self, $field_name, $target, $value)),*,
                    $($field_name_optional: fill_field_optional!($self, $field_name_optional, $target, $value)),*,
                    state: Default::default(),
                }
            };
        }

        impl<T> $builder_name<T> {

            $(
            pub fn $field_name_optional(self, $field_name_optional: $field_type_optional) -> $builder_name<T> {
                fill_struct_optional!(self, $field_name_optional, $field_name_optional)
            }
            )*

        }

        $(
            impl $builder_name<$current_state> {
                fn $field_name(self, $field_name: $field_type) -> $builder_name<$next_state> {
                    fill_struct!(self, $field_name, $field_name, $next_state)
                }
            }
        )*

        impl $builder_name<Buildable> {
            pub fn build(self) -> $name {
                $name {
                    $($field_name: self.$field_name.unwrap()),*,
                    $($field_name_optional: self.$field_name_optional),*,
                }
            }
        }
    };
}

#[derive(Debug, PartialEq)]
struct Bar;

build_builder! {
    #[derive(Debug, PartialEq)]
    struct Foo [
        WithA,
        WithB
    ]{
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    };
    [builder_name => builder::BuilderFoo]
}

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .optional(vec![45, -78])
        .c(Bar)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

Nous allons maintenant sceller notre builder et tout ce qui doit l’être dans le module.

Il faut faire attention à la visibilité de ce que l’on manipule.

Notre module de scellement doit bien évidemment être public.

Nous devons aussi importer les éléments du module supérieur:

pub mod builder{
    use super::*;
}

Notre builder scellé

Code Rust
macro_rules! fill_field {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                Some($value)
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! fill_field_optional {
    ($self:ident, $field_name:ident, $target:ident, $value:expr) => {

        tt_if!{
            condition = [{tt_equal}]
            input = [{ $target $field_name }]
            true = [{
                $value
            }]
            false = [{
                $self.$field_name
            }]
        }
    };
}

macro_rules! build_builder {
    (
        $(#[$attr:meta])*
        $struct_vis:vis struct $name:ident [
            $($state:ident),*$(,)*
        ]{
            $($field_name:ident : $field_type:ty, [ $current_state:ident => $next_state:ident ])*
            #[optional fields]
            $($field_name_optional:ident : $field_type_optional:ty),*$(,)*
        };
        [builder_name => $mod_name:ident::$builder_name:ident]
    ) => {

        #[$($attr)*]
        $struct_vis struct $name {
            $($field_name: $field_type),*,
            $($field_name_optional: $field_type_optional),*,
        }

        impl $name {
            fn builder() -> $mod_name::$builder_name<$mod_name::Init> {
                $mod_name::$builder_name::default()
            }
        }

        pub mod $mod_name {

            use super::*;

            use std::marker::PhantomData;
            use tt_call::tt_if;
            use tt_equal::tt_equal;


            #[derive(Default)]
            pub struct Init;
            $(pub struct $state);*;
            pub struct Buildable;

            #[derive(Default)]
            pub struct $builder_name<T> {
                $($field_name: Option<$field_type>),*,
                $($field_name_optional: $field_type_optional),*,
                state: PhantomData<T>,
            }

            macro_rules! fill_struct {
                ($self:ident, $target:ident, $value:expr, $new_state:ty) => {
                    $builder_name::<$new_state> {
                        $($field_name: fill_field!($self, $field_name, $target, $value)),*,
                        $($field_name_optional: fill_field_optional!($self, $field_name_optional, $target, $value)),*,
                        state: Default::default(),
                    }
                };
            }

            macro_rules! fill_struct_optional {
                ($self:ident, $target:ident, $value:expr) => {
                    $builder_name::<T> {
                        $($field_name: fill_field!($self, $field_name, $target, $value)),*,
                        $($field_name_optional: fill_field_optional!($self, $field_name_optional, $target, $value)),*,
                        state: Default::default(),
                    }
                };
            }

            impl<T> $builder_name<T> {

                $(
                pub fn $field_name_optional(self, $field_name_optional: $field_type_optional) -> $builder_name<T> {
                    fill_struct_optional!(self, $field_name_optional, $field_name_optional)
                }
                )*

            }

            $(
                impl $builder_name<$current_state> {
                    pub fn $field_name(self, $field_name: $field_type) -> $builder_name<$next_state> {
                        fill_struct!(self, $field_name, $field_name, $next_state)
                    }
                }
            )*

            impl $builder_name<Buildable> {
                pub fn build(self) -> $name {
                    $name {
                        $($field_name: self.$field_name.unwrap()),*,
                        $($field_name_optional: self.$field_name_optional),*,
                    }
                }
            }
        }
    };
}

#[derive(Debug, PartialEq)]
struct Bar;

build_builder! {
    #[derive(Debug, PartialEq)]
    struct Foo [
        WithA,
        WithB
    ]{
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    };
    [builder_name => builder::BuilderFoo]
}

#[test]
fn with_optional() {
    let foo = Foo::builder()
        .a(12)
        .b(45.5)
        .optional(vec![45, -78])
        .c(Bar)
        .build();

    let expected = Foo {
        a: 12,
        b: 45.5,
        c: Bar,
        optional: vec![45, -78],
        optional2: false,
    };
    assert_eq!(foo, expected);
}

Ce qui nous permet d’obtenir le résultat suivant:

fn main() {
    let builder : builder::BuilderFoo<builder::Init> = Foo::builder();
}

Comparaison des codes

Sans macros, nous avions tout ça à écrire:

Code Rust
use std::marker::PhantomData;


#[derive(Debug, PartialEq)]
struct Bar;

#[derive(Debug, PartialEq)]
struct Foo {
    a: u8,
    b: f32,
    c: Bar,
    optional: Vec<i8>,
    optional2: bool,
}

impl Foo {
    fn builder() -> Builder<Init> {
        Builder::default()
    }
}

#[derive(Default)]
struct Init;
struct WithA;
struct WithB;
struct Buildable;

#[derive(Default)]
struct Builder<T> {
    a: Option<u8>,
    b: Option<f32>,
    c: Option<Bar>,
    optional: Vec<i8>,
    optional2: bool,
    state: PhantomData<T>,
}

impl<T> Builder<T> {
    pub fn optional(self, optional: Vec<i8>) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            c: self.c,
            optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }

    pub fn optional2(self, optional2: bool) -> Builder<T> {
        Builder::<T> {
            a: self.a,
            b: self.b,
            c: self.c,
            optional: self.optional,
            optional2,
            state: Default::default(),
        }
    }
}

impl Builder<Init> {
    pub fn a(self, a: u8) -> Builder<WithA> {
        Builder::<WithA> {
            a: Some(a),
            b: self.b,
            c: self.c,
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<WithA> {
    pub fn b(self, b: f32) -> Builder<WithB> {
        Builder::<WithB> {
            a: self.a,
            b: Some(b),
            c: self.c,
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<WithB> {
    pub fn c(self, c: Bar) -> Builder<Buildable> {
        Builder::<Buildable> {
            a: self.a,
            b: self.b,
            c: Some(c),
            optional: self.optional,
            optional2: self.optional2,
            state: Default::default(),
        }
    }
}

impl Builder<Buildable> {
    pub fn build(self) -> Foo {
        Foo {
            a: self.a.unwrap(),
            b: self.b.unwrap(),
            c: self.c.unwrap(),
            optional: self.optional,
            optional2: self.optional2,
        }
    }
}

Avec la macro, il suffit de:

build_builder! {
    #[derive(Debug, PartialEq)]
    struct Foo [
        WithA,
        WithB
    ]{
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    };
    [builder_name => builder::BuilderFoo]
}

Pour pouvoir appeler le code:

let foo = Foo::builder()
    .a(12)
    .optional2(bool)
    .b(45.5)
    .optional(vec![45, -78])
    .c(Bar)
    .build();

Amusons nous un peu

Pour un champ obligatoire d : String. Nous pouvons appeler notre macro ainsi.

Nous créons un nouvel état WithC, puis la transition WithB => WithC pour intercaler notre nouvelle état.

build_builder! {
    #[derive(Debug, PartialEq)]
    struct Foo [
        WithA,
        WithB,
        WithC,
    ]{
        a: u8,      [ Init  => WithA ]
        b: f32,     [ WithA => WithB ]
        c: Bar,     [ WithB => WithC ]
        d: String   [ WithC => Buildable ]
        #[optional fields]
        optional: Vec<i8>,
        optional2: bool
    };
    [builder_name => builder::BuilderFoo]
}

Et ainsi, pouvoir définir notre champ d.

let foo = Foo::builder()
    .a(12)
    .optional2(bool)
    .b(45.5)
    .optional(vec![45, -78])
    .c(Bar)
    .d("String".to_string())
    .build();

Le champ d doit être obligatoirement défini, sinon, une erreur de compilation est levée.

Conclusion

Et bien quel voyage ! 😄

Nous avons notre builder qui est entièrement généré par macro.

Il est évidemment perfectible et ne gére pas tous les cas existants.

Par exemple, il n’est pas possible d’avoir de lifetime dans la déclaration de type de champ et la syntaxe est encore un trop rigide pour être utilisable dans tous les cas.

Mais nous avons l’essentiel !

J’espère que cette application pratique des macros vous aura plu.

Et si vous le désirez, je pourrai écrire un article sur les différentes améliorations que l’on peut lui apporter. 😀

Merci de votre lecture et à 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.