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.