1. Les fils d’exécution
Un processeur moderne a plusieurs cœurs. Un fil d’exécution (thread) permet de faire travailler plusieurs cœurs en parallèle.
use std::thread;
use std::time::Duration;
fn main() {
let poignee = thread::spawn(|| {
for i in 1..=3 {
println!(" fil secondaire : étape {i}");
thread::sleep(Duration::from_millis(50));
}
42 // valeur renvoyée par le fil
});
for i in 1..=3 {
println!("fil principal : étape {i}");
thread::sleep(Duration::from_millis(30));
}
let resultat = poignee.join().expect("le fil a paniqué");
println!("le fil a renvoyé {resultat}");
}
L’ordre d’affichage change d’une exécution à l’autre : les fils avancent en même temps. join() attend la fin du fil et récupère sa valeur.
Donner des données à un fil : move
use std::thread;
fn main() {
let notes = vec![12, 15, 9];
let poignee = thread::spawn(move || { // move : le fil prend possession de notes
let somme: i32 = notes.iter().sum();
somme
});
// println!("{:?}", notes); ← refusé : notes appartient désormais au fil
println!("somme : {}", poignee.join().unwrap());
}
Sans move, le compilateur refuse : le fil pourrait vivre plus longtemps que main et utiliser un vecteur déjà libéré. On retrouve exactement les règles du niveau 2, appliquées au parallélisme.
Découper un travail : thread::scope
use std::thread;
fn main() {
let nombres: Vec<u64> = (1..=1_000_000).collect();
let morceaux: Vec<&[u64]> = nombres.chunks(250_000).collect();
let total: u64 = thread::scope(|s| {
let poignees: Vec<_> = morceaux
.iter()
.map(|m| s.spawn(move || m.iter().sum::<u64>()))
.collect();
poignees.into_iter().map(|p| p.join().unwrap()).sum()
});
println!("total : {total}"); // 500000500000
}
Avec thread::scope, les fils sont garantis terminés à la sortie du bloc : ils peuvent donc emprunter des données du fil principal, sans move ni copie.
2. Partager des données : Arc et Mutex
Plusieurs fils qui modifient la même donnée, c’est la recette d’un accès concurrent (data race) : deux fils lisent 10, ajoutent 1, écrivent 11, et une incrémentation est perdue. En C ou en Java, ce bug compile et apparaît au hasard. En Rust, il ne compile pas.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let compteur = Arc::new(Mutex::new(0));
let mut poignees = Vec::new();
for _ in 0..8 {
let c = Arc::clone(&compteur); // une nouvelle poignée vers le même compteur
poignees.push(thread::spawn(move || {
for _ in 0..1000 {
let mut valeur = c.lock().unwrap(); // on obtient l'accès exclusif
*valeur += 1;
} // le verrou est relâché ici
}));
}
for p in poignees {
p.join().unwrap();
}
println!("compteur : {}", *compteur.lock().unwrap()); // toujours 8000
}
| Outil | Rôle |
|---|---|
Arc<T> | Plusieurs propriétaires d’une même valeur, entre fils (compteur de références atomique) |
Mutex<T> | Un seul fil à la fois accède à la valeur ; lock() attend son tour |
RwLock<T> | Plusieurs lecteurs ou un écrivain : la règle d’emprunt, à l’exécution |
| types atomiques | AtomicU64… pour un simple compteur, sans verrou |
3. Communiquer par canaux
« Ne communiquez pas en partageant la mémoire, partagez la mémoire en communiquant. » Un canal transporte des valeurs d’un fil à un autre.
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (emetteur, recepteur) = mpsc::channel();
for id in 1..=3 {
let tx = emetteur.clone();
thread::spawn(move || {
for mesure in 0..3 {
let texte = format!("capteur {id} : mesure {mesure}");
tx.send(texte).unwrap(); // la valeur est déplacée dans le canal
thread::sleep(Duration::from_millis(10 * id));
}
});
}
drop(emetteur); // sans ça, la boucle ne s'arrêterait jamais
for message in recepteur { // s'arrête quand tous les émetteurs ont disparu
println!("{message}");
}
}
mpsc signifie multiple producers, single consumer : plusieurs émetteurs, un seul récepteur. Une valeur envoyée est déplacée : l’émetteur ne peut plus la modifier, aucun partage n’est possible.
4. La programmation asynchrone
Un serveur web passe l’essentiel de son temps à attendre : le réseau, le disque, une base de données. Créer un fil par connexion devient coûteux au-delà de quelques milliers. La programmation asynchrone permet à un petit nombre de fils de gérer des dizaines de milliers de tâches : pendant qu’une tâche attend, le fil en exécute une autre.
# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
use std::time::{Duration, Instant};
use tokio::time::sleep;
async fn interroger(serveur: &str, ms: u64) -> String {
sleep(Duration::from_millis(ms)).await; // attente non bloquante
format!("{serveur} a répondu en {ms} ms")
}
#[tokio::main]
async fn main() {
let debut = Instant::now();
// en séquence : 300 + 200 + 100 ms
println!("{}", interroger("srv-a", 300).await);
println!("{}", interroger("srv-b", 200).await);
println!("{}", interroger("srv-c", 100).await);
println!("séquentiel : {:?}", debut.elapsed());
// en concurrence : environ 300 ms au total
let debut = Instant::now();
let (a, b, c) = tokio::join!(
interroger("srv-a", 300),
interroger("srv-b", 200),
interroger("srv-c", 100),
);
println!("{a}\n{b}\n{c}");
println!("concurrent : {:?}", debut.elapsed());
// tâches indépendantes
let tache = tokio::spawn(async { interroger("srv-d", 50).await });
println!("{}", tache.await.unwrap());
}
- Une fonction
asyncne s’exécute pas quand on l’appelle : elle renvoie une future, un calcul en attente. .awaitexécute la future et cède la main pendant les attentes.- Rust ne fournit pas l’exécuteur des futures : c’est le rôle de Tokio, la bibliothèque la plus utilisée, activée par
#[tokio::main]. - Dans une fonction
async, on n’utilise jamaisstd::thread::sleepni une lecture de fichier bloquante : elles immobilisent tout le fil et toutes les tâches qui s’y trouvent. On utilise leurs équivalentstokio::.
| Fils (threads) | Tâches asynchrones | |
|---|---|---|
| Adapté à | Calculs lourds (CPU) | Attentes nombreuses (réseau, entrées-sorties) |
| Coût d’une unité | Quelques Mo de mémoire | Quelques Ko |
| Nombre raisonnable | Quelques dizaines | Des dizaines de milliers |
5. Une API REST avec Axum
Une API REST expose des ressources par HTTP : GET pour lire, POST pour créer, PUT pour remplacer, DELETE pour supprimer, avec des données au format JSON. Axum, construit sur Tokio, est l’un des cadriciels web les plus utilisés en Rust.
# Cargo.toml
[dependencies]
axum = "0.8"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
use axum::{
extract::{Path, State},
http::StatusCode,
routing::get,
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, RwLock};
#[derive(Clone, Serialize)]
struct Poste {
id: u32,
nom: String,
salle: u16,
}
#[derive(Deserialize)]
struct NouveauPoste {
nom: String,
salle: u16,
}
type Etat = Arc<RwLock<Vec<Poste>>>;
async fn lister(State(etat): State<Etat>) -> Json<Vec<Poste>> {
Json(etat.read().unwrap().clone())
}
async fn lire(State(etat): State<Etat>, Path(id): Path<u32>) -> Result<Json<Poste>, StatusCode> {
etat.read()
.unwrap()
.iter()
.find(|p| p.id == id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
async fn creer(
State(etat): State<Etat>,
Json(entree): Json<NouveauPoste>,
) -> Result<(StatusCode, Json<Poste>), (StatusCode, String)> {
let nom = entree.nom.trim();
if nom.is_empty() || nom.len() > 32 {
return Err((StatusCode::UNPROCESSABLE_ENTITY, "nom : 1 à 32 caractères".into()));
}
let mut postes = etat.write().unwrap();
let id = postes.iter().map(|p| p.id).max().unwrap_or(0) + 1;
let poste = Poste { id, nom: nom.to_string(), salle: entree.salle };
postes.push(poste.clone());
Ok((StatusCode::CREATED, Json(poste)))
}
#[tokio::main]
async fn main() {
let etat: Etat = Arc::new(RwLock::new(vec![
Poste { id: 1, nom: "PC-207-01".into(), salle: 207 },
]));
let app = Router::new()
.route("/postes", get(lister).post(creer))
.route("/postes/{id}", get(lire))
.with_state(etat);
let ecoute = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("API sur http://127.0.0.1:3000");
axum::serve(ecoute, app).await.unwrap();
}
$ curl http://127.0.0.1:3000/postes
[{"id":1,"nom":"PC-207-01","salle":207}]
$ curl -i -X POST http://127.0.0.1:3000/postes \
-H "Content-Type: application/json" -d '{"nom":"PC-112-04","salle":112}'
HTTP/1.1 201 Created
{"id":2,"nom":"PC-112-04","salle":112}
$ curl -i http://127.0.0.1:3000/postes/99
HTTP/1.1 404 Not Found
$ curl -i -X POST http://127.0.0.1:3000/postes \
-H "Content-Type: application/json" -d '{"nom":"X","salle":70000}'
HTTP/1.1 422 Unprocessable Entity
- Chaque route associe un chemin et une méthode HTTP à une fonction
asyncappelée gestionnaire. - Les extracteurs (
State,Path,Json) récupèrent et désérialisent les données de la requête : un JSON mal formé ou une salle supérieure à 65 535 sont refusés automatiquement, avant même d’atteindre ton code. - Un gestionnaire renvoie tout ce qui peut devenir une réponse :
Json(…), un code d’état, un tuple des deux, ou unResultde ces éléments. - L’état partagé est un
Arc<RwLock<…>>: exactement les outils de la partie 2.
6. unsafe : les limites de la garantie
Certaines opérations ne peuvent pas être vérifiées par le compilateur : manipuler une adresse mémoire brute, appeler une fonction écrite en C, dialoguer avec le matériel. Rust les autorise dans un bloc unsafe, où le programmeur prend la responsabilité de la sûreté.
fn main() {
let x = 42;
let adresse = &x as *const i32; // pointeur brut : autorisé
let valeur = unsafe { *adresse }; // le déréférencer : seulement dans unsafe
println!("{valeur}");
}
unsafene désactive pas le vérificateur : il autorise seulement quelques opérations supplémentaires.- La bibliothèque standard et certaines crates en contiennent, encapsulées derrière une interface sûre et auditée.
- Dans son propre code d’application, on n’en a presque jamais besoin. On peut l’interdire totalement en ajoutant en tête de
main.rsoulib.rs:#![forbid(unsafe_code)]. cargo geigercompte le codeunsafeprésent dans les dépendances : un indicateur utile pour choisir une crate.
7. Projet final : l’API d’inventaire du parc
8. Et après ?
| Direction | Pour faire quoi | À explorer |
|---|---|---|
| Web complet | Base de données, authentification, gabarits HTML | sqlx, tower-http, askama, Leptos |
| Outils système | Remplacer des scripts par des outils rapides et sûrs | clap, walkdir, sysinfo, ssh2 |
| Réseau et sécurité | Analyseurs de trames, scanners, outils de supervision | pnet, rustls, ring |
| Embarqué | Microcontrôleurs sans système d’exploitation | Embassy, embedded-hal |
| WebAssembly | Du Rust exécuté dans le navigateur | wasm-bindgen, wasm-pack |
| Aller plus loin dans le langage | Tout comprendre en profondeur | Le livre officiel « The Rust Programming Language », Rustlings, Exercism |
9. Bilan du parcours
Exercices corrigés
Ce niveau touche à des bibliothèques qui évoluent vite. Les corrigés ont été écrits pour Tokio 1 et Axum 0.8 : si une écriture a changé, la documentation de chaque crate sur docs.rs et les exemples de son dépôt sont la référence.
Exercice 7.1 — Parallélisme
Voir le corrigé
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
fn est_premier(n: u64) -> bool {
if n < 2 { return false; }
let mut d = 2;
while d * d <= n {
if n % d == 0 { return false; }
d += 1;
}
true
}
fn pseudo_aleatoire(graine: u64) -> u64 {
let t = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_nanos() as u64;
(t ^ graine.wrapping_mul(2654435761)) % 40 + 10
}
fn main() {
// 1. Premiers : un fil, puis quatre
const MAX: u64 = 200_000;
let debut = Instant::now();
let seul = (0..MAX).filter(|&n| est_premier(n)).count();
println!("1 fil : {seul} premiers en {:?}", debut.elapsed());
let debut = Instant::now();
let quart = MAX / 4;
let total: usize = thread::scope(|s| {
let poignees: Vec<_> = (0..4)
.map(|i| s.spawn(move || (i * quart..(i + 1) * quart).filter(|&n| est_premier(n)).count()))
.collect();
poignees.into_iter().map(|p| p.join().unwrap()).sum()
});
println!("4 fils : {total} premiers en {:?}", debut.elapsed());
// 2. Téléchargements simulés
let (tx, rx) = mpsc::channel();
for fil in 1..=4u64 {
let tx = tx.clone();
thread::spawn(move || {
for fichier in 1..=5u64 {
let ms = pseudo_aleatoire(fil * 10 + fichier);
thread::sleep(Duration::from_millis(ms));
tx.send((fil, fichier, ms)).unwrap();
}
});
}
drop(tx);
let mut cumul = 0;
for (fil, fichier, ms) in rx {
println!("fil {fil} : fichier {fichier} en {ms} ms");
cumul += ms;
}
println!("temps cumulé : {cumul} ms");
// 3. Compteur atomique
let debut = Instant::now();
let compteur = Arc::new(AtomicU64::new(0));
let poignees: Vec<_> = (0..8)
.map(|_| {
let c = Arc::clone(&compteur);
thread::spawn(move || {
for _ in 0..1000 {
c.fetch_add(1, Ordering::Relaxed);
}
})
})
.collect();
for p in poignees { p.join().unwrap(); }
println!("atomique : {} en {:?}", compteur.load(Ordering::Relaxed), debut.elapsed());
}
Explication
Le découpage en quatre intervalles donne des charges inégales (les grands nombres sont plus longs à tester), mais le gain reste net sur une machine à plusieurs cœurs. Le compteur atomique est plus rapide que le Mutex car il n’y a ni attente ni verrou : le processeur garantit que l’addition est indivisible.
Exercice 7.2 — Vérifier un parc de serveurs
Voir le corrigé
// Cargo.toml : tokio = { version = "1", features = ["full"] }
use std::time::{Duration, Instant};
use tokio::net::TcpStream;
use tokio::time::timeout;
async fn verifier(hote: &str, port: u16) -> (String, bool, u128) {
let debut = Instant::now();
let cible = format!("{hote}:{port}");
let ok = matches!(
timeout(Duration::from_secs(2), TcpStream::connect(&cible)).await,
Ok(Ok(_))
);
(cible, ok, debut.elapsed().as_millis())
}
#[tokio::main]
async fn main() {
let cibles = [
("192.168.56.10", 22), ("192.168.56.10", 80), ("192.168.56.11", 3389),
("192.168.56.12", 445), ("192.168.56.20", 53), ("127.0.0.1", 3000),
];
let debut = Instant::now();
let taches: Vec<_> = cibles
.iter()
.map(|&(h, p)| tokio::spawn(async move { verifier(h, p).await }))
.collect();
let mut resultats = Vec::new();
for t in taches {
resultats.push(t.await.expect("tâche interrompue"));
}
resultats.sort_by_key(|r| r.2);
for (cible, ok, ms) in &resultats {
println!("{cible:<22} {:<9} {ms:>5} ms", if *ok { "ouvert" } else { "fermé" });
}
println!("total : {:?}", debut.elapsed());
}
Explication
timeout renvoie une erreur si la future n’a pas abouti dans le délai ; connect renvoie elle-même un Result. D’où le motif Ok(Ok(_)) : « pas de dépassement de délai, et connexion réussie ». Toutes les tâches démarrent immédiatement : le temps total est celui de la plus lente, borné à 2 secondes.
Pour aller plus loin
Ce programme est un petit scanner de ports. Utilisé sur un réseau qui n’est pas le tien, il constitue une tentative d’intrusion au regard de la loi.
Projet final : l’API d’inventaire du parc
Voir le corrigé
Le corrigé complet dépasse la place d’un livret ; voici le squelette et les éléments décisifs. Les dépendances : axum = "0.8", tokio (full), serde (derive), serde_json, subtle, et pour les tests tower avec la fonctionnalité util.
#![forbid(unsafe_code)]
use axum::{
extract::{Path, Query, Request, State},
http::{header, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Response},
routing::get,
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use std::sync::Arc;
use tokio::sync::RwLock;
// ---------- types validés ----------
#[derive(Clone, Serialize, Deserialize, PartialEq)]
#[serde(try_from = "String", into = "String")]
struct NomPoste(String);
impl TryFrom<String> for NomPoste {
type Error = String;
fn try_from(s: String) -> Result<Self, String> {
let ok = (3..=32).contains(&s.len())
&& s.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '-');
if ok { Ok(NomPoste(s)) } else { Err("nom : 3 à 32 caractères A-Z, 0-9 ou -".into()) }
}
}
impl From<NomPoste> for String {
fn from(n: NomPoste) -> String { n.0 }
}
#[derive(Clone, Serialize, Deserialize)]
struct Poste { id: u32, nom: NomPoste, salle: u16, ram_go: u32, ip: IpAddr }
#[derive(Deserialize)]
struct Saisie { nom: NomPoste, salle: u16, ram_go: u32, ip: IpAddr }
#[derive(Deserialize)]
struct Filtre { salle: Option<u16>, ram_max: Option<u32> }
// ---------- erreurs ----------
enum ErreurApi { Introuvable, Conflit, NonAutorise, Interne }
impl IntoResponse for ErreurApi {
fn into_response(self) -> Response {
let (code, msg) = match self {
ErreurApi::Introuvable => (StatusCode::NOT_FOUND, "poste introuvable"),
ErreurApi::Conflit => (StatusCode::CONFLICT, "ce nom est déjà utilisé"),
ErreurApi::NonAutorise => (StatusCode::UNAUTHORIZED, "authentification requise"),
ErreurApi::Interne => (StatusCode::INTERNAL_SERVER_ERROR, "erreur interne"),
};
(code, Json(serde_json::json!({ "erreur": msg }))).into_response()
}
}
// ---------- état ----------
struct Etat { postes: RwLock<Vec<Poste>>, jeton: String, fichier: String }
type Partage = Arc<Etat>;
async fn sauvegarder(e: &Etat, postes: &[Poste]) -> Result<(), ErreurApi> {
let json = serde_json::to_string_pretty(postes).map_err(|_| ErreurApi::Interne)?;
tokio::fs::write(&e.fichier, json).await.map_err(|err| {
eprintln!("écriture de {} impossible : {err}", e.fichier); // détail au journal
ErreurApi::Interne // réponse sobre
})
}
// ---------- authentification ----------
async fn exiger_jeton(State(e): State<Partage>, req: Request, next: Next) -> Result<Response, ErreurApi> {
use subtle::ConstantTimeEq;
let fourni = req.headers().get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.unwrap_or("");
if fourni.as_bytes().ct_eq(e.jeton.as_bytes()).into() {
Ok(next.run(req).await)
} else {
Err(ErreurApi::NonAutorise)
}
}
// ---------- gestionnaires ----------
async fn lister(State(e): State<Partage>, Query(f): Query<Filtre>) -> Json<Vec<Poste>> {
let postes = e.postes.read().await;
Json(postes.iter()
.filter(|p| f.salle.is_none_or(|s| p.salle == s))
.filter(|p| f.ram_max.is_none_or(|r| p.ram_go <= r))
.cloned().collect())
}
async fn lire(State(e): State<Partage>, Path(id): Path<u32>) -> Result<Json<Poste>, ErreurApi> {
e.postes.read().await.iter().find(|p| p.id == id).cloned().map(Json).ok_or(ErreurApi::Introuvable)
}
async fn creer(State(e): State<Partage>, Json(s): Json<Saisie>) -> Result<(StatusCode, Json<Poste>), ErreurApi> {
let mut postes = e.postes.write().await; // contrôle ET ajout sous le même verrou
if postes.iter().any(|p| p.nom == s.nom) {
return Err(ErreurApi::Conflit);
}
let id = postes.iter().map(|p| p.id).max().unwrap_or(0) + 1;
let p = Poste { id, nom: s.nom, salle: s.salle, ram_go: s.ram_go, ip: s.ip };
postes.push(p.clone());
sauvegarder(&e, &postes).await?;
Ok((StatusCode::CREATED, Json(p)))
}
async fn supprimer(State(e): State<Partage>, Path(id): Path<u32>) -> Result<StatusCode, ErreurApi> {
let mut postes = e.postes.write().await;
let avant = postes.len();
postes.retain(|p| p.id != id);
if postes.len() == avant { return Err(ErreurApi::Introuvable); }
sauvegarder(&e, &postes).await?;
Ok(StatusCode::NO_CONTENT)
}
fn application(etat: Partage) -> Router {
let lecture = Router::new()
.route("/postes", get(lister))
.route("/postes/{id}", get(lire));
let ecriture = Router::new()
.route("/postes", axum::routing::post(creer))
.route("/postes/{id}", axum::routing::delete(supprimer))
.route_layer(middleware::from_fn_with_state(etat.clone(), exiger_jeton));
lecture.merge(ecriture).with_state(etat)
}
#[tokio::main]
async fn main() {
let jeton = std::env::var("API_JETON").expect("la variable API_JETON doit être définie");
assert!(jeton.len() >= 32, "API_JETON doit faire au moins 32 caractères");
let fichier = String::from("inventaire.json");
let postes = match tokio::fs::read_to_string(&fichier).await {
Ok(t) => serde_json::from_str(&t).expect("inventaire.json invalide"),
Err(_) => Vec::new(),
};
let etat = Arc::new(Etat { postes: RwLock::new(postes), jeton, fichier });
let ecoute = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(ecoute, application(etat)).await.unwrap();
}
Explication
#[serde(try_from = "String")] fait passer chaque nom reçu par TryFrom : un nom invalide est refusé pendant la désérialisation, avec un code 422, sans une ligne de validation dans les gestionnaires. Les routes d’écriture sont regroupées sous une couche (route_layer) qui exige le jeton : impossible d’oublier la vérification sur une nouvelle route de ce groupe. La comparaison du jeton se fait en temps constant (subtle) pour ne pas révéler, par le temps de réponse, combien de caractères sont justes. Le contrôle d’unicité et l’ajout se font sous le même verrou d’écriture : deux requêtes simultanées ne peuvent pas créer deux fois le même nom. Le verrou est ici celui de Tokio (tokio::sync::RwLock), qui peut être conservé pendant un .await, contrairement à celui de la bibliothèque standard.
is_none_or demande Rust 1.82 ou plus récent ; sur une version plus ancienne, écris f.salle.map_or(true, |s| p.salle == s).
Pour le test des routes, tower::ServiceExt::oneshot envoie une requête à l’application sans ouvrir de port :
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;
fn etat_test() -> Partage {
Arc::new(Etat { postes: RwLock::new(Vec::new()), jeton: "x".repeat(32), fichier: "test.json".into() })
}
#[tokio::test]
async fn creation_sans_jeton_refusee() {
let req = Request::post("/postes")
.header("content-type", "application/json")
.body(Body::from(r#"{"nom":"PC-01","salle":207,"ram_go":8,"ip":"10.0.0.1"}"#))
.unwrap();
let rep = application(etat_test()).oneshot(req).await.unwrap();
assert_eq!(rep.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn poste_inconnu() {
let req = Request::get("/postes/42").body(Body::empty()).unwrap();
let rep = application(etat_test()).oneshot(req).await.unwrap();
assert_eq!(rep.status(), StatusCode::NOT_FOUND);
}
#[test]
fn nom_invalide_refuse() {
assert!(NomPoste::try_from(String::from("pc 01")).is_err());
assert!(NomPoste::try_from(String::from("PC-207-01")).is_ok());
}
}
Pour aller plus loin
Les refus de désérialisation (422) renvoient pour l’instant le message texte par défaut d’Axum. Pour des erreurs toutes au format JSON, on intercepte ce rejet : la documentation d’Axum le décrit dans sa partie consacrée aux extracteurs.
Les routes PUT et POST /import suivent les mêmes modèles. Pour l’import, ajoute la bibliothèque du niveau 6 en dépendance locale : analyse-journal = { path = "../analyse-journal" }, et appelle analyse::analyser(corps.lines(), 5). C’est l’intérêt d’avoir séparé bibliothèque et exécutable.
🧠 Testez vos connaissances
5 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.
© 2026 Bruno Jousseaume. Ce cours est publié sous licence CC BY-NC-SA 4.0 : vous pouvez le partager et l’adapter en citant l’auteur et un lien vers cette page, sans usage commercial, et en partageant vos adaptations sous la même licence.