Aller au contenu
FrontBackTips

Rust : Le monorepo sans friction

Centralisation de la connaissance, facilitation de la synchronisation des librairies et des services, standardisation de l'outillage... L'approche mono-repo offre de nombreux avantages, mais comment le mettre en place avec Rust ?

packages illustation
Photo par Ayrat

Définition d'un mono repo Rust

Un projet Rust est défini par un dossier source avec un fichier src/main.rs(pour une app) ou src/lib.rs(pour une crate) et un fichier toml ./Cargo.toml ressemblant à :

[package]  
name = "project name"  
version = "0.1.0"  
edition = "2021"  
  
[features]  
  
[dependencies]  

Imaginons un monorepo qui comprend trois parties : le front, le back (avec du sql) et les types en commun. Les trois projets auront chacun leur dossier et dans le dossier root, nous auront un toml qui définira les espaces de travail. Nous obtiendrons donc l'arborescence suivante :

root
 |-api_types
 |  |-src
 |  |  |-lib.rs
 |  |-Cargo.toml
 |-back
 |  |-src
 |  |  |-main.rs
 |  |-Cargo.toml
 |-front
 |  |-src
 |  |  |-main.rs
 |  |-Cargo.toml
 |-Cargo.toml

root/Cargo.toml:

[workspace]  
  
members = [  
    "api_types",  
    "back",  
    "front",  
]

et chaque Cargo.toml dans les projets suivront le template normal d'un projet Rust.

Partager du code entre les espaces de travail.

Pour poursuivre l'exemple précédent, déclarons le module qui va être partagé :

root/api_types/Cargo.toml:

[package]  
name = "api_types"  
version = "0.1.0"  
edition = "2021"  
  
[dependencies]  
serde = { version = "1.0", features = ["derive"] }  
serde_json = "1.0"  
sqlx = { version = "0.6", features = [ "runtime-async-std-native-tls", "postgres", "macros" ] }  

Nous importons les dépendances de serde, serde_json pour les sérialisations en JSON et sqlx pour rendre la structure utilisable facilement côté back.

Les features permettent de ne prendre que le code qui nous intéresse dans une librairie. Ainsi, pour sqlx nous importons :

  • la partie postgresql, qui permet la connexion à la base de données éponyme
  • la partie macro, qui permettra de dériver les structures.
  • la partie runtime-async-std-native-tls qui permet l'utilisation des fonctions asynchrones

Les documentations de chaque crate vous indiquent quelle feature charger pour quelle fonctionnalité.

Déclarons une structure qui pourra être utilisé dans les deux autres apps :

root/api_types/src/lib.rs :

use serde::{Deserialize,Serialize}; 
use sqlx;  
  
#[derive(Serialize,Deserialize, sqlx::Type)]  
#[sqlx(type_name = "container_type_enum")]
pub enum ContainerType {  
    Fish,  
    Vegetable,  
    Other,  
}

#[derive(Serialize,Deserialize)]
pub struct Container {
	pub name: String,
	pub volume: u64,
	pub content: ContainerType
}

Chargeons ensuite notre lib dans nos deux app :

root/back/Cargo.toml

[package]  
name = "back"  
version = "0.1.0"  
edition = "2021"  

[dependencies]  
api_types = { path = "../api_types"} 
serde = { version = "1.0", features = ["derive"] }  
serde_json = "1.0"  
sqlx = { version = "0.6", features = [ "postgres", "macros" ] }

root/front/Cargo.toml

[package]  
name = "front"  
version = "0.1.0"  
edition = "2021"  
  
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html  
  
[dependencies]  
api_types = {path="../api_types"}  
serde = { version = "1.0", features = ["derive"] }  
serde_json = "1"

Et pour utiliser la structure, côté back :
root/src/back/src/mains.rs

  
use api_types::container::{Container, ContainerType};

async fn main() { 
	let a = Container {name: "Aquarium".to_string(), volume: 200, content: ContainerType::Fish};
}

Déclarer les features pour booster vos dépendences

Afin de réduire la taille des exécutables et le temps de compilation, nous allons déclarer des features. Ce sont des options de compilations permettant de n'embarquer que le code nécessaire à une fonctionnalité. Nous aurons ici :

  • pour le back : sérialiser avec serde pour envoyer au front, sérialiser et dé-sérialiser pour enregistrer dans la base (pour l'exemple d'une base sql)
  • pour le front : dé-sérialiser avec serde

root/api_types/src/lib.rs

#[cfg_attr(feature = "front", derive(Deserialize))]  
#[cfg_attr(feature = "back", derive(Serialize, sqlx::Type))]  
#[cfg_attr(feature = "back", sqlx(type_name = "container_type_enum"))]  
pub enum ContainerType {  
    Fish,  
    Vegetable,  
    Other,  
}

#[cfg_attr(feature = "front", derive(Deserialize))]  
#[cfg_attr(feature = "back", derive(Serialize, sqlx::Type))]  
struct SimpleStruct {
	pub name: String,
	pub volume: u64,
	pub content: ContainerType
}

Dans #[cfg_attr(feature = "front", derive(Deserialize))] , le compilateur appliquera la macro derive(Deserialize) uniquement si la feature front est demandée. Mais il faut encore déclarer les features possibles dans le Cargo.toml

root/api_types/Cargo.toml

[package]  
name = "api_types"  
version = "0.1.0"  
edition = "2021"  

[features]    
back = ["dep:sqlx"]  
front = []
  
[dependencies]  
serde = { version = "1.0", features = ["derive"] }  
serde_json = "1.0"  
sqlx = { version = "0.6", optional = true, features = [ "runtime-async-std-native-tls", "postgres", "macros" ] }  

back = ["dep:sqlx"] signifie que pour une application demandant la feature back, il faudra la dépendance sqlx que nous venons de passer en optionnel.

Pour les utiliser nous modifierons les toml :

  • du front
[dependencies]  
api_types = { path = "../api_types" features = ["front"]} 
  • du back
[dependencies]  
api_types = { path = "../api_types" features = ["back"]} 

Et voici les commandes pour lancer les builds :

  • cargo build -p front
  • cargo build -p back

Conclusion

Dans cet article nous aurons appris comment faire un monorepo et comment rendre modulable une crate grâce aux features. Pour aller plus loin, vous pouvez regarder comment publier une crate, packager vos applications pour différentes plateformes ou utiliser docker comme environnement unique.

Dernier