style(i18n): translate all code and comments to English

Rename French identifiers to English across all source files:
  - validate_cidr / validate_ip_in_network (was valider_*)
  - known_protocol (was protocole_connu)
  - counter / doubled (was compteur / double)
  - InvalidCidr / InvalidIp / IpOutsideNetwork (was *Invalide / *HorsReseau)
  - test names and error messages

All comments, doc strings, .expect() messages, and tracing logs converted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 19:56:47 +02:00
parent 4c11a8608b
commit 18804e740c
10 changed files with 346 additions and 354 deletions

View File

@@ -1,8 +1,8 @@
// app.rs — Composants racine de l'application Leptos // app.rs — Root Leptos components
// //
// Ce fichier contient deux composants : // This file contains two components:
// - `Shell` : le document HTML complet (head + body) — SSR uniquement // - `Shell` : full HTML document (head + body) — SSR only
// - `App` : le contenu de la page avec le routeur — partagé SSR + WASM // - `App` : page content and router — shared between SSR and WASM
use leptos::prelude::*; use leptos::prelude::*;
use leptos_meta::*; use leptos_meta::*;
@@ -13,89 +13,88 @@ use leptos_router::{
use crate::client::home::HomePage; use crate::client::home::HomePage;
// Shell — document HTML complet rendu par le serveur Axum // Shell — full HTML document rendered by the Axum server.
// //
// Ce composant n'existe qu'en mode SSR (`#[cfg(feature = "ssr")]`). // This component only exists in SSR mode (`#[cfg(feature = "ssr")]`).
// Il fournit la structure HTML de base que leptos_meta ne peut pas créer seul : // It provides the HTML skeleton that leptos_meta requires:
// un <head> et un <body> valides. Sans ça, les composants <Title>, <Stylesheet> // a valid <head> and <body>. Without it, <Title> and <Stylesheet>
// de leptos_meta n'ont nulle part où s'injecter. // components have nowhere to inject their output.
// //
// Flux de rendu SSR : // SSR rendering flow:
// 1. Axum appelle Shell() pour chaque requête // 1. Axum calls Shell() for each incoming request
// 2. Shell rend le <head> avec MetaTags (placeholder rempli par App) // 2. Shell renders <head> with MetaTags (a placeholder filled by App)
// 3. Shell rend le <body> contenant App() // 3. Shell renders <body> containing App()
// 4. App() appelle provide_meta_context() et définit les métadonnées // 4. App() calls provide_meta_context() and registers metadata
// 5. Leptos collecte les métadonnées et les injecte dans MetaTags rétroactivement // 5. Leptos retroactively injects that metadata into MetaTags
// 6. HydrationScripts génère les <script> pour charger le bundle WASM // 6. HydrationScripts generates <script> tags to load the WASM bundle
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
#[component] #[component]
pub fn Shell( pub fn Shell(
// LeptosOptions contient la config du projet (chemins, noms de fichiers, ports...) // LeptosOptions holds project configuration (paths, file names, ports...).
// Utilisée par HydrationScripts pour construire les URLs du bundle WASM. // Used by HydrationScripts to build the WASM bundle URLs.
options: leptos::config::LeptosOptions, options: leptos::config::LeptosOptions,
) -> impl IntoView { ) -> impl IntoView {
view! { view! {
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
// MetaTags : placeholder leptos_meta injecte les balises collectées // MetaTags: placeholder where leptos_meta injects tags collected
// depuis les composants <Title>, <Stylesheet>, <Meta>... définis dans App(). // from <Title>, <Stylesheet>, <Meta>... defined inside App().
<MetaTags/> <MetaTags/>
// HydrationScripts : génère les balises <link> et <script> qui chargent // HydrationScripts: generates <link> and <script> tags that load
// le bundle WASM compilé par trunk et appellent la fonction hydrate() de lib.rs. // the trunk-compiled WASM bundle and call hydrate() from lib.rs.
<HydrationScripts options=options.clone()/> <HydrationScripts options=options.clone()/>
// AutoReload : hot-reload en développement (no-op en production). // AutoReload: hot-reload during development (no-op in production).
// S'active uniquement si la variable d'env LEPTOS_WATCH est définie. // Only activates when the LEPTOS_WATCH environment variable is set.
<AutoReload options/> <AutoReload options/>
</head> </head>
<body> <body>
// App() s'exécute ici, fournit le contexte meta et rend le contenu de la page // App() runs here, provides the meta context, and renders page content.
<App/> <App/>
</body> </body>
</html> </html>
} }
} }
// App — composant racine partagé entre le serveur (SSR) et le navigateur (WASM) // App — root component shared between the server (SSR) and the browser (WASM).
// //
// Ce composant est rendu : // This component is rendered:
// - côté serveur : dans le <body> du Shell, pour générer le HTML // - server-side : inside the Shell <body>, to generate HTML
// - côté navigateur : via hydrate() dans lib.rs, pour attacher la réactivité // - client-side : via hydrate() in lib.rs, to attach reactivity
// //
// `-> impl IntoView` : retourne "quelque chose affichable". Le type exact est opaque // `-> impl IntoView` : returns "something displayable". The concrete type is
// car le compilateur Leptos génère un type complexe à partir de la macro `view!`. // opaque because Leptos's `view!` macro generates a complex internal type.
#[component] #[component]
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
// Initialise le système de métadonnées Leptos. // Initialize the Leptos metadata context.
// Sans cet appel, <Title>, <Stylesheet>, <Meta> dans les composants enfants // Without this call, <Title>, <Stylesheet>, and <Meta> in child components
// n'auraient pas de contexte où stocker les métadonnées. // would have no context to store their metadata in.
provide_meta_context(); provide_meta_context();
view! { view! {
// Définit le titre de l'onglet navigateur <Title text="Rust IPAM — IP Address Manager"/>
<Title text="Rust IPAM — Gestionnaire d'adresses IP"/>
// Charge le CSS global depuis /pkg/rust-ipam.css // Load the global CSS from /pkg/rust-ipam.css.
// Ce fichier est généré par trunk à partir de style.css (si ajouté plus tard) // This file is generated by trunk from style.css (to be added later).
<Stylesheet id="main" href="/pkg/rust-ipam.css"/> <Stylesheet id="main" href="/pkg/rust-ipam.css"/>
// Le Router gère la navigation sans rechargement de page. // Router handles client-side navigation without full page reloads.
// Côté serveur, il détermine quel composant rendre selon l'URL. // On the server, it determines which component to render for the requested URL.
<Router> <Router>
<main> <main>
// <Routes> est le conteneur pour toutes les définitions de routes. // <Routes> is the container for all route definitions.
// `fallback` est affiché si aucune route ne correspond. // `fallback` is displayed when no route matches the current URL.
<Routes fallback=|| view! { <Routes fallback=|| view! {
<div class="page-erreur"> <div class="not-found">
<h1>"404 — Page introuvable"</h1> <h1>"404 — Page not found"</h1>
<a href="/">"Retour à l'accueil"</a> <a href="/">"Back to home"</a>
</div> </div>
}> }>
// path!(/) correspond à l'URL racine "/" // path!(/) matches the root URL "/"
// Ajouter de nouvelles pages ici : // Add new pages here, e.g.:
// <Route path=path!("/reseaux") view=ReseauxPage/> // <Route path=path!("/networks") view=NetworksPage/>
<Route path=path!("/") view=HomePage/> <Route path=path!("/") view=HomePage/>
</Routes> </Routes>
</main> </main>

View File

@@ -1,75 +1,75 @@
// client/home.rs — Page d'accueil // client/home.rs — Home page
// //
// Ce fichier illustre les concepts fondamentaux de Leptos : // Demonstrates the core Leptos concepts:
// - Composants (`#[component]`) // - Components (`#[component]`)
// - Signals (valeurs réactives) // - Signals (reactive values)
// - Memos (valeurs dérivées) // - Memos (derived values)
// - Gestion d'événements (`on:click`) // - Event handling (`on:click`)
use leptos::prelude::*; use leptos::prelude::*;
// Composant de la page d'accueil. // Home page component.
// //
// En Leptos, un composant est une simple fonction Rust avec l'attribut `#[component]`. // In Leptos, a component is a plain Rust function annotated with `#[component]`.
// Elle est appelée une seule fois pour construire le graphe réactif // It is called once to build the reactive graph
// ce n'est pas comme React où le composant se "ré-exécute" à chaque mise à jour. // unlike React, it does not re-execute on every update.
#[component] #[component]
pub fn HomePage() -> impl IntoView { pub fn HomePage() -> impl IntoView {
// `RwSignal<T>` (Read-Write Signal) est une valeur réactive mutable. // `RwSignal<T>` (Read-Write Signal) is a mutable reactive value.
// //
// Quand on appelle `.set()` ou `.update()`, Leptos identifie automatiquement // When `.set()` or `.update()` is called, Leptos automatically identifies
// tous les éléments du DOM qui dépendent de ce signal et les met à jour // all DOM nodes that depend on this signal and updates only those
// sans Virtual DOM, sans diff complet : seulement ce qui change. // no Virtual DOM, no full diff: surgical updates only.
// //
// `i32` = entier signé 32 bits (le type entier par défaut en Rust) // `i32` = signed 32-bit integer (Rust's default integer type)
let compteur = RwSignal::new(0i32); let counter = RwSignal::new(0i32);
// `Memo<T>` est une valeur calculée à partir d'un ou plusieurs signals. // `Memo<T>` is a value derived from one or more signals.
// Elle se recalcule automatiquement quand `compteur` change, mais // It recomputes automatically when `counter` changes, but only
// ne notifie ses dépendants que si sa valeur a effectivement changé. // notifies its dependents when the result actually differs.
// //
// `move |_|` : une closure qui capture `compteur` par déplacement (ownership). // `move |_|`: a closure that captures `counter` by move (takes ownership).
// - `move` : la closure prend possession de `compteur` // - `move` : the closure owns `counter`
// - `|_|` : elle ignore son argument (la valeur précédente du memo) // - `|_|` : ignores the argument (the previous memo value)
let double = Memo::new(move |_| compteur.get() * 2); let doubled = Memo::new(move |_| counter.get() * 2);
view! { view! {
<div class="page-accueil"> <div class="home-page">
<h1>"Rust IPAM"</h1> <h1>"Rust IPAM"</h1>
<p class="sous-titre">"Gestionnaire d'adresses IP"</p> <p class="subtitle">"IP Address Manager"</p>
// --- Démonstration de la réactivité --- // --- Reactivity demo ---
// Dans un vrai projet IPAM, on afficherait ici la liste des sous-réseaux, // In the real IPAM app, this section will show the network list,
// les adresses disponibles, les statistiques d'utilisation... // available addresses, usage statistics, etc.
<section class="demo-reactive"> <section class="reactivity-demo">
<h2>"Réactivité Leptos"</h2> <h2>"Leptos Reactivity Demo"</h2>
// `{compteur}` insère la valeur du signal directement dans le DOM. // `{counter}` inserts the signal value directly into the DOM.
// Leptos met à jour UNIQUEMENT ce nœud texte quand compteur change. // Leptos updates ONLY this text node when counter changes —
// Pas de re-render du composant entier : c'est granulaire et efficace. // the entire component does not re-render.
<p>"Compteur : " {compteur}</p> <p>"Counter: " {counter}</p>
// `{double}` : idem, mais pour le memo (valeur dérivée) // `{doubled}`: same, but for the memo (derived value)
<p>"Double : " {double}</p> <p>"Doubled: " {doubled}</p>
<div class="boutons"> <div class="buttons">
// `on:click` attache un event listener au bouton. // `on:click` attaches a JavaScript event listener.
// //
// `.update(|n| *n += 1)`: // `.update(|n| *n += 1)`:
// - prend une closure qui reçoit une référence mutable `&mut i32` // - takes a closure receiving a `&mut i32`
// - `*n` déréférence le pointeur pour modifier la valeur pointée // - `*n` dereferences the pointer to modify the value
// - `+= 1` incrémente la valeur en place // - `+= 1` increments in place
<button on:click=move |_| compteur.update(|n| *n += 1)> <button on:click=move |_| counter.update(|n| *n += 1)>
"+" "+"
</button> </button>
<button on:click=move |_| compteur.update(|n| *n -= 1)> <button on:click=move |_| counter.update(|n| *n -= 1)>
"-" "-"
</button> </button>
// `.set(0)` remplace directement la valeur (plus simple qu'update ici) // `.set(0)` replaces the value directly (simpler than update here)
<button on:click=move |_| compteur.set(0)> <button on:click=move |_| counter.set(0)>
"Réinitialiser" "Reset"
</button> </button>
</div> </div>
</section> </section>

View File

@@ -1,12 +1,12 @@
// client/mod.rs — Module client (composants UI) // client/mod.rs — Client UI module
// //
// Contient les pages et composants Leptos de l'application. // Contains Leptos pages and components.
// //
// Important : malgré le nom "client", ce code s'exécute des DEUX côtés : // Despite the name "client", this code runs on BOTH sides:
// - Côté serveur : pour générer le HTML initial (SSR) // - Server-side : to generate the initial HTML (SSR)
// - Côté navigateur : compilé en WASM pour rendre l'interface interactive // - Browser : compiled to WASM to make the interface interactive
// //
// Ne pas mettre ici de code qui nécessite des APIs navigateur (window, document...) // Do not place code here that requires browser-only APIs (window, document...)
// sans le protéger avec `#[cfg(target_arch = "wasm32")]`. // without guarding it with `#[cfg(target_arch = "wasm32")]`.
pub mod home; // Page d'accueil pub mod home; // Home page

View File

@@ -1,41 +1,41 @@
// lib.rs — Racine de la bibliothèque partagée // lib.rs — Shared library root
// //
// Ce fichier est compilé dans les DEUX modes : // This file is compiled in BOTH modes:
// "ssr" → le serveur Axum l'utilise pour rendre du HTML // "ssr" → used by the Axum server to render HTML
// "hydrate" → trunk le compile en WebAssembly pour le navigateur // "hydrate" → compiled by trunk into WebAssembly for the browser
// //
// C'est ce partage de code qui rend Leptos "full-stack" : // This code sharing is what makes Leptos "full-stack":
// on écrit les composants une fois, ils s'exécutent des deux côtés. // components are written once and run on both sides.
// Déclaration des sous-modules de cette bibliothèque. // Declare the sub-modules of this library.
// `pub` les rend accessibles depuis main.rs et d'autres crates. // `pub` makes them accessible from main.rs and other crates.
pub mod app; // Composant racine App() et configuration du routeur pub mod app; // Root App() component and router configuration
pub mod client; // Pages et composants de l'interface utilisateur pub mod client; // UI pages and Leptos components
pub mod models; // Structs de données partagés server + client (Network, Host, Port, Application) pub mod models; // Shared data structs: Network, Host, Port, Application
pub mod server; // Handlers HTTP et logique métier côté serveur pub mod server; // HTTP handlers and server-side business logic
// Point d'entrée WebAssembly — exécuté par le navigateur au chargement du bundle .wasm // WebAssembly entry point — called by the browser when the .wasm bundle loads.
// //
// `#[cfg(feature = "hydrate")]` : ce code n'existe que dans le bundle WASM. // `#[cfg(feature = "hydrate")]` : this code only exists in the WASM bundle.
// `#[wasm_bindgen(start)]` : demande à wasm-bindgen d'appeler cette fonction // `#[wasm_bindgen(start)]` : instructs wasm-bindgen to call this function
// automatiquement sans intervention JavaScript. // automatically, without any JavaScript glue.
#[cfg(feature = "hydrate")] #[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen(start)] #[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn hydrate() { pub fn hydrate() {
use crate::app::App; use crate::app::App;
// Active les messages d'erreur Rust dans la console du navigateur. // Enable Rust panic messages in the browser console.
// Sans ça, un panic Rust en WASM affiche juste "unreachable executed" — inutile. // Without this, a Rust panic in WASM only shows "unreachable executed" — useless.
// `set_once()` garantit qu'on ne l'initialise pas plusieurs fois si hydrate() est appelé plusieurs fois. // `set_once()` ensures the hook is registered at most once.
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
// Monte l'application Leptos dans le <body> de la page HTML. // Mount the Leptos application into the <body> of the HTML page.
// //
// En mode "hydration" (SSR + WASM), Leptos ne recrée pas le DOM depuis zéro. // In "hydration" mode (SSR + WASM), Leptos does not rebuild the DOM from scratch.
// Il trouve le HTML déjà rendu par le serveur et y attache les event listeners // It finds the HTML already rendered by the server and attaches event listeners
// pour rendre l'interface interactive. C'est plus rapide qu'un SPA classique // to make the interface interactive. This is faster than a classic SPA
// qui construit tout le DOM côté client. // that builds the entire DOM on the client side.
// //
// `hydrate_body` (Leptos 0.7) = mode SSR + hydration (≠ `mount_to_body` qui repart de zéro) // `hydrate_body` (Leptos 0.7) = SSR + hydration mode (≠ `mount_to_body` which starts fresh)
leptos::mount::hydrate_body(App); leptos::mount::hydrate_body(App);
} }

View File

@@ -1,92 +1,91 @@
// main.rs — Point d'entrée du serveur Axum // main.rs — Axum server entry point
// //
// Ce fichier est compilé UNIQUEMENT en mode "ssr" (Server-Side Rendering). // This file is compiled ONLY when the "ssr" feature is enabled.
// `#[cfg(feature = "ssr")]` est l'équivalent d'un `#ifdef` en C : // `#[cfg(feature = "ssr")]` works like `#ifdef` in C:
// le code qu'il protège n'existe pas dans le bundle WASM. // the guarded code does not exist in the WASM bundle.
// //
// Pour lancer le serveur : // Run the server:
// cargo run --features ssr // cargo run --features ssr
// //
// Pour lancer avec des logs détaillés : // Run with verbose logs:
// RUST_LOG=debug cargo run --features ssr // RUST_LOG=debug cargo run --features ssr
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
#[tokio::main] #[tokio::main]
// `#[tokio::main]` transforme `fn main()` synchrone en une fonction asynchrone, // `#[tokio::main]` turns the synchronous `fn main()` into an async function
// gérée par le runtime Tokio. Sans ça, Rust ne sait pas exécuter du code `async`. // managed by the Tokio runtime. Without it, Rust cannot execute `async` code.
async fn main() { async fn main() {
use axum::Router; use axum::Router;
use leptos::config::get_configuration; use leptos::config::get_configuration;
use leptos_axum::{generate_route_list, LeptosRoutes};
use leptos::view; use leptos::view;
use leptos_axum::{generate_route_list, LeptosRoutes};
use rust_ipam::{ use rust_ipam::{
app::{App, Shell}, app::{App, Shell},
server::{config::AppConfig, routes::not_found_handler}, server::{config::AppConfig, routes::not_found_handler},
}; };
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
// Initialise les logs structurés. // Initialize structured logging.
// tracing::info!(), tracing::warn!(), etc. n'affichent rien sans cet initialisateur. // tracing::info!(), tracing::warn!(), etc. produce no output without this.
// RUST_LOG=debug cargo run --features ssr → active les logs debug // RUST_LOG=debug cargo run --features ssr → enables debug-level logs
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter( .with_env_filter(
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
) )
.init(); .init();
tracing::info!("Démarrage du serveur Rust IPAM..."); tracing::info!("Starting Rust IPAM server...");
// Charge la configuration depuis .env / variables d'environnement. // Load configuration from environment variables / .env file.
// On arrête le serveur immédiatement si la config est invalide — il ne peut // The server cannot start without knowing which database to connect to,
// pas fonctionner sans savoir à quelle base de données se connecter. // so we abort immediately on any configuration error.
let app_config = AppConfig::from_env() let app_config = AppConfig::from_env()
.expect("Erreur de configuration — vérifier le fichier .env"); .expect("Configuration error — check your .env file");
tracing::info!("Base de données : {} ({})", app_config.backend, app_config.database_url); tracing::info!("Database: {} ({})", app_config.backend, app_config.database_url);
// `Some("Cargo.toml")` indique à Leptos de lire la section // `Some("Cargo.toml")` tells Leptos to read the [package.metadata.leptos]
// [package.metadata.leptos] du Cargo.toml pour la configuration // section from Cargo.toml (file paths, output names, server address...).
// (noms de fichiers, chemins, adresse serveur...).
let conf = get_configuration(Some("Cargo.toml")) let conf = get_configuration(Some("Cargo.toml"))
.expect("Impossible de charger la configuration Leptos depuis Cargo.toml"); .expect("Failed to load Leptos configuration from Cargo.toml");
let leptos_options = conf.leptos_options; let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr; let addr = leptos_options.site_addr;
// Analyse les composants `<Route>` dans `App` pour construire // Walk all `<Route>` components inside `App` to build the list of URLs
// la liste des URLs que Leptos SSR doit gérer. // that Leptos SSR must handle.
let routes = generate_route_list(App); let routes = generate_route_list(App);
// Construit le routeur Axum avec le pattern builder (chaînage de méthodes). // Build the Axum router using the builder pattern (method chaining).
let app = Router::new() let app = Router::new()
// Sert les fichiers statiques compilés par trunk (WASM, JS...). // Serve static files compiled by trunk (WASM, JS...).
// Trunk les place dans target/site/pkg/ (configuré dans [package.metadata.leptos]). // Trunk places them in target/site/pkg/ as configured in [package.metadata.leptos].
.nest_service("/pkg", ServeDir::new("target/site/pkg")) .nest_service("/pkg", ServeDir::new("target/site/pkg"))
// Branche les routes Leptos dans Axum. // Mount all Leptos routes into Axum.
// Pour chaque URL, Axum rend Shell() en HTML et le renvoie au navigateur. // For each URL, Axum renders Shell() to HTML and sends it to the browser.
// Shell() contient App() qui fournit le contenu de la page. // Shell() contains App(), which provides the page content.
.leptos_routes(&leptos_options, routes, { .leptos_routes(&leptos_options, routes, {
// On clone les options pour les capturer dans la closure. // Clone options so the closure can capture them.
// Le `move` transfère la propriété de `leptos_options` dans la closure. // `move` transfers ownership of `leptos_options` into the closure.
let leptos_options = leptos_options.clone(); let leptos_options = leptos_options.clone();
move || view! { <Shell options=leptos_options.clone()/> } move || view! { <Shell options=leptos_options.clone()/> }
}) })
.fallback(not_found_handler) .fallback(not_found_handler)
// Partage les options Leptos avec tous les handlers via le système d'état Axum. // Share Leptos options with all handlers via Axum's state system.
.with_state(leptos_options); .with_state(leptos_options);
let listener = tokio::net::TcpListener::bind(&addr) let listener = tokio::net::TcpListener::bind(&addr)
.await .await
.expect(&format!("Impossible d'écouter sur l'adresse {}", addr)); .expect(&format!("Failed to bind to address {}", addr));
tracing::info!("Serveur disponible sur http://{}", addr); tracing::info!("Server listening on http://{}", addr);
axum::serve(listener, app) axum::serve(listener, app)
.await .await
.expect("Erreur critique du serveur"); .expect("Fatal server error");
} }
// Ce bloc vide est obligatoire pour que le compilateur trouve un `fn main()` // This empty block is required so the compiler finds a `fn main()`
// en mode WASM (où la feature "ssr" n'est pas activée). // when building in WASM mode (where the "ssr" feature is not enabled).
// En WASM, le vrai point d'entrée est la fonction `hydrate()` dans lib.rs. // In WASM, the real entry point is `hydrate()` in lib.rs.
#[cfg(not(feature = "ssr"))] #[cfg(not(feature = "ssr"))]
fn main() {} fn main() {}

View File

@@ -1,93 +1,92 @@
// models.rs — Modèles de données partagés (server + client) // models.rs — Shared data models (server + client)
// //
// Ce module définit les structs qui représentent les entités métier du projet IPAM. // This module defines the structs that represent the IPAM domain entities.
// Ils sont compilés côté serveur ET côté WASM car Leptos en a besoin des deux côtés : // They are compiled for both the server and WASM, because Leptos needs them
// - Serveur : pour lire/écrire en BDD et rendre le HTML // on both sides:
// - Client : pour afficher les données dans les composants Leptos // - Server : to read/write the database and render HTML
// - Client : to display data inside Leptos components
// //
// Chaque struct dérive `Serialize` et `Deserialize` de serde. // Each struct derives `Serialize` and `Deserialize` from serde.
// C'est obligatoire pour que Leptos puisse transférer les données entre // This is required for Leptos to transfer data between the server and the
// le serveur et le navigateur via les "server functions" (#[server]). // browser through server functions (#[server]).
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// ─── Réseau ─────────────────────────────────────────────────────────────────── // ─── Network ──────────────────────────────────────────────────────────────────
/// Un réseau IP défini par sa plage CIDR. /// An IP network defined by its CIDR range.
/// ///
/// Exemple : { id: 1, cidr: "192.168.1.0/24" } /// Example: { id: 1, cidr: "192.168.1.0/24" }
/// → plage de 192.168.1.0 à 192.168.1.255 (254 hôtes utilisables) /// → covers 192.168.1.0 to 192.168.1.255 (254 usable hosts)
/// ///
/// La notation CIDR (Classless Inter-Domain Routing) combine l'adresse réseau /// CIDR (Classless Inter-Domain Routing) combines the network address and
/// et le masque en un seul champ : <adresse>/<longueur du préfixe>. /// the subnet mask into a single field: <address>/<prefix length>.
/// /24 = 24 bits de masque = 255.255.255.0 /// /24 = 24-bit mask = 255.255.255.0
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Network { pub struct Network {
/// Identifiant unique auto-incrémenté par la base de données. /// Unique identifier, auto-incremented by the database.
/// `i64` est le type entier signé 64 bits — correspond à `BIGINT` en SQL. /// `i64` is a signed 64-bit integer — maps to `BIGINT` in SQL.
pub id: i64, pub id: i64,
/// Plage d'adresses en notation CIDR. /// Address range in CIDR notation.
/// Ex: "10.0.0.0/8", "172.16.0.0/12", "192.168.1.0/24" /// Examples: "10.0.0.0/8", "172.16.0.0/12", "192.168.1.0/24"
pub cidr: String, pub cidr: String,
} }
// ─── Hôte ───────────────────────────────────────────────────────────────────── // ─── Host ─────────────────────────────────────────────────────────────────────
/// Un hôte (machine, serveur, équipement réseau) appartenant à un réseau. /// A host (server, workstation, network device) belonging to a network.
/// ///
/// Contrainte : l'IP doit appartenir à la plage CIDR du réseau référencé /// Constraint: the IP address must fall within the CIDR range of the network
/// par `network_id`. Cette contrainte est vérifiée à la création/modification. /// referenced by `network_id`. This is enforced on creation and update.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Host { pub struct Host {
pub id: i64, pub id: i64,
/// Nom descriptif de l'hôte. Ex: "serveur-web-01", "routeur-principal" /// Human-readable name. Examples: "web-server-01", "main-router"
pub name: String, pub name: String,
/// Adresse IPv4 de l'hôte, stockée en texte. /// IPv4 address stored as text. Example: "192.168.1.10"
/// Ex: "192.168.1.10" /// We use String instead of IpAddr to simplify serialization
/// On utilise String plutôt que IpAddr pour simplifier la sérialisation /// and database storage.
/// et le stockage en base de données.
pub ip: String, pub ip: String,
/// Référence vers le réseau auquel appartient cet hôte. /// Foreign key referencing the network this host belongs to.
/// C'est une "clé étrangère" (Foreign Key) vers la table `networks`.
pub network_id: i64, pub network_id: i64,
} }
// ─── Port ───────────────────────────────────────────────────────────────────── // ─── Port ─────────────────────────────────────────────────────────────────────
/// Un port réseau ouvert sur un hôte, avec sa description probable. /// A network port open on a host, with its likely protocol description.
/// ///
/// Les numéros de ports standards (01023) sont des "well-known ports". /// Well-known ports (01023) have standardized protocol assignments.
/// Un port peut être associé à plusieurs applications (association non-stricte). /// A port can be associated with multiple applications (non-strict relation).
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Port { pub struct Port {
/// Numéro de port TCP/UDP. /// TCP/UDP port number.
/// `u16` : entier non signé 16 bits → plage 0 à 65535. /// `u16` is an unsigned 16-bit integer → range 0 to 65535,
/// C'est exactement la plage valide pour les ports réseau. /// which exactly matches the valid range for network ports.
pub number: u16, pub number: u16,
/// Description du protocole probable sur ce port. /// Description of the likely protocol on this port.
/// `Option<String>` : peut être absent (None) si le protocole est inconnu. /// `Option<String>`: may be absent (None) when the protocol is unknown.
/// Ex: Some("SSH"), Some("HTTPS"), None /// Examples: Some("SSH"), Some("HTTPS"), None
pub description: Option<String>, pub description: Option<String>,
/// Hôte sur lequel ce port est ouvert. /// The host on which this port is open.
pub host_id: i64, pub host_id: i64,
} }
impl Port { impl Port {
/// Retourne la description standard pour les ports les plus courants. /// Returns the standard description for common well-known ports.
/// Utilisé pour pré-remplir la description lors de l'ajout d'un port. /// Used to pre-fill the description field when adding a port.
/// ///
/// `match` est l'équivalent Rust d'un switch/case, mais exhaustif : /// `match` is Rust's exhaustive pattern-matching construct (like switch/case,
/// le compilateur oblige à gérer tous les cas possibles. /// but the compiler enforces that all cases are handled).
pub fn protocole_connu(numero: u16) -> Option<&'static str> { pub fn known_protocol(number: u16) -> Option<&'static str> {
// `&'static str` : référence vers une chaîne qui vit toute la durée du programme // `&'static str`: a reference to a string that lives for the entire
// (les littéraux de chaînes comme "SSH" sont stockés dans le binaire compilé) // program lifetime (string literals are stored in the compiled binary).
match numero { match number {
21 => Some("FTP"), 21 => Some("FTP"),
22 => Some("SSH"), 22 => Some("SSH"),
23 => Some("Telnet"), 23 => Some("Telnet"),
@@ -100,33 +99,33 @@ impl Port {
3306 => Some("MySQL"), 3306 => Some("MySQL"),
5432 => Some("PostgreSQL"), 5432 => Some("PostgreSQL"),
6379 => Some("Redis"), 6379 => Some("Redis"),
8080 => Some("HTTP alternatif"), 8080 => Some("HTTP (alternate)"),
_ => None, // `_` est le pattern "tout le reste" (wildcard) _ => None, // `_` is the wildcard pattern — matches everything else
} }
} }
} }
// ─── Application ────────────────────────────────────────────────────────────── // ─── Application ──────────────────────────────────────────────────────────────
/// Une application qui utilise un ou plusieurs ports. /// An application that uses one or more ports.
/// ///
/// L'association entre application et port est non-stricte : /// The association between an application and a port is non-strict:
/// un même port peut être partagé par plusieurs applications. /// the same port can be shared by multiple applications.
/// Ex: le port 80 peut être utilisé par Nginx ET par un proxy applicatif. /// Example: port 80 might be used by both Nginx and an application proxy.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Application { pub struct Application {
pub id: i64, pub id: i64,
/// Nom de l'application. Ex: "Nginx", "PostgreSQL", "Prometheus" /// Application name. Examples: "Nginx", "PostgreSQL", "Prometheus"
pub name: String, pub name: String,
} }
// ─── Association ApplicationPort ────────────────────────────────────────── // ─── ApplicationPort ──────────────────────────────────────────────────────────
/// Lien entre une application et un port (relation many-to-many). /// Join record linking an application to a port (many-to-many relationship).
/// ///
/// On utilise un struct dédié plutôt qu'un Vec<Port> dans Application /// A dedicated struct is used instead of Vec<Port> inside Application
/// pour correspondre directement à la table de jointure en base de données. /// so it maps directly to the join table in the database.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplicationPort { pub struct ApplicationPort {
pub application_id: i64, pub application_id: i64,

View File

@@ -1,44 +1,44 @@
// server/config.rs — Configuration de l'application // server/config.rs — Application configuration
// //
// Ce module charge et valide la configuration au démarrage du serveur. // This module loads and validates configuration at server startup.
// Toute la configuration passe par des variables d'environnement, // All configuration is read from environment variables,
// elles-mêmes chargées depuis un fichier `.env` via dotenvy. // which can be populated from a `.env` file via dotenvy.
// //
// Principe : une seule variable suffit pour choisir la base de données. // A single variable is enough to select the database backend:
// DATABASE_URL=sqlite://data/ipam.db → SQLite (dev, pas de serveur nécessaire) // DATABASE_URL=sqlite://data/ipam.db → SQLite (dev, no server needed)
// DATABASE_URL=postgresql://user:pw@host/db → PostgreSQL (production) // DATABASE_URL=postgresql://user:pw@host/db → PostgreSQL (production)
use thiserror::Error; use thiserror::Error;
// ─── Erreurs de configuration ───────────────────────────────────────────────── // ─── Configuration errors ─────────────────────────────────────────────────────
// `#[derive(Error)]` de la crate thiserror génère automatiquement l'impl // `#[derive(Error)]` from thiserror automatically generates the impl for the
// du trait standard `std::error::Error` — pas besoin de l'écrire à la main. // standard `std::error::Error` trait — no need to write it by hand.
// //
// `#[error("...")]` définit le message affiché par Display / println!("{}", err). // `#[error("...")]` defines the message shown by Display / println!("{}", err).
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ConfigError { pub enum ConfigError {
// `#[from]` permet de convertir automatiquement un VarError en ConfigError // `#[from]` auto-converts a VarError into ConfigError via the `?` operator.
// via l'opérateur `?`. Ex: std::env::var("X")? dans une fn -> Result<_, ConfigError> // Example: std::env::var("X")? in a fn -> Result<_, ConfigError>
#[error("Variable d'environnement manquante : {0}")] #[error("Missing environment variable: {0}")]
MissingVar(#[from] std::env::VarError), MissingVar(#[from] std::env::VarError),
#[error("URL de base de données non reconnue : '{0}' — doit commencer par sqlite:// ou postgresql://")] #[error("Unknown database URL '{0}' — must start with sqlite:// or postgresql://")]
UnknownBackend(String), UnknownBackend(String),
} }
// ─── Backend de base de données ─────────────────────────────────────────────── // ─── Database backend ─────────────────────────────────────────────────────────
// `#[derive(Debug, Clone)]` génère automatiquement : // `#[derive(Debug, Clone)]` automatically generates:
// - Debug : permet d'afficher la valeur avec {:?} dans les logs // - Debug : allows printing the value with {:?} in logs
// - Clone : permet de copier la valeur (nécessaire pour l'état Axum) // - Clone : allows copying the value (required for Axum state)
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum DatabaseBackend { pub enum DatabaseBackend {
Postgres, Postgres,
Sqlite, Sqlite,
} }
// Impl Display pour pouvoir écrire tracing::info!("Backend : {}", backend) // Display impl allows writing: tracing::info!("Backend: {}", backend)
impl std::fmt::Display for DatabaseBackend { impl std::fmt::Display for DatabaseBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@@ -48,35 +48,35 @@ impl std::fmt::Display for DatabaseBackend {
} }
} }
// ─── Configuration principale ───────────────────────────────────────────────── // ─── Application configuration ────────────────────────────────────────────────
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AppConfig { pub struct AppConfig {
/// URL complète de connexion à la base de données. /// Full database connection URL.
/// Ex: "sqlite://data/ipam.db" ou "postgresql://user:pw@localhost/ipam" /// Examples: "sqlite://data/ipam.db" or "postgresql://user:pw@localhost/ipam"
pub database_url: String, pub database_url: String,
/// Backend détecté automatiquement depuis le préfixe de DATABASE_URL. /// Backend detected automatically from the DATABASE_URL prefix.
pub backend: DatabaseBackend, pub backend: DatabaseBackend,
} }
impl AppConfig { impl AppConfig {
/// Charge la configuration depuis les variables d'environnement. /// Loads configuration from environment variables.
/// ///
/// Ordre de priorité pour les variables : /// Variable priority order:
/// 1. Variables déjà définies dans le shell (ex: export DATABASE_URL=...) /// 1. Variables already set in the shell (e.g. export DATABASE_URL=...)
/// 2. Fichier `.env` à la racine du projet /// 2. `.env` file at the project root
/// ///
/// Retourne une `ConfigError` si DATABASE_URL est absente ou invalide. /// Returns `ConfigError` if DATABASE_URL is missing or unrecognized.
pub fn from_env() -> Result<Self, ConfigError> { pub fn from_env() -> Result<Self, ConfigError> {
// Charge le fichier .env s'il existe. // Load the .env file if it exists.
// `let _ =` ignore silencieusement l'erreur si .env est absent — // `let _ =` silently ignores the error when .env is absent —
// c'est voulu : en production les variables sont injectées directement. // this is intentional: in production, variables are injected directly.
let _ = dotenvy::dotenv(); let _ = dotenvy::dotenv();
// `std::env::var` retourne Result<String, VarError>. // `std::env::var` returns Result<String, VarError>.
// Le `?` propage l'erreur VarError, convertie en ConfigError::MissingVar // The `?` propagates VarError, converted to ConfigError::MissingVar
// grâce au `#[from]` défini plus haut. // thanks to the `#[from]` attribute above.
let database_url = std::env::var("DATABASE_URL")?; let database_url = std::env::var("DATABASE_URL")?;
let backend = Self::detect_backend(&database_url)?; let backend = Self::detect_backend(&database_url)?;
@@ -84,17 +84,16 @@ impl AppConfig {
Ok(Self { database_url, backend }) Ok(Self { database_url, backend })
} }
/// Déduit le backend depuis le préfixe de l'URL. /// Infers the database backend from the URL prefix.
/// ///
/// `&str` : référence vers une chaîne — pas de copie, juste un emprunt. /// `&str`: a borrowed string reference — no copy, just a borrow.
fn detect_backend(url: &str) -> Result<DatabaseBackend, ConfigError> { fn detect_backend(url: &str) -> Result<DatabaseBackend, ConfigError> {
// `starts_with` est une méthode de &str qui vérifie le préfixe
if url.starts_with("postgresql://") || url.starts_with("postgres://") { if url.starts_with("postgresql://") || url.starts_with("postgres://") {
Ok(DatabaseBackend::Postgres) Ok(DatabaseBackend::Postgres)
} else if url.starts_with("sqlite://") { } else if url.starts_with("sqlite://") {
Ok(DatabaseBackend::Sqlite) Ok(DatabaseBackend::Sqlite)
} else { } else {
// `to_string()` crée un String owned depuis un &str // `to_string()` creates an owned String from a &str
Err(ConfigError::UnknownBackend(url.to_string())) Err(ConfigError::UnknownBackend(url.to_string()))
} }
} }

View File

@@ -1,15 +1,13 @@
// server/mod.rs — Module serveur // server/mod.rs — Server-side module
// //
// Contient tout le code qui s'exécute uniquement côté serveur. // Contains all code that runs on the server only.
// //
// Certains sous-modules utilisent des crates SSR-only (dotenvy, ipnetwork...) // Some sub-modules depend on SSR-only crates (dotenvy, ipnetwork...)
// et sont donc protégés par `#[cfg(feature = "ssr")]` pour ne pas être // and are therefore gated with `#[cfg(feature = "ssr")]` to prevent
// compilés dans le bundle WASM. // them from being compiled into the WASM bundle.
// Accessible des deux côtés (handlers HTTP sont SSR mais le module est déclaré shared)
pub mod routes; pub mod routes;
// Modules SSR uniquement — utilisent des crates non disponibles en WASM
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub mod config; pub mod config;

View File

@@ -1,26 +1,26 @@
// server/routes.rs — Handlers HTTP Axum additionnels // server/routes.rs — Additional Axum HTTP handlers
// //
// Ces handlers complètent les routes gérées par Leptos. // These handlers complement the routes managed by Leptos.
// Exemples d'usages futurs : // Intended for future use:
// - Endpoints API REST (/api/...) // - REST API endpoints (/api/...)
// - Exports de fichiers (CSV, PDF...) // - File exports (CSV, PDF...)
// - Webhooks entrants // - Incoming webhooks
// - Health check pour le monitoring (/health) // - Health check endpoint (/health)
// //
// `#[cfg(feature = "ssr")]` protège tout ce fichier : // The entire file is guarded by `#[cfg(feature = "ssr")]`:
// Axum n'existe pas dans le bundle WASM, donc on ne compile ce code qu'en mode serveur. // Axum does not exist in the WASM bundle, so this code is server-only.
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
use axum::{http::StatusCode, response::IntoResponse}; use axum::{http::StatusCode, response::IntoResponse};
// Handler 404 — utilisé comme fallback dans main.rs pour toute URL non reconnue. // Fallback 404 handler — used in main.rs for any URL not matched by Leptos or Axum.
// //
// `impl IntoResponse` : Axum accepte n'importe quel type qui implémente ce trait. // `impl IntoResponse`: Axum accepts any type that implements this trait.
// Un tuple `(StatusCode, &str)` l'implémente automatiquement : // A `(StatusCode, &str)` tuple implements it automatically:
// Axum en fait une réponse HTTP 404 avec le corps "Page introuvable". // Axum turns it into an HTTP 404 response with the given body.
// //
// `async fn` est obligatoire pour les handlers Axum, même sans opération asynchrone. // `async fn` is required for Axum handlers, even without any async operations.
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub async fn not_found_handler() -> impl IntoResponse { pub async fn not_found_handler() -> impl IntoResponse {
(StatusCode::NOT_FOUND, "Page introuvable") (StatusCode::NOT_FOUND, "Not found")
} }

View File

@@ -1,64 +1,62 @@
// server/validation.rs — Validation des données métier // server/validation.rs — Business rule validation
// //
// Ce module vérifie les règles métier qui ne peuvent pas être exprimées // This module enforces rules that cannot be expressed through Rust's type system
// uniquement par les types Rust. Il s'exécute côté serveur uniquement // alone. It runs server-side only because it uses `ipnetwork` for CIDR arithmetic.
// car il utilise `ipnetwork` pour les calculs CIDR.
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use std::net::IpAddr; use std::net::IpAddr;
use thiserror::Error; use thiserror::Error;
// ─── Erreurs de validation ──────────────────────────────────────────────────── // ─── Validation errors ────────────────────────────────────────────────────────
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ValidationError { pub enum ValidationError {
/// Le format CIDR est invalide (ex: "192.168.1/24" au lieu de "192.168.1.0/24") /// The CIDR string is malformed (e.g. "192.168.1/24" instead of "192.168.1.0/24")
#[error("CIDR invalide '{0}' : {1}")] #[error("Invalid CIDR '{0}': {1}")]
CidrInvalide(String, ipnetwork::IpNetworkError), InvalidCidr(String, ipnetwork::IpNetworkError),
/// L'adresse IP n'a pas le bon format /// The IP address string is malformed
#[error("Adresse IP invalide '{0}' : {1}")] #[error("Invalid IP address '{0}': {1}")]
IpInvalide(String, std::net::AddrParseError), InvalidIp(String, std::net::AddrParseError),
/// L'IP de l'hôte n'appartient pas à la plage du réseau /// The host IP does not fall within the network's CIDR range
#[error("L'adresse IP {ip} n'appartient pas au réseau {cidr}")] #[error("IP address {ip} does not belong to network {cidr}")]
IpHorsReseau { ip: String, cidr: String }, IpOutsideNetwork { ip: String, cidr: String },
} }
// ─── Validation du CIDR ─────────────────────────────────────────────────────── // ─── CIDR validation ──────────────────────────────────────────────────────────
/// Vérifie qu'une chaîne est un CIDR valide. /// Validates that a string is a well-formed CIDR block.
/// Retourne le réseau parsé si valide. /// Returns the parsed network on success.
/// ///
/// `&str` : on emprunte la chaîne sans en prendre possession (pas de copie). /// `&str`: borrowed string reference — no copy, just a borrow.
/// `Result<T, E>` : soit une valeur T (succès), soit une erreur E (échec). /// `Result<T, E>`: either a value T (success) or an error E (failure).
pub fn valider_cidr(cidr: &str) -> Result<IpNetwork, ValidationError> { pub fn validate_cidr(cidr: &str) -> Result<IpNetwork, ValidationError> {
cidr.parse::<IpNetwork>() cidr.parse::<IpNetwork>()
// `.map_err` transforme l'erreur si `parse` échoue // `.map_err` transforms the error if `parse` fails.
// `|e|` est une closure : une fonction anonyme qui prend `e` en paramètre // `|e|` is a closure: an anonymous function that takes `e` as a parameter.
.map_err(|e| ValidationError::CidrInvalide(cidr.to_string(), e)) .map_err(|e| ValidationError::InvalidCidr(cidr.to_string(), e))
} }
// ─── Validation de l'appartenance IP ↔ Réseau ──────────────────────────────── // ─── IP-in-network validation ─────────────────────────────────────────────────
/// Vérifie qu'une adresse IP appartient à la plage d'un réseau CIDR. /// Verifies that an IP address belongs to a given CIDR network range.
/// ///
/// Règle métier clé : un hôte doit toujours être dans le réseau auquel il appartient. /// Key business rule: a host must always reside within its assigned network.
/// Ex : 192.168.1.10 ✓ dans 192.168.1.0/24 /// Example: 192.168.1.10 ✓ in 192.168.1.0/24
/// 10.0.0.1 ✗ dans 192.168.1.0/24 /// 10.0.0.1 ✗ in 192.168.1.0/24
pub fn valider_ip_dans_reseau(ip: &str, cidr: &str) -> Result<(), ValidationError> { pub fn validate_ip_in_network(ip: &str, cidr: &str) -> Result<(), ValidationError> {
// On parse le CIDR et l'IP, propageant les erreurs avec `?` let network = validate_cidr(cidr)?;
let reseau = valider_cidr(cidr)?;
let adresse: IpAddr = ip let address: IpAddr = ip
.parse() .parse()
.map_err(|e| ValidationError::IpInvalide(ip.to_string(), e))?; .map_err(|e| ValidationError::InvalidIp(ip.to_string(), e))?;
// `IpNetwork::contains` retourne true si l'IP est dans la plage // `IpNetwork::contains` returns true if the address falls within the range.
if reseau.contains(adresse) { if network.contains(address) {
Ok(()) Ok(())
} else { } else {
Err(ValidationError::IpHorsReseau { Err(ValidationError::IpOutsideNetwork {
ip: ip.to_string(), ip: ip.to_string(),
cidr: cidr.to_string(), cidr: cidr.to_string(),
}) })
@@ -67,44 +65,44 @@ pub fn valider_ip_dans_reseau(ip: &str, cidr: &str) -> Result<(), ValidationErro
// ─── Tests ──────────────────────────────────────────────────────────────────── // ─── Tests ────────────────────────────────────────────────────────────────────
// `#[cfg(test)]` : ce bloc n'est compilé que lors de `cargo test` // `#[cfg(test)]`: this block is only compiled when running `cargo test`.
// Écrire les tests directement dans le même fichier est idiomatique en Rust. // Writing tests in the same file as the code being tested is idiomatic Rust.
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
// `super::*` importe tout depuis le module parent (ce fichier) // `super::*` imports everything from the parent module (this file).
use super::*; use super::*;
#[test] #[test]
fn ip_dans_reseau_valide() { fn ip_within_valid_network() {
// `.unwrap()` est acceptable dans les tests — si ça échoue, le test échoue // `.unwrap()` is acceptable in tests — a failure here fails the test.
assert!(valider_ip_dans_reseau("192.168.1.10", "192.168.1.0/24").is_ok()); assert!(validate_ip_in_network("192.168.1.10", "192.168.1.0/24").is_ok());
assert!(valider_ip_dans_reseau("10.0.0.1", "10.0.0.0/8").is_ok()); assert!(validate_ip_in_network("10.0.0.1", "10.0.0.0/8").is_ok());
assert!(valider_ip_dans_reseau("172.16.5.100", "172.16.0.0/12").is_ok()); assert!(validate_ip_in_network("172.16.5.100", "172.16.0.0/12").is_ok());
} }
#[test] #[test]
fn ip_hors_reseau() { fn ip_outside_network() {
assert!(valider_ip_dans_reseau("10.0.0.1", "192.168.1.0/24").is_err()); assert!(validate_ip_in_network("10.0.0.1", "192.168.1.0/24").is_err());
assert!(valider_ip_dans_reseau("192.168.2.1", "192.168.1.0/24").is_err()); assert!(validate_ip_in_network("192.168.2.1", "192.168.1.0/24").is_err());
} }
#[test] #[test]
fn cidr_invalide() { fn invalid_cidr() {
assert!(valider_cidr("pas-un-cidr").is_err()); assert!(validate_cidr("not-a-cidr").is_err());
assert!(valider_cidr("192.168.1/24").is_err()); // adresse tronquée assert!(validate_cidr("192.168.1/24").is_err()); // truncated address
assert!(valider_cidr("192.168.1.0/33").is_err()); // préfixe > 32 assert!(validate_cidr("192.168.1.0/33").is_err()); // prefix > 32
} }
#[test] #[test]
fn ip_invalide() { fn invalid_ip() {
assert!(valider_ip_dans_reseau("999.0.0.1", "192.168.1.0/24").is_err()); assert!(validate_ip_in_network("999.0.0.1", "192.168.1.0/24").is_err());
assert!(valider_ip_dans_reseau("pas-une-ip", "192.168.1.0/24").is_err()); assert!(validate_ip_in_network("not-an-ip", "192.168.1.0/24").is_err());
} }
#[test] #[test]
fn protocole_connu() { fn known_protocol() {
assert_eq!(crate::models::Port::protocole_connu(22), Some("SSH")); assert_eq!(crate::models::Port::known_protocol(22), Some("SSH"));
assert_eq!(crate::models::Port::protocole_connu(443), Some("HTTPS")); assert_eq!(crate::models::Port::known_protocol(443), Some("HTTPS"));
assert_eq!(crate::models::Port::protocole_connu(9999), None); assert_eq!(crate::models::Port::known_protocol(9999), None);
} }
} }