Builder Rust

Lecture 21 min. ‱

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

Note

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

Note

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

Note

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.

Note

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

Note

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 :

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:

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

Note

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 ne marchera pas ^^’)

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

Note

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:

Note

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:

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.

Note

Si l’Automatisation ne marche pas, c’est qu’on n’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:

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

Note

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