Chapitre 3 • Crates
Cartouche
| Champ | Valeur |
|---|---|
| Auteur·e | Élise |
| Édition | 2024-08-22 |
| Durée | 2 séances (un jour) |
| Taille des équipes | 1 personne |
| Rendu | via git, dépôt $YEAR_rust_c3, droits en lecture à delivery_collector |
Règlement
Prenez connaissance du règlement ici
L’intégralité de la bibliothèque standard est autorisée. Vous êtes encouragés à vous servir des outils qu’elle vous propose s’ils résolvent un de vos problèmes. Votre objectif c’est d’éviter de ré-inventer la roue quand ça n’est pas nécessaire.
Barème
| Critère | Points Accessibles |
|---|---|
| E1 clap opérations simples | 5 |
| E2 serde historique | 5 |
| E3 delta temporel | 5 |
| E4 insertion et suppression | 5 |
| Total des critères | 20 |
| Malus par lignes en faute | -0,5 |
| Malus dépôt sale | -5 |
| Total | N/A |
Introduction
L’un des avantages majeurs de rust, c’est la facilité avec laquelle on peut utiliser des bibliothèques externes. Aucune manipulation particulière n’est nécessaire. Les fonctionnalités de gestions de dépendances sont intégrées à cargo. Ainsi on peut ajouter une bibliothèque à un projet grâce à la commande cargo add ou en ajoutant une ligne à Cargo.toml.
Les bibliothèques en rust on tout de même des particularités :
- On les appelle des
crates. - Ce ne sont pas des bibliothèques partagées comme en C ou C++. Ainsi elles ne sont pas distribuée en
.soou.dll. - Les bibliothèques sont statiquement compilées. C’est à dire que votre exécutable final contient votre code et celui des bibliothèques employées.
- Elles ne sont pas distribuées compilées : chaque personne qui compile votre projet compile aussi toutes ses dépendances, ce qui prend du temps sur des projets d’ampleur. Heureusement la compilation est itérative, rendant toutes les compilations successives plus rapides.
Aujourd’hui vous allez découvrir trois des crates les populaires :
clappour gérer les arguments d’un programme ;serdepour générer et lire différents formats de fichiers d’échange/stockage de données ;chronopour travailler avec des dates.
Clap : gestion des arguments
Explications
En rust la façon la plus populaire de gérer les arguments dans les programmes c’est d’utiliser clap.
Pour installer clap :
cargo add clap --features=derive
Sa fonctionnalité derive nous permet de définir une structure qui va être remplie par les arguments du programme que clap va parser.
use clap::Parser;
#[derive(Parser)]
struct Opt {
action: String,
quantity_items: Vec<String>,
#[arg(short, long)]
file: Option<String>,
}
pub fn main() {
let opt = Opt::parse();
// [...]
}
Exemple de commandes qui fonctionneraient avec la structure ci-dessus :
cargo run -- insert "1 Potato" "7 Tomato" -f inventory.json
# action: "insert", quantity_items: ["1 Potato", "7 Tomato"],
# file: Some("inventory.json")
cargo run -- remove "1 Potato"
# action: "remove", quantity_items: ["1 Potato"], file: None
Les champs qui ne sont pas décorés par arg sont des arguments positionnels, ils sont remplis dans l’ordres des arguments.
En revanche, les champs décorés par arg sont des drapeaux (pensez -f example.json ou --file=example.json). Les champs Option représentent des flags avec valeurs qui ne sont pas obligatoires.
Les fonctionnalités de clap sont beaucoup plus poussées que ce que l’on voit dans ce court exemple. Consultez les exemples du livre de recettes pour vous en inspirer. Vous pouvez aussi consulter les tutoriels. Faites des expériences, cherchez ce qui vous plait le plus.
Exercice 1 : clap opérations simples
Fichier à rendre : src/bin/task_1.rs
En utilisant clap, écrivez un programme qui exécute des opérations simples passées en arguments de votre programme et affiche le résultat sur le terminal.
$ cargo run --bin task_1 -- add 5 10 3
18
$ cargo run --bin task_1 -- div 50 5
10
$ cargo run --bin task_1 -- mul 10 5
50
$ cargo run --bin task_1 -- avg 50 100 150
100
Implémentez les additions, soustractions, multiplications, divisions, et les moyennes.
Les commandes add, sub, avg prennent une liste de nombre. Les autres n’en prennent que deux.
Serde : générer et lire des formats communs
Explications
La sérialisation, c’est le principe de passer de données qui sont en binaire dans la RAM d’un processus, ou en base de donnée, et de les transformer en un format d’échange pour le transit (dans des paquets réseau, en radiofréquence, etc.) ou le stockage (sur le disque, sur cassette, en format papier, en QRcode, etc.)
L’objectif de la sérialisation c’est de permettre à un autre processus du même programme ou à un autre programme de reconstruire les données sérialisées pour les manipuler comme n’importe quelle autre donnée en RAM. Le nom de se processus inverse, c’est la dé-sérialisation.
Le format de sérialisation préféré des API WEB aujourd’hui c’est le JSON, mais il en existe bien d’autres.
La sérialisation en rust se fait grâce au crate serde et à grâce à un générateur/interpréteur comme serde_json, ron, etc..
Une structure peut être sérialisée avec un générateur compatible dès lors qu’elle implémente serde::Serialize à l’aide de derive.
#[derive(serde::Serialize)]
struct InventoryRow {
item: String,
quantity: usize,
}
fn main() {
let vec = sample_data();
ser_pretty_print(&vec);
ser_to_file(vec);
}
fn ser_to_file(vec: Vec<InventoryRow>) {
let file = match std::fs::File::create("invent.json") {
Ok(file) => file,
Err(err) => {
eprintln!("failed opening/creating the file {err}");
return;
}
};
match serde_json::ser::to_writer(&file, &vec) {
Ok(_) => println!("ser to file success"),
Err(err) => eprintln!("ser failed {}", err),
}
}
fn ser_pretty_print(vec: &Vec<InventoryRow>) {
match serde_json::ser::to_string_pretty(vec) {
Ok(out) => println!("{out}"),
Err(err) => eprintln!("ser to string failed {err}"),
};
}
Tous les champs de votre structure doivent aussi implémenter Serialize, de la même manière. Les types qui font parti du langage rust ont déjà une implémentation par défaut de Serialize. Les crates que vous installez on souvent une feature serde qui rend ses types et structures compatibles.
Le générateur que vous choisissez documentera comment est-ce qu’il s’utilise :
Les générateurs sont généralement capables de générer une chaîne de caractère dans le langage qu’ils couvrent, ou alors d’écrire directement dans un fichier.
Une structure peut être dé-sérialisée avec un parser/générateur dès lors qu’elle implémente serde::Deserialize.
#[derive(serde::Deserialize, Debug)]
struct InventoryRow {
item: String,
quantity: usize,
}
fn main() {
deser();
}
fn deser() {
let file = match std::fs::File::open("invent.json") {
Ok(file) => file,
Err(err) => {
eprintln!("failed to open the file {}", err);
return;
}
};
let deserialized: Vec<InventoryRow> = match serde_json::de::from_reader(&file) {
Ok(deserialized) => deserialized,
Err(err) => {
eprintln!("failed to deserialise {}", err);
return;
}
};
dbg!(deserialized);
}
Testez les comportement des générateurs, et essayer de sérialiser une structure de votre conception. Cherchez ensuite comment faire l’opération inverse : remplir une structure à base de JSON on de RON.
Exercice 2 : serde historique des opérations
Fichier à rendre : src/bin/task_2.rs
Partez de votre exercice 1.
Écrivez une structure HistoryRow qui permet de stocker :
- le nom de l’opération
- les opérandes de l’opération
- le résultat de l’opération.
Faites-la dériver de serde::Serialize et de serde::Deserialize.
L’objectif de votre programme c’est de tenir un historique de toutes les opérations que vous avez faites. Pour ce faire à chaque invocation, vous devez charger et dé-sérialiser l’historique, ajouter l’opération actuelle et son résultat à la liste dans l’historique, sérialiser l’historique, ré-écrire le fichier d’historique avec le nouveau contenu.
Exemple d’un fichier history.json :
[
{
"kind": "add",
"operands": [12, 1600, 5],
"result": 1617
},
{
"kind": "mul",
"operands": [12, 10],
"result": 120
},
{
"kind": "avg",
"operands": [50, 100, 150],
"result": 1617
}
]
Autrement dit, votre programme doit avoir un déroulement similaire à celui-ci :
- dé-sérialisation du fichier
history.json; - parsing des arguments du programme ;
- exécution de l’opération et affichage du résultat ;
- ajout de l’opération et de son résultat dans l’historique ;
- sérialisation de l’historique et remplacement du contenu du fichier
history.json.
Le résultat final doit ressembler à ceci :
$ cargo run --bin task_2 -- add 5 10 3
18
$ cat history.json | jq
[
{
"kind": "add",
"operands": [5, 10, 3],
"result": 18
}
]
& cargo run --bin task_2 -- div 50 5
10
$ cat history.json | jq
[
{
"kind": "add",
"operands": [5, 10, 3],
"result": 18
},
{
"kind": "div",
"operands": [50, 5],
"result": 10
},
]
$ cargo run --bin task_2 -- mul 10 5
50
$ cat history.json | jq
[
{
"kind": "add",
"operands": [5, 10, 3],
"result": 18
},
{
"kind": "div",
"operands": [50, 5],
"result": 10
},
{
"kind": "mul",
"operands": [10, 5],
"result": 50
}
]
$
Chrono
Explications
Pour les dates on utilise en général le crate chrono. Lorsque le fuseau horaire n’est pas important on utilise NaiveDate .
Installez chrono en ajoutant cette ligne à votre Cargo.toml.
chrono = { version = "0.4.38", default-features = false, features = ["alloc", "now", "serde", "std", "clock"] }
Il existe plusieurs façon de créer une date, la plupart renvoie un result, au cas où les paramètres ne permettent pas de créer une date valide. À vous de gérer leur erreurs quand elles peuvent se produire.
// L'utilisation de "use" avec un prelude est toujours autorisée par la norme
use chrono::prelude::*;
let example_ymd = NaiveDate::from_ymd_opt(2024, 04, 25).unwrap();
// L'utilisation de "use" avec un trait est toujours autorisée par la norme
use std::str::FromStr;
let example_from_str = NaiveDate::from_str("2024-08-02").unwrap();
let example_now = Local::now().date_naive();
Pour le reste des fonctionnalités, je vous invite à épier la documentation et à les tester.
Exercice 3 : delta temporel
Fichier à rendre : src/bin/task_3.rs
À partir d’un fichier JSON comme celui ci-dessous, affichez pour chaque événement son nom et le delta temps entre aujourd’hui et l’événement. Triés de façon ascendante.
[
{
"name": "ZEvent 2024",
"date": "2024-09-06"
},
{
"name": "Airbus A350 maiden flight",
"date": "2013-06-14"
},
{
"name": "MIKU EXPO tour",
"date": "2024-10-29"
},
{
"name": "Metro Line 14 extention",
"date": "2024-06-24"
}
]
$ date --iso-8601=date
2024-08-22
$ cargo run --bin task_3 -- important_events.json
Airbus A350 maiden flight 11 years ago
Metro Line 14 extension 2 months ago
=== today ===
ZEvent 2024 in 15 days
MIKU EXPO tour in 2 months
Exercice 4 : insertion et suppression
Fichier à rendre : src/bin/task_4.rs
En partant de votre exercice 4, ajoutez la possibilité de supprimer ou d’insérer des dates au fichier avec les commandes :
$ date --iso-8601=date
2024-08-22
$ cargo run --bin task_3 -- important_events.json
Airbus A350 maiden flight 11 years ago
Metro Line 14 extension 2 months ago
=== today ===
ZEvent 2024 in 15 days
MIKU EXPO tour in 2 months
$ cargo run --bin task_4 -- important_evens.json add "End of year" "2025-06-27"
Airbus A350 maiden flight 11 years ago
Metro Line 14 extension 2 months ago
=== today ===
ZEvent 2024 in 15 days
MIKU EXPO tour in 2 months
End of year in 10 months
$ cargo run --bin task_4 -- important_evens.json
Airbus A350 maiden flight 11 years ago
Metro Line 14 extension 2 months ago
=== today ===
ZEvent 2024 in 15 days
MIKU EXPO tour in 2 months
End of year in 10 months
$ cargo run --bin task_4 -- important_evens.json remove "ZEvent 2024"
Airbus A350 maiden flight 11 years ago
Metro Line 14 extension 2 months ago
=== today ===
MIKU EXPO tour in 2 months
End of year in 10 months