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:
18
.env.example
Normal file
18
.env.example
Normal file
@@ -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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
/target
|
||||
.env
|
||||
data/
|
||||
|
||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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") ---
|
||||
|
||||
|
||||
10
src/main.rs
10
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...).
|
||||
|
||||
101
src/server/config.rs
Normal file
101
src/server/config.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user