From b6d1e22d2511ec7546a7058586547b6c3363da2a Mon Sep 17 00:00:00 2001 From: mathieu Date: Fri, 15 May 2026 19:44:55 +0200 Subject: [PATCH] feat(config): add database configuration layer with backend detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add AppConfig loaded from .env via dotenvy. DATABASE_URL prefix determines the backend (sqlite:// → SQLite, postgresql:// → PostgreSQL). ConfigError via thiserror gives clear messages on missing or unknown URLs. Server logs the chosen backend at startup. Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 18 ++++++++ .gitignore | 2 + Cargo.lock | 7 +++ Cargo.toml | 3 ++ src/main.rs | 10 ++++- src/server/config.rs | 101 +++++++++++++++++++++++++++++++++++++++++++ src/server/mod.rs | 1 + 7 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 src/server/config.rs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c447bd0 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# .env.example — Copier ce fichier en .env et adapter les valeurs +# +# Ce fichier est versionné dans git pour servir de référence. +# Le fichier .env réel contient des secrets et ne doit JAMAIS être commité. + +# --- Base de données --- +# Choisir UNE des deux options ci-dessous : + +# Option 1 : SQLite (recommandé pour le développement — aucun serveur requis) +# Le fichier sera créé automatiquement s'il n'existe pas. +DATABASE_URL=sqlite://data/ipam.db + +# Option 2 : PostgreSQL (recommandé pour la production) +# DATABASE_URL=postgresql://utilisateur:motdepasse@localhost:5432/ipam + +# --- Logs --- +# Niveau de log : error | warn | info | debug | trace +RUST_LOG=info diff --git a/.gitignore b/.gitignore index ea8c4bf..2753164 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +.env +data/ diff --git a/Cargo.lock b/Cargo.lock index c851d78..942430b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -315,6 +315,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "drain_filter_polyfill" version = "0.1.3" @@ -1590,6 +1596,7 @@ version = "0.1.0" dependencies = [ "axum", "console_error_panic_hook", + "dotenvy", "leptos", "leptos_axum", "leptos_meta", diff --git a/Cargo.toml b/Cargo.toml index 47a476e..7620993 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ ssr = [ "dep:tower-http", "dep:leptos_axum", "dep:tracing-subscriber", + "dep:dotenvy", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", @@ -59,6 +60,8 @@ leptos_axum = { version = "0.7", optional = true } tower-http = { version = "0.5", features = ["fs"], optional = true } # Formateur de logs pour le terminal (affiche les messages tracing::info!...) tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true } +# Charge automatiquement le fichier .env au démarrage du serveur +dotenvy = { version = "0.15", optional = true } # --- Dépendances client uniquement (activées par la feature "hydrate") --- diff --git a/src/main.rs b/src/main.rs index 5d368fa..30b8437 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ async fn main() { use leptos::view; use rust_ipam::{ app::{App, Shell}, - server::routes::not_found_handler, + server::{config::AppConfig, routes::not_found_handler}, }; use tower_http::services::ServeDir; @@ -36,6 +36,14 @@ async fn main() { tracing::info!("Démarrage du serveur Rust IPAM..."); + // Charge la configuration depuis .env / variables d'environnement. + // On arrête le serveur immédiatement si la config est invalide — il ne peut + // pas fonctionner sans savoir à quelle base de données se connecter. + let app_config = AppConfig::from_env() + .expect("Erreur de configuration — vérifier le fichier .env"); + + tracing::info!("Base de données : {} ({})", app_config.backend, app_config.database_url); + // `Some("Cargo.toml")` indique à Leptos de lire la section // [package.metadata.leptos] du Cargo.toml pour la configuration // (noms de fichiers, chemins, adresse serveur...). diff --git a/src/server/config.rs b/src/server/config.rs new file mode 100644 index 0000000..e2d9de3 --- /dev/null +++ b/src/server/config.rs @@ -0,0 +1,101 @@ +// server/config.rs — Configuration de l'application +// +// Ce module charge et valide la configuration au démarrage du serveur. +// Toute la configuration passe par des variables d'environnement, +// elles-mêmes chargées depuis un fichier `.env` via dotenvy. +// +// Principe : une seule variable suffit pour choisir la base de données. +// DATABASE_URL=sqlite://data/ipam.db → SQLite (dev, pas de serveur nécessaire) +// DATABASE_URL=postgresql://user:pw@host/db → PostgreSQL (production) + +use thiserror::Error; + +// ─── Erreurs de configuration ───────────────────────────────────────────────── + +// `#[derive(Error)]` de la crate thiserror génère automatiquement l'impl +// du trait standard `std::error::Error` — pas besoin de l'écrire à la main. +// +// `#[error("...")]` définit le message affiché par Display / println!("{}", err). +#[derive(Debug, Error)] +pub enum ConfigError { + // `#[from]` permet de convertir automatiquement un VarError en ConfigError + // via l'opérateur `?`. Ex: std::env::var("X")? dans une fn -> Result<_, ConfigError> + #[error("Variable d'environnement manquante : {0}")] + MissingVar(#[from] std::env::VarError), + + #[error("URL de base de données non reconnue : '{0}' — doit commencer par sqlite:// ou postgresql://")] + UnknownBackend(String), +} + +// ─── Backend de base de données ─────────────────────────────────────────────── + +// `#[derive(Debug, Clone)]` génère automatiquement : +// - Debug : permet d'afficher la valeur avec {:?} dans les logs +// - Clone : permet de copier la valeur (nécessaire pour l'état Axum) +#[derive(Debug, Clone, PartialEq)] +pub enum DatabaseBackend { + Postgres, + Sqlite, +} + +// Impl Display pour pouvoir écrire tracing::info!("Backend : {}", backend) +impl std::fmt::Display for DatabaseBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DatabaseBackend::Postgres => write!(f, "PostgreSQL"), + DatabaseBackend::Sqlite => write!(f, "SQLite"), + } + } +} + +// ─── Configuration principale ───────────────────────────────────────────────── + +#[derive(Debug, Clone)] +pub struct AppConfig { + /// URL complète de connexion à la base de données. + /// Ex: "sqlite://data/ipam.db" ou "postgresql://user:pw@localhost/ipam" + pub database_url: String, + + /// Backend détecté automatiquement depuis le préfixe de DATABASE_URL. + pub backend: DatabaseBackend, +} + +impl AppConfig { + /// Charge la configuration depuis les variables d'environnement. + /// + /// Ordre de priorité pour les variables : + /// 1. Variables déjà définies dans le shell (ex: export DATABASE_URL=...) + /// 2. Fichier `.env` à la racine du projet + /// + /// Retourne une `ConfigError` si DATABASE_URL est absente ou invalide. + pub fn from_env() -> Result { + // Charge le fichier .env s'il existe. + // `let _ =` ignore silencieusement l'erreur si .env est absent — + // c'est voulu : en production les variables sont injectées directement. + let _ = dotenvy::dotenv(); + + // `std::env::var` retourne Result. + // Le `?` propage l'erreur VarError, convertie en ConfigError::MissingVar + // grâce au `#[from]` défini plus haut. + let database_url = std::env::var("DATABASE_URL")?; + + let backend = Self::detect_backend(&database_url)?; + + Ok(Self { database_url, backend }) + } + + /// Déduit le backend depuis le préfixe de l'URL. + /// + /// `&str` : référence vers une chaîne — pas de copie, juste un emprunt. + fn detect_backend(url: &str) -> Result { + // `starts_with` est une méthode de &str qui vérifie le préfixe + if url.starts_with("postgresql://") || url.starts_with("postgres://") { + Ok(DatabaseBackend::Postgres) + } else if url.starts_with("sqlite://") { + Ok(DatabaseBackend::Sqlite) + } else { + // `to_string()` crée un String owned depuis un &str + Err(ConfigError::UnknownBackend(url.to_string())) + } + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index 59ae352..2a94d54 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -9,4 +9,5 @@ // Elles apparaissent comme des appels asynchrones normaux côté client, // mais s'exécutent exclusivement sur le serveur — transparence totale. +pub mod config; pub mod routes;