feat(config): add database configuration layer with backend detection

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 19:44:55 +02:00
parent 18134d6f4b
commit b6d1e22d25
7 changed files with 141 additions and 1 deletions

View File

@@ -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...).

101
src/server/config.rs Normal file
View File

@@ -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<Self, ConfigError> {
// 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<String, VarError>.
// 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<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://") {
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()))
}
}
}

View File

@@ -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;