Aller au contenu

Exemples d'implémentation en Rust

Deux projets complets illustrent les usages les plus courants de Rust en production : une API REST CRUD avec persistance base de données, et un outil système performant compilable en WebAssembly. Les deux exemples utilisent l'edition 2024 et les crates stables les plus récentes.


Projet 1 — API REST CRUD avec Axum et SQLx

Ce projet implémenté un CRUD complet sur une ressource Item avec Axum pour le routage, SQLx pour la persistance SQLite, et thiserror pour la gestion des erreurs métier.

Structure du projet

api-crud/
├── Cargo.toml
├── migrations/
│   └── 001_items.sql
└── src/
    ├── main.rs
    ├── erreur.rs
    └── item.rs

Cargo.toml

[package]
name = "api-crud"
version = "0.1.0"
edition = "2024"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "macros"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"
tower-http = { version = "0.5", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"

migrations/001_items.sql

CREATE TABLE IF NOT EXISTS items (
    id      INTEGER PRIMARY KEY AUTOINCREMENT,
    nom     TEXT    NOT NULL,
    valeur  REAL    NOT NULL DEFAULT 0.0
);

src/erreur.rs — types d'erreurs métier

use thiserror::Error;

/// Erreurs metier de l'application
#[derive(Error, Debug)]
pub enum ErreurApp {
    #[error("Item introuvable : id={0}")]
    NonTrouve(i64),

    #[error("Donnees invalides : {0}")]
    Invalide(String),

    #[error("Erreur base de donnees : {0}")]
    BDD(#[from] sqlx::Error),
}

// Conversion vers une reponse HTTP Axum
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use serde_json::json;

impl IntoResponse for ErreurApp {
    fn into_response(self) -> Response {
        let (statut, message) = match &self {
            ErreurApp::NonTrouve(_) => (StatusCode::NOT_FOUND, self.to_string()),
            ErreurApp::Invalide(_) => (StatusCode::BAD_REQUEST, self.to_string()),
            ErreurApp::BDD(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Erreur interne".into()),
        };
        (statut, Json(json!({ "erreur": message }))).into_response()
    }
}

src/item.rs — handlers CRUD

use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::Json,
};
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;

use crate::erreur::ErreurApp;

#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
pub struct Item {
    pub id: i64,
    pub nom: String,
    pub valeur: f64,
}

/// Payload de creation / mise a jour (sans id)
#[derive(Debug, Deserialize)]
pub struct ItemPayload {
    pub nom: String,
    pub valeur: f64,
}

/// Validation du payload
fn valider(p: &ItemPayload) -> Result<(), ErreurApp> {
    if p.nom.trim().is_empty() {
        return Err(ErreurApp::Invalide("Le nom ne peut pas etre vide".into()));
    }
    if p.valeur < 0.0 {
        return Err(ErreurApp::Invalide("La valeur doit etre positive".into()));
    }
    Ok(())
}

// GET /items
pub async fn lister(State(pool): State<SqlitePool>) -> Result<Json<Vec<Item>>, ErreurApp> {
    let items = sqlx::query_as!(Item, "SELECT id, nom, valeur FROM items ORDER BY id")
        .fetch_all(&pool)
        .await?;
    Ok(Json(items))
}

// GET /items/:id
pub async fn obtenir(
    Path(id): Path<i64>,
    State(pool): State<SqlitePool>,
) -> Result<Json<Item>, ErreurApp> {
    sqlx::query_as!(Item, "SELECT id, nom, valeur FROM items WHERE id = ?", id)
        .fetch_optional(&pool)
        .await?
        .map(Json)
        .ok_or(ErreurApp::NonTrouve(id))
}

// POST /items
pub async fn creer(
    State(pool): State<SqlitePool>,
    Json(payload): Json<ItemPayload>,
) -> Result<(StatusCode, Json<Item>), ErreurApp> {
    valider(&payload)?;
    let id = sqlx::query!(
        "INSERT INTO items (nom, valeur) VALUES (?, ?) RETURNING id",
        payload.nom,
        payload.valeur,
    )
    .fetch_one(&pool)
    .await?
    .id;

    let item = sqlx::query_as!(Item, "SELECT id, nom, valeur FROM items WHERE id = ?", id)
        .fetch_one(&pool)
        .await?;
    Ok((StatusCode::CREATED, Json(item)))
}

// PUT /items/:id
pub async fn modifier(
    Path(id): Path<i64>,
    State(pool): State<SqlitePool>,
    Json(payload): Json<ItemPayload>,
) -> Result<Json<Item>, ErreurApp> {
    valider(&payload)?;
    let nb = sqlx::query!(
        "UPDATE items SET nom = ?, valeur = ? WHERE id = ?",
        payload.nom, payload.valeur, id
    )
    .execute(&pool)
    .await?
    .rows_affected();

    if nb == 0 {
        return Err(ErreurApp::NonTrouve(id));
    }
    obtenir(Path(id), State(pool)).await
}

// DELETE /items/:id
pub async fn supprimer(
    Path(id): Path<i64>,
    State(pool): State<SqlitePool>,
) -> Result<StatusCode, ErreurApp> {
    let nb = sqlx::query!("DELETE FROM items WHERE id = ?", id)
        .execute(&pool)
        .await?
        .rows_affected();

    if nb == 0 {
        return Err(ErreurApp::NonTrouve(id));
    }
    Ok(StatusCode::NO_CONTENT)
}

src/main.rs

mod erreur;
mod item;

use axum::routing::{delete, get, post, put};
use axum::Router;
use sqlx::sqlite::SqlitePoolOptions;
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialisation des traces
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .init();

    // Connexion SQLite avec pool
    let pool = SqlitePoolOptions::new()
        .max_connections(5)
        .connect("sqlite://items.db?mode=rwc")
        .await?;

    // Migration automatique
    sqlx::migrate!("./migrations").run(&pool).await?;

    let app = Router::new()
        .route("/items", get(item::lister).post(item::creer))
        .route("/items/:id", get(item::obtenir).put(item::modifier).delete(item::supprimer))
        .with_state(pool);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    tracing::info!("Serveur demarre sur http://0.0.0.0:3000");
    axum::serve(listener, app).await?;
    Ok(())
}

Projet 2 — Outil système : parseur de logs en JSON

Ce projet analyse des fichiers de logs (format Apache/Nginx) avec des expressions régulières, produit une sortie JSON structurée, et peut être compile en WebAssembly pour une utilisation dans le navigateur.

Cargo.toml

[package]
name = "log-parser"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib", "rlib"]  # cdylib requis pour WASM

[dependencies]
regex = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"

# Dependances WASM (activees uniquement en cible wasm32)
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2"

src/lib.rs — logique principale

use regex::Regex;
use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
use thiserror::Error;

// OnceLock compile la regex une seule fois au premier appel
static REGEX_ACCESS: OnceLock<Regex> = OnceLock::new();

fn regex_access() -> &'static Regex {
    REGEX_ACCESS.get_or_init(|| {
        // Format Combined Log Format (Apache/Nginx)
        Regex::new(
            r#"^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) \S+" (\d{3}) (\d+|-)"#
        ).expect("Regex invalide — erreur statique")
    })
}

#[derive(Error, Debug)]
pub enum ErreurParse {
    #[error("Ligne non reconnue : {0}")]
    FormatInconnu(String),
}

/// Entree de log parsee et structuree
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct EntreeLog {
    pub ip: String,
    pub date: String,
    pub methode: String,
    pub chemin: String,
    pub statut: u16,
    pub taille: Option<u64>,
}

/// Tente de parser une ligne de log au format Combined Log Format
pub fn parser_ligne(ligne: &str) -> Result<EntreeLog, ErreurParse> {
    let caps = regex_access()
        .captures(ligne)
        .ok_or_else(|| ErreurParse::FormatInconnu(ligne.to_owned()))?;

    let taille = caps[6].parse::<u64>().ok(); // "-" devient None

    Ok(EntreeLog {
        ip:      caps[1].to_owned(),
        date:    caps[2].to_owned(),
        methode: caps[3].to_owned(),
        chemin:  caps[4].to_owned(),
        statut:  caps[5].parse().unwrap_or(0),
        taille,
    })
}

/// Parse un fichier entier et retourne les entrees valides avec le compte d'erreurs
pub fn parser_fichier(contenu: &str) -> (Vec<EntreeLog>, usize) {
    let mut entrees = Vec::new();
    let mut nb_erreurs = 0;

    for ligne in contenu.lines() {
        let ligne = ligne.trim();
        if ligne.is_empty() { continue; }

        match parser_ligne(ligne) {
            Ok(entree) => entrees.push(entree),
            Err(_) => nb_erreurs += 1,
        }
    }
    (entrees, nb_erreurs)
}

/// Point d'entree WASM — expose au JavaScript via wasm-bindgen
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn analyser_logs(contenu: &str) -> String {
    let (entrees, nb_erreurs) = parser_fichier(contenu);
    serde_json::json!({
        "entrees": entrees,
        "nb_lignes": entrees.len() + nb_erreurs,
        "nb_erreurs": nb_erreurs,
    })
    .to_string()
}

src/main.rs — point d'entree CLI

use std::{env, fs, process};
use log_parser::parser_fichier;

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 2 {
        eprintln!("Usage : log-parser <fichier.log>");
        process::exit(1);
    }

    let contenu = fs::read_to_string(&args[1]).unwrap_or_else(|e| {
        eprintln!("Impossible de lire {} : {}", args[1], e);
        process::exit(1);
    });

    let (entrees, nb_erreurs) = parser_fichier(&contenu);

    let sortie = serde_json::json!({
        "entrees": entrees,
        "nb_lignes": entrees.len() + nb_erreurs,
        "nb_erreurs": nb_erreurs,
    });

    // Serialisation JSON compacte vers stdout
    println!("{}", serde_json::to_string_pretty(&sortie).unwrap());
}

Compilation WebAssembly

# Installation de wasm-pack
cargo install wasm-pack

# Compilation pour le navigateur (ES module)
wasm-pack build --target web --release

# Compilation pour Node.js
wasm-pack build --target nodejs --release

# Artefacts generes dans pkg/
# log_parser.js      — bindings JavaScript
# log_parser_bg.wasm — module WASM
# log_parser.d.ts    — types TypeScript
// Utilisation dans le navigateur (ES module)
import init, { analyser_logs } from './pkg/log_parser.js';

await init();

const contenu = `127.0.0.1 - - [10/Apr/2024:12:00:00 +0000] "GET /index.html HTTP/1.1" 200 1234`;
const resultat = JSON.parse(analyser_logs(contenu));
console.log(resultat);

Performance WASM vs natif

Le module WASM produit par wasm-pack tourne a environ 80-90% de la vitesse native pour les tâches CPU-bound comme le parsing de regex. Pour les grandes quantites de logs (> 100 Mo), la version CLI native reste préférée. Le WASM est idéal pour les previews en ligne ou les outils embarqués dans un éditeur web.