Publié le 13 juillet 2022
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ésumeCode 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.
# [ 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) ;
}
# [ test ]
fn without_b ( ) {
let foo = Foo:: builder( ) . a ( 12 ) . build ( ) ;
let expected = Foo { a: 12 , b: 45. 5 } ;
assert_eq! ( foo, expected) ;
}
# [ 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 đ
# [ test ]
fn basic ( ) { }
# [ test ]
fn inverted ( ) { }
# [ test ]
fn without_b ( ) { }
# [ test ]
fn without_a ( ) { }
Nous venons de contraindre notre systĂšme !
Récapitulons:Code Rust # [ derive ( Debug, PartialEq, Default ) ]
struct Foo {
a : u8 ,
b : f32 ,
}
impl Foo {
fn builder ( ) -> Builder {
Builder:: default( )
}
}
# [ derive ( Default ) ]
struct Builder {
a : u8 ,
b : f32 ,
}
struct WithA {
a : u8 ,
b : f32 ,
}
struct Buildable {
a : u8 ,
b : f32 ,
}
impl Builder {
pub fn a ( self , a : u8 ) -> WithA {
WithA { a, b: self . b }
}
}
impl WithA {
pub fn b ( self , b : f32 ) -> Buildable {
Buildable { a: self . a, b }
}
}
impl Buildable {
pub fn build ( self ) -> Foo {
Foo {
a: self . a,
b: self . b,
}
}
}
# [ test ]
fn basic ( ) {
let foo = Foo:: builder( ) . a ( 12 ) . b ( 45. 5 ) . build ( ) ;
let expected = Foo { a: 12 , b: 45. 5 } ;
assert_eq! ( foo, expected) ;
}
La rĂ©pĂ©tition câest le mal! Dans la programmation, sâil y a bien une chose qui est insupportable, câest la rĂ©pĂ©tion.
Imaginez, ici nous nâavons que 2 champs, mais avec trois champs, ça demande dâĂ©crire quelque chose comme ceci:
# [ derive ( Default ) ]
struct Builder {
a : u8 ,
b : f32 ,
c : bool ,
}
struct WithA {
a : u8 ,
b : f32 ,
c : bool ,
}
struct WithB {
a : u8 ,
b : f32 ,
c : bool ,
}
struct Buildable {
a : u8 ,
b : f32 ,
}
Ăa devient fastidieux! Et rappellez vous, le maĂźtre mot qui nous guide durant notre Ă©popĂ©e est la fainĂ©antise. đ
Quâutilise-t-on en Rust lorsque lâon manipule des structures avec des champs qui peuvent avoir des types variables?
Les génériques bien sûr !!
Réécrivons notre code dans ce sens:
struct Init ;
struct WithA ;
struct Buildable ;
# [ derive ( Default ) ]
struct Builder < T> {
a : u8 ,
b : f32 ,
}
impl Builder < Init> {
pub fn a ( self , a : u8 ) -> Builder< WithA> {
Builder:: < WithA> { a, b: self . b }
}
}
impl Builder < WithA> {
pub fn b ( self , b : f32 ) -> Builder< Buildable> {
Builder:: < Buildable> { a: self . a, b }
}
}
impl Builder < Buildable> {
pub fn build ( self ) -> Foo {
Foo {
a: self . a,
b: self . b,
}
}
}
Et oui! Init
est de retour, je vous avais bien dit quâon allait sâen occuper plus tard ^^.
Compilons !
| struct Builder<T> {
| ^ unused parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`
= help: if you intended `T` to be a const parameter, use `const T: usize` instead
Hum presque đ„
Mais heureusement le compilateur nous met sur la voie. Il faut que nous rajoutions un champ supplĂ©mentaire qui va tenir lâĂ©tat de la structure.
Ce type un peu spécial, se nomme un PhantomData
et ne sert que de marqueur permettant de tenir notre <T>
.
# [ derive ( Default ) ]
struct Builder < T> {
a : u8 ,
b : f32 ,
state : PhantomData< T> ,
}
impl Builder < Init> {
pub fn a ( self , a : u8 ) -> Builder< WithA> {
Builder:: < WithA> {
a,
b:
self . b, state: Default :: default( )
}
}
}
impl Builder < WithA> {
pub fn b ( self , b : f32 ) -> Builder< Buildable> {
Builder:: < Buildable> {
a: self . a,
b,
state: Default :: default( )
}
}
}
impl Builder < Buildable> {
pub fn build ( self ) -> Foo {
Foo {
a: self . a,
b: self . b,
}
}
}
Bon, cela commence à ressembler à quelque chose sans trop de répétitions.
Comme de coutume, un nouveau milestone !Code Rust use std:: marker:: PhantomData;
# [ derive ( Debug, PartialEq, Default ) ]
struct Foo {
a : u8 ,
b : f32 ,
}
impl Foo {
fn builder ( ) -> Builder< Init> {
Builder:: default( )
}
}
# [ derive ( Default ) ]
struct Init ;
struct WithA ;
struct WithB ;
struct Buildable ;
# [ derive ( Default ) ]
struct Builder < T> {
a : u8 ,
b : f32 ,
state : PhantomData< T> ,
}
impl Builder < Init> {
pub fn a ( self , a : u8 ) -> Builder< WithA> {
Builder:: < WithA> {
a,
b: self . b,
state: Default :: default( ) ,
}
}
}
impl Builder < WithA> {
pub fn b ( self , b : f32 ) -> Builder< Buildable> {
Builder:: < Buildable> {
a: self . a,
b,
state: Default :: default( ) ,
}
}
}
impl Builder < Buildable> {
pub fn build ( self ) -> Foo {
Foo {
a: self . a,
b: self . b,
}
}
}
# [ test ]
fn basic ( ) {
let foo = Foo:: builder( )
. a ( 12 )
. b ( 45. 5 )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5
} ;
assert_eq! ( foo, expected) ;
}
Vous reprendrez bien un peu dâoptions ? Maintenant que les critĂšres 1 & 3 de notre cahier des charges sont remplis (Ă©chec Ă la compilation et tous les champs obligatoires doivent avoir Ă©tĂ© appellĂ©s).
Nous pouvons réécrire notre machine à états pour y inclure notre appel à optional()
, pour chaque état de la machine.
flowchart LR
fin([Foo construit])
start([Foo Ă construire])
start-->|"builder()"|Init
Init[Init ou Builder]-->|"a()"|WithA
WithA-->|"b()"|Buildable
Buildable-->|"build()"|fin
Init-->|"optional()"|Init
WithA-->|"optional()"|WithA
Buildable-->|"optional()"|Buildable
Ătablissons nos tests, nous sommes en TDD, ne perdons pas les bonnes habitudes. đ
# [ test ]
fn with_optional ( ) {
let foo = Foo:: builder( )
. a ( 12 )
. b ( 45. 5 )
. optional ( vec! [ 45 , - 78 ] )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ 45 , - 78 ] ,
} ;
assert_eq! ( foo, expected) ;
}
Pour y parvenir, nous allons profiter dâavoir une structure gĂ©nĂ©rique.
En effet si lâon Ă©crit:
struct Builder < T> {
a : u8 ,
b : f32 ,
optional : Vec < i8 > ,
state : PhantomData< T>
}
impl Builder < T> {
pub fn optional ( optional : Vec < i8 > ) -> Builder< T> {
Builder:: < T> {
a: self . a,
b: self . b,
optional
}
}
}
On crée une fonction optional()
qui :
si elle est appellée par Builder<Init>
, renverra un nouveau Builder<Init>
si elle est appellée par Builder<WithA>
, renverra un nouveau Builder<WithA>
et de maniÚre générale, si elle est appellée par Builder<T>
, elle renverra un nouveau Builder<T>
Réécrivons notre code: use std:: marker:: PhantomData;
# [ derive ( Debug, PartialEq, Default ) ]
struct Foo {
a : u8 ,
b : f32 ,
optional : Vec < i8 > ,
}
impl Foo {
fn builder ( ) -> Builder< Init> {
Builder:: default( )
}
}
# [ derive ( Default ) ]
struct Init ;
struct WithA ;
struct WithB ;
struct Buildable ;
# [ derive ( Default ) ]
struct Builder < T> {
a : u8 ,
b : f32 ,
optional : Vec < i8 > ,
state : PhantomData< T> ,
}
impl < T> Builder < T> {
pub fn optional ( self , optional : Vec < i8 > ) -> Builder< T> {
Builder:: < T> {
a: self . a,
b: self . b,
optional,
state: Default :: default( ) ,
}
}
}
impl Builder < Init> {
pub fn a ( self , a : u8 ) -> Builder< WithA> {
Builder:: < WithA> {
a,
b: self . b,
optional: self . optional,
state: Default :: default( ) ,
}
}
}
impl Builder < WithA> {
pub fn b ( self , b : f32 ) -> Builder< Buildable> {
Builder:: < Buildable> {
a: self . a,
b,
optional: self . optional,
state: Default :: default( ) ,
}
}
}
impl Builder < Buildable> {
pub fn build ( self ) -> Foo {
Foo {
a: self . a,
b: self . b,
optional: self . optional,
}
}
}
# [ test ]
fn basic ( ) {
let foo = Foo:: builder( )
. a ( 12 )
. b ( 45. 5 )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ ] ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_optional ( ) {
let foo = Foo:: builder( )
. a ( 12 )
. b ( 45. 5 )
. optional ( vec! [ 45 , - 78 ] )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ 45 , - 78 ] ,
} ;
assert_eq! ( foo, expected) ;
}
Nos deux tests sont au verts ! â
Essayons de voir si notre méthode optional()
peut ĂȘtre placĂ©e nâimporte oĂč.Code Rust # [ test ]
fn with_optional_before_a ( ) {
let foo = Foo:: builder( )
. a ( 12 )
. optional ( vec! [ 45 , - 78 ] )
. b ( 45. 5 )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ 45 , - 78 ] ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_optional_before_builder ( ) {
let foo = Foo:: builder( )
. optional ( vec! [ 45 , - 78 ] )
. a ( 12 )
. b ( 45. 5 )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ 45 , - 78 ] ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_optional_duplicated ( ) {
let foo = Foo:: builder( )
. optional ( vec! [ 45 , - 78 ] )
. a ( 12 )
. b ( 45. 5 )
. optional ( vec! [ 45 , - 78 , - 1 ] )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ 45 , - 78 , - 1 ] ,
} ;
assert_eq! ( foo, expected) ;
}
Tout nos tests sont au verts ! â
Et si nous avons plus dâun champ optionel ? MĂȘme combat, si on a Ă la fois optional
et optional2
, notre machine à état devient:
flowchart LR
fin([Foo construit])
start([Foo Ă construire])
start-->|"builder()"|Init
Init[Init ou Builder]-->|"a()"|WithA
WithA-->|"b()"|Buildable
Buildable-->|"build()"|fin
Init-->|"optional()"|Init
WithA-->|"optional()"|WithA
Buildable-->|"optional()"|Buildable
Init-->|"optional2()"|Init
WithA-->|"optional2()"|WithA
Buildable-->|"optional2()"|Buildable
Ce qui nous donne lâimplĂ©mentation suivante: use std:: marker:: PhantomData;
# [ derive ( Debug, PartialEq, Default ) ]
struct Foo {
a : u8 ,
b : f32 ,
optional : Vec < i8 > ,
optional2 : bool ,
}
impl Foo {
fn builder ( ) -> Builder< Init> {
Builder:: default( )
}
}
# [ derive ( Default ) ]
struct Init ;
struct WithA ;
struct WithB ;
struct Buildable ;
# [ derive ( Default ) ]
struct Builder < T> {
a : u8 ,
b : f32 ,
optional : Vec < i8 > ,
optional2 : bool ,
state : PhantomData< T> ,
}
impl < T> Builder < T> {
pub fn optional ( self , optional : Vec < i8 > ) -> Builder< T> {
Builder:: < T> {
a: self . a,
b: self . b,
optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
pub fn optional2 ( self , optional2 : bool ) -> Builder< T> {
Builder:: < T> {
a: self . a,
b: self . b,
optional: self . optional,
optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < Init> {
pub fn a ( self , a : u8 ) -> Builder< WithA> {
Builder:: < WithA> {
a,
b: self . b,
optional: self . optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < WithA> {
pub fn b ( self , b : f32 ) -> Builder< Buildable> {
Builder:: < Buildable> {
a: self . a,
b,
optional: self . optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < Buildable> {
pub fn build ( self ) -> Foo {
Foo {
a: self . a,
b: self . b,
optional: self . optional,
optional2: self . optional2,
}
}
}
# [ test ]
fn basic ( ) {
let foo = Foo:: builder( ) . a ( 12 ) . b ( 45. 5 ) . build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ ] ,
optional2: false ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_optional ( ) {
let foo = Foo:: builder( )
. a ( 12 )
. b ( 45. 5 )
. optional ( vec! [ 45 , - 78 ] )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ 45 , - 78 ] ,
optional2: false ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_optional_before_a ( ) {
let foo = Foo:: builder( )
. a ( 12 )
. optional ( vec! [ 45 , - 78 ] )
. b ( 45. 5 )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ 45 , - 78 ] ,
optional2: false ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_optional_before_builder ( ) {
let foo = Foo:: builder( )
. optional ( vec! [ 45 , - 78 ] )
. a ( 12 )
. b ( 45. 5 )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ 45 , - 78 ] ,
optional2: false ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_optional_duplicated ( ) {
let foo = Foo:: builder( )
. optional ( vec! [ 45 , - 78 ] )
. a ( 12 )
. b ( 45. 5 )
. optional ( vec! [ 45 , - 78 , - 1 ] )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ 45 , - 78 , - 1 ] ,
optional2: false ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_more_than_one_optional_field ( ) {
let foo = Foo:: builder( )
. a ( 12 )
. optional2 ( true )
. b ( 45. 5 )
. optional ( vec! [ 45 , - 78 ] )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
optional: vec! [ 45 , - 78 ] ,
optional2: true ,
} ;
assert_eq! ( foo, expected) ;
}
Je vous passe la phase de TDD, mais oui câest du vert aussi. đ
Petite amélioration Nous avons supposé que tous les champs de notre structure implémentaient le trait Default
.
Mais ce nâest pas forcĂ©ment vrai.Code Rust use std:: marker:: PhantomData;
# [ derive ( Debug, PartialEq ) ]
struct Bar ;
# [ derive ( Debug, PartialEq ) ]
struct Foo {
a : u8 ,
b : f32 ,
c : Bar,
optional : Vec < i8 > ,
optional2 : bool ,
}
impl Foo {
fn builder ( ) -> Builder< Init> {
Builder:: default( )
}
}
# [ derive ( Default ) ]
struct Init ;
struct WithA ;
struct WithB ;
struct Buildable ;
# [ derive ( Default ) ]
struct Builder < T> {
a : u8 ,
b : f32 ,
c : Bar,
optional : Vec < i8 > ,
optional2 : bool ,
state : PhantomData< T> ,
}
impl < T> Builder < T> {
pub fn optional ( self , optional : Vec < i8 > ) -> Builder< T> {
Builder:: < T> {
a: self . a,
b: self . b,
c: self . c,
optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
pub fn optional2 ( self , optional2 : bool ) -> Builder< T> {
Builder:: < T> {
a: self . a,
b: self . b,
c: self . c,
optional: self . optional,
optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < Init> {
pub fn a ( self , a : u8 ) -> Builder< WithA> {
Builder:: < WithA> {
a,
b: self . b,
c: self . c,
optional: self . optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < WithA> {
pub fn b ( self , b : f32 ) -> Builder< WithB> {
Builder:: < WithB> {
a: self . a,
b,
c: self . c,
optional: self . optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < WithB> {
pub fn c ( self , c : Bar) -> Builder< Buildable> {
Builder:: < Buildable> {
a: self . a,
b: self . b,
c,
optional: self . optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < Buildable> {
pub fn build ( self ) -> Foo {
Foo {
a: self . a,
b: self . b,
c: self . c,
optional: self . optional,
optional2: self . optional2,
}
}
}
Ce code ne compile pas:
28 | #[derive(Default)]
| ------- in this derive macro expansion
...
32 | c: Bar,
| ^^^^^^ the trait `Default` is not implemented for `Bar`
|
= note: this error originates in the derive macro `Default` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Bar` with `#[derive(Default)]`
Cargo nâest pas content. đ„
Soit nous imposons que tous les champs implémentent Default
, et du coup on impose une contrainte supplémentaire et non nécessaire.
Ou plus malin, on sâappuie sur les Optional
et on réécrit les choses ainsi:Code Rust use std:: marker:: PhantomData;
# [ derive ( Debug, PartialEq ) ]
struct Bar ;
# [ derive ( Debug, PartialEq ) ]
struct Foo {
a : u8 ,
b : f32 ,
c : Bar,
optional : Vec < i8 > ,
optional2 : bool ,
}
impl Foo {
fn builder ( ) -> Builder< Init> {
Builder:: default( )
}
}
# [ derive ( Default ) ]
struct Init ;
struct WithA ;
struct WithB ;
struct Buildable ;
# [ derive ( Default ) ]
struct Builder < T> {
a : Option < u8 > ,
b : Option < f32 > ,
c : Option < Bar> ,
optional : Vec < i8 > ,
optional2 : bool ,
state : PhantomData< T> ,
}
impl < T> Builder < T> {
pub fn optional ( self , optional : Vec < i8 > ) -> Builder< T> {
Builder:: < T> {
a: self . a,
b: self . b,
c: self . c,
optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
pub fn optional2 ( self , optional2 : bool ) -> Builder< T> {
Builder:: < T> {
a: self . a,
b: self . b,
c: self . c,
optional: self . optional,
optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < Init> {
pub fn a ( self , a : u8 ) -> Builder< WithA> {
Builder:: < WithA> {
a: Some ( a) ,
b: self . b,
c: self . c,
optional: self . optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < WithA> {
pub fn b ( self , b : f32 ) -> Builder< WithB> {
Builder:: < WithB> {
a: self . a,
b: Some ( b) ,
c: self . c,
optional: self . optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < WithB> {
pub fn c ( self , c : Bar) -> Builder< Buildable> {
Builder:: < Buildable> {
a: self . a,
b: self . b,
c: Some ( c) ,
optional: self . optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < Buildable> {
pub fn build ( self ) -> Foo {
Foo {
a: self . a. unwrap ( ) ,
b: self . b. unwrap ( ) ,
c: self . c. unwrap ( ) ,
optional: self . optional,
optional2: self . optional2,
}
}
}
# [ test ]
fn basic ( ) {
let foo = Foo:: builder( ) . a ( 12 ) . b ( 45. 5 ) . c ( Bar) . build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
c: Bar,
optional: vec! [ ] ,
optional2: false ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_optional ( ) {
let foo = Foo:: builder( )
. a ( 12 )
. b ( 45. 5 )
. c ( Bar)
. optional ( vec! [ 45 , - 78 ] )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
c: Bar,
optional: vec! [ 45 , - 78 ] ,
optional2: false ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_optional_before_a ( ) {
let foo = Foo:: builder( )
. a ( 12 )
. optional ( vec! [ 45 , - 78 ] )
. b ( 45. 5 )
. c ( Bar)
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
c: Bar,
optional: vec! [ 45 , - 78 ] ,
optional2: false ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_optional_before_builder ( ) {
let foo = Foo:: builder( )
. optional ( vec! [ 45 , - 78 ] )
. a ( 12 )
. b ( 45. 5 )
. c ( Bar)
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
c: Bar,
optional: vec! [ 45 , - 78 ] ,
optional2: false ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_optional_duplicated ( ) {
let foo = Foo:: builder( )
. optional ( vec! [ 45 , - 78 ] )
. a ( 12 )
. b ( 45. 5 )
. c ( Bar)
. optional ( vec! [ 45 , - 78 , - 1 ] )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
c: Bar,
optional: vec! [ 45 , - 78 , - 1 ] ,
optional2: false ,
} ;
assert_eq! ( foo, expected) ;
}
# [ test ]
fn with_more_than_one_optional_field ( ) {
let foo = Foo:: builder( )
. a ( 12 )
. optional2 ( true )
. b ( 45. 5 )
. c ( Bar)
. optional ( vec! [ 45 , - 78 ] )
. build ( ) ;
let expected = Foo {
a: 12 ,
b: 45. 5 ,
c: Bar,
optional: vec! [ 45 , - 78 ] ,
optional2: true ,
} ;
assert_eq! ( foo, expected) ;
}
Ăa compile et en plus nous nâobligeons plus lâimplĂ©mentation du trait Default
sur Bar
. Tout est beau dans le meilleur des mondes !
Macros Je sais pas vous, mais moi je trouve que ce code se rĂ©pĂšte un peu beaucoup. đ€
Rajouter un ou plusieurs champs est encore plus fastidieux !
Nous devons trouver un moyen de nous faciliter la tĂąche.
Ce moyen est tout trouvé en Rust et il se nomme les macros.
Jâai dâailleurs fait un autre article dessus, si vous voulez dĂ©couvrir ou vous rafraĂźchir la mĂ©moire sur ce concept.
Automatiser la création des champs des structures builders Nous allons y aller pas à pas pour construire notre automatisation.
La premiĂšre chose que lâon remarque est que les implĂ©mentations se ressemblent et se rĂ©pĂštent Ă quelques diffĂ©rences prĂšs.
impl < T> Builder < T> {
pub fn optional ( self , optional : Vec < i8 > ) -> Builder< T> {
Builder:: < T> {
a: self . a,
b: self . b,
c: self . c,
optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
pub fn optional2 ( self , optional2 : bool ) -> Builder< T> {
Builder:: < T> {
a: self . a,
b: self . b,
c: self . c,
optional: self . optional,
optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < Init> {
pub fn a ( self , a : u8 ) -> Builder< WithA> {
Builder:: < WithA> {
a: Some ( a) ,
b: self . b,
c: self . c,
optional: self . optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
}
impl Builder < WithA> {
pub fn b ( self , b : f32 ) -> Builder< Buildable> {
Builder:: < Buildable> {
a: self . a,
b: Some ( b) ,
c: self . c,
optional: self . optional,
optional2: self . optional2,
state: Default :: default( ) ,
}
}
}
On peut déjà sortir deux cas:
Si lâon traite des champs obligatoires, nous avons deux possibilitĂ©s:
champ : self.champ
: cas oĂč lâon dĂ©sire transmettre la valeur du champ de lâĂ©tape prĂ©cĂ©dente vers lâĂ©tape suivante sans modificationchamp: Some(champ)
: cas oĂč lâon dĂ©sire configurer la valeur du champ pour lâĂ©tape suivante avec la valeur en entrĂ©e.Si lâon traite des champs optionels, nous avons aussi deux possibilitĂ©s:
champ : self.champ
: mĂȘme principe que pour les champs optionnelschamp
: on réalise un remplacement de la valeur pour la suite des opérations 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) } else {
self . a }
Pour le champ b
:
if stringify! ( b ) == stringify! ( a ) {
Some ( a) } else {
self . b }
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:
<A>
et <B>
: les expressions qui vont ĂȘtre comparĂ©es.condition
: quel opération renvoyant un booléen sera appliqué à <A>
et <B>
.true
: code à générer en cas de succÚsfalse
: code Ă gĂ©nĂ©rer en cas dâĂ©checLa 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 builderCode 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:
La premiÚre, qui génÚre des champs en champ: Option<type de champ>
La seconde : champ : type de champ
Pour les distinguer, nous allons rajouter un discriminant permettant Ă la macro de comprendre ce quâelle analyse.
Notre macro va se nommer build_builder!
. Et aura pour motif de match:
Premier essai de motif de match
macro_rules! build_builder {
( $struct_vis:vis struct $name : ident {
$ ($field_name : ident : $field_type : ty ,) *
$ ($field_name_optional : ident : $field_type_optional : ty , [optional]) *
} ) => {
println! ( " la structure {} " , stringify! ( $name ) ) ;
$ ( println! ( " \t a le champ obligatoire {} a pour type {} " , stringify! ( $field_name ) , stringify! ( $field_type ) ) ) ; * ;
$ ( println! ( " \t a 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! ( " \t a le champ obligatoire {} a pour type {} " , stringify! ( $field_name ) , stringify! ( $field_type ) ) ) ; * ;
$ ( println! ( " \t a 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! ( " \t a le champ obligatoire {} a pour type {} " , stringify! ( $field_name ) , stringify! ( $field_type ) ) ) ; * ;
$ ( println! ( " \t a 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 ) ,* $ ( ,) *
} ) => {
$ (
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 ! â€ïž