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
|
/target
|
||||||
|
.env
|
||||||
|
data/
|
||||||
|
|||||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -315,6 +315,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenvy"
|
||||||
|
version = "0.15.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "drain_filter_polyfill"
|
name = "drain_filter_polyfill"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@@ -1590,6 +1596,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
|
"dotenvy",
|
||||||
"leptos",
|
"leptos",
|
||||||
"leptos_axum",
|
"leptos_axum",
|
||||||
"leptos_meta",
|
"leptos_meta",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ ssr = [
|
|||||||
"dep:tower-http",
|
"dep:tower-http",
|
||||||
"dep:leptos_axum",
|
"dep:leptos_axum",
|
||||||
"dep:tracing-subscriber",
|
"dep:tracing-subscriber",
|
||||||
|
"dep:dotenvy",
|
||||||
"leptos/ssr",
|
"leptos/ssr",
|
||||||
"leptos_meta/ssr",
|
"leptos_meta/ssr",
|
||||||
"leptos_router/ssr",
|
"leptos_router/ssr",
|
||||||
@@ -59,6 +60,8 @@ leptos_axum = { version = "0.7", optional = true }
|
|||||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||||
# Formateur de logs pour le terminal (affiche les messages tracing::info!...)
|
# Formateur de logs pour le terminal (affiche les messages tracing::info!...)
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true }
|
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") ---
|
# --- 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 leptos::view;
|
||||||
use rust_ipam::{
|
use rust_ipam::{
|
||||||
app::{App, Shell},
|
app::{App, Shell},
|
||||||
server::routes::not_found_handler,
|
server::{config::AppConfig, routes::not_found_handler},
|
||||||
};
|
};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
@@ -36,6 +36,14 @@ async fn main() {
|
|||||||
|
|
||||||
tracing::info!("Démarrage du serveur Rust IPAM...");
|
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
|
// `Some("Cargo.toml")` indique à Leptos de lire la section
|
||||||
// [package.metadata.leptos] du Cargo.toml pour la configuration
|
// [package.metadata.leptos] du Cargo.toml pour la configuration
|
||||||
// (noms de fichiers, chemins, adresse serveur...).
|
// (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,
|
// Elles apparaissent comme des appels asynchrones normaux côté client,
|
||||||
// mais s'exécutent exclusivement sur le serveur — transparence totale.
|
// mais s'exécutent exclusivement sur le serveur — transparence totale.
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
|||||||
Reference in New Issue
Block a user