Files
pmomusic/pmoserver/src/server.rs

776 lines
25 KiB
Rust
Raw Normal View History

//! # Module Server - API de haut niveau pour Axum
//!
//! Ce module fournit une abstraction simple et ergonomique pour créer des serveurs HTTP
//! avec Axum, en cachant la complexité de la configuration et du routage.
//!
//! ## Fonctionnalités
//!
//! - 🚀 **Routes JSON simples** : Ajoutez des endpoints API avec `add_route()`
//! - 📁 **Fichiers statiques** : Servez des assets avec `add_dir()`
//! - ⚛️ **Applications SPA** : Support pour Vue.js/React avec `add_spa()`
//! - 🔀 **Redirections** : Redirigez des routes avec `add_redirect()`
//! - 🎯 **Handlers personnalisés** : Support SSE, WebSocket, etc. avec `add_handler_with_state()`
2025-10-01 07:42:16 +02:00
//! - 📚 **Documentation API** : OpenAPI/Swagger automatique avec `add_openapi()`
//! - ⚡ **Gestion gracieuse** : Arrêt propre sur Ctrl+C
use crate::logs::{LogState, init_logging, log_dump, log_sse};
use axum::extract::State;
use axum::handler::Handler;
use axum::response::Redirect;
2025-11-29 01:56:40 +01:00
use axum::routing::{any, get, post};
use axum::{Json, Router};
use axum_embed::ServeEmbed;
use pmoconfig::get_config;
use rust_embed::RustEmbed;
use serde::Serialize;
2025-10-07 15:03:52 +02:00
use std::future::Future;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::{signal, sync::RwLock, task::JoinHandle};
2025-12-05 07:08:03 +01:00
use tokio_util::sync::CancellationToken;
2025-12-17 07:25:47 +01:00
use tracing::{error, info, warn};
2025-10-18 22:14:41 +02:00
use utoipa::OpenApi;
2025-10-19 13:42:29 +02:00
use utoipa_swagger_ui::SwaggerUi;
/// Info serveur sérialisable
2025-10-01 07:42:16 +02:00
#[derive(Clone, Serialize, utoipa::ToSchema)]
pub struct ServerInfo {
pub name: String,
pub base_url: String,
pub http_port: u16,
}
/// Entrée du registre d'API
#[derive(Clone, Serialize, utoipa::ToSchema)]
pub struct ApiRegistryEntry {
/// Nom de l'API
pub name: String,
/// Chemin de base de l'API
pub path: String,
/// Chemin vers Swagger UI
pub swagger_ui_path: String,
/// Chemin vers le JSON OpenAPI
pub openapi_json_path: String,
/// Nombre d'endpoints
pub endpoint_count: usize,
/// Version de l'API
pub version: String,
/// Description de l'API
pub description: Option<String>,
/// Titre de l'API
pub title: String,
}
/// Liste des APIs enregistrées
#[derive(Clone, Serialize, utoipa::ToSchema)]
pub struct ApiRegistry {
/// Liste des APIs disponibles
pub apis: Vec<ApiRegistryEntry>,
/// Nombre total d'endpoints
pub total_endpoints: usize,
}
type ApiRegistryState = Arc<RwLock<Vec<ApiRegistryEntry>>>;
/// Handler pour l'endpoint /api/registry
async fn get_api_registry(State(registry): State<ApiRegistryState>) -> Json<ApiRegistry> {
let apis = registry.read().await.clone();
let total_endpoints = apis.iter().map(|api| api.endpoint_count).sum();
Json(ApiRegistry {
apis,
total_endpoints,
})
}
/// Serveur principal
pub struct Server {
name: String,
base_url: String,
http_port: u16,
router: Arc<RwLock<Router>>,
2025-10-01 07:42:16 +02:00
api_router: Arc<RwLock<Option<Router>>>,
join_handle: Option<JoinHandle<()>>,
2025-10-06 20:24:04 +02:00
log_state: Option<LogState>,
api_registry: ApiRegistryState,
2025-12-05 07:08:03 +01:00
shutdown_token: CancellationToken,
}
impl Server {
/// Crée une nouvelle instance de serveur
///
/// # Arguments
///
/// * `name` - Nom du serveur (pour les logs)
/// * `base_url` - URL de base (ex: "http://localhost:3000")
/// * `http_port` - Port HTTP à écouter
///
/// # Exemple
///
/// ```rust
2025-10-09 22:36:33 +02:00
/// # use pmoserver::Server;
/// let server = Server::new("MyAPI", "http://localhost:3000", 3000);
/// ```
pub fn new(name: impl Into<String>, base_url: impl Into<String>, http_port: u16) -> Self {
let api_registry = Arc::new(RwLock::new(Vec::new()));
let base_url = base_url.into();
// Créer le router initial avec l'endpoint de registre
// Note: le base_url_layer est appliqué plus tard via le fallback dynamique
let registry_route = Router::new()
.route("/api/registry", get(get_api_registry))
.with_state(api_registry.clone());
let server = Self {
name: name.into(),
base_url,
http_port,
router: Arc::new(RwLock::new(registry_route)),
2025-10-01 07:42:16 +02:00
api_router: Arc::new(RwLock::new(None)),
join_handle: None,
2025-10-06 20:24:04 +02:00
log_state: None,
api_registry,
2025-12-05 07:08:03 +01:00
shutdown_token: CancellationToken::new(),
};
// Initialiser PMO_SERVER_URL avec l'URL complète (incluant le port).
// base_url() normalise l'URL en ajoutant le port si absent.
// SAFETY: appelé une seule fois au démarrage du serveur, avant tout thread concurrent.
unsafe { std::env::set_var("PMO_SERVER_URL", server.base_url()) };
server
}
pub fn new_configured() -> Self {
let config = get_config();
let url = config.get_base_url();
let port = config.get_http_port();
2025-10-07 15:03:52 +02:00
Self::new("PMO-Music-Server", url, port)
}
2025-12-05 07:08:03 +01:00
/// Retourne une copie du token d'arrêt gracieux
///
/// Ce token peut être donné aux composants qui ont besoin de savoir
/// quand le serveur s'arrête (threads, tâches longues, etc.)
pub fn shutdown_token(&self) -> CancellationToken {
self.shutdown_token.clone()
}
/// Ajoute une route JSON dynamique
///
/// Crée un endpoint qui retourne du JSON. La closure fournie sera appelée
/// à chaque requête GET sur le chemin spécifié.
///
/// # Arguments
///
/// * `path` - Chemin de la route (ex: "/api/hello")
/// * `f` - Closure async retournant une valeur sérialisable
///
/// # Exemple
///
2025-10-09 22:36:33 +02:00
/// ```rust,ignore
/// # use pmoserver::Server;
/// # #[tokio::main]
/// # async fn main() {
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
/// server.add_route("/api/status", || async {
/// serde_json::json!({
/// "status": "online",
/// "version": "1.0.0"
/// })
/// }).await;
/// # }
/// ```
pub async fn add_route<F, Fut, T>(&mut self, path: &str, f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
2025-10-07 15:03:52 +02:00
Fut: Future<Output = T> + Send + 'static,
T: Serialize + Send + 'static,
{
let f = Arc::new(f);
let handler = {
let f = f.clone();
move || {
let f = f.clone();
async move { Json(f().await) }
}
};
let route = Router::new().route("/", get(handler));
let mut r = self.router.write().await;
2025-10-07 15:03:52 +02:00
*r = if path == "/" {
std::mem::take(&mut *r).merge(route)
} else {
std::mem::take(&mut *r).nest(path, route)
};
}
2025-10-07 15:03:52 +02:00
/// Ajoute un handler Axum standard
pub async fn add_handler<H, T>(&mut self, path: &str, handler: H)
where
H: Handler<T, ()> + Clone + 'static,
T: 'static,
{
let route = Router::new().route("/", get(handler.clone()));
2025-10-07 13:12:44 +02:00
let mut r = self.router.write().await;
2025-10-07 15:03:52 +02:00
*r = if path == "/" {
std::mem::take(&mut *r).merge(route)
} else {
std::mem::take(&mut *r).nest(path, route)
};
}
2025-10-07 13:12:44 +02:00
2025-10-07 15:03:52 +02:00
/// Ajoute un handler POST avec état
pub async fn add_post_handler_with_state<H, T, S>(&mut self, path: &str, handler: H, state: S)
where
H: Handler<T, S> + Clone + 'static,
T: 'static,
S: Clone + Send + Sync + 'static,
{
let route = Router::new()
.route("/", post(handler.clone()))
.with_state(state.clone());
2025-10-07 13:12:44 +02:00
2025-10-07 15:03:52 +02:00
let mut r = self.router.write().await;
*r = if path == "/" {
std::mem::take(&mut *r).merge(route)
2025-10-07 13:12:44 +02:00
} else {
2025-10-07 15:03:52 +02:00
std::mem::take(&mut *r).nest(path, route)
2025-10-07 13:12:44 +02:00
};
2025-10-07 15:03:52 +02:00
}
2025-10-07 13:12:44 +02:00
2025-10-07 15:03:52 +02:00
/// Ajoute un handler avec état
pub async fn add_handler_with_state<H, T, S>(&mut self, path: &str, handler: H, state: S)
where
H: Handler<T, S> + Clone + 'static,
T: 'static,
S: Clone + Send + Sync + 'static,
{
let route = Router::new()
.route("/", get(handler.clone()))
.with_state(state.clone());
let mut r = self.router.write().await;
*r = if path == "/" {
std::mem::take(&mut *r).merge(route)
} else {
std::mem::take(&mut *r).nest(path, route)
};
2025-10-07 13:12:44 +02:00
}
2025-10-07 15:03:52 +02:00
2025-11-29 01:56:40 +01:00
/// Ajoute un handler qui accepte tous les verbes HTTP (ANY) avec état
pub async fn add_any_handler_with_state<H, T, S>(&mut self, path: &str, handler: H, state: S)
where
2025-11-29 01:56:40 +01:00
H: Handler<T, S> + Clone + 'static,
T: 'static,
S: Clone + Send + Sync + 'static,
{
let route = Router::new()
.route("/", any(handler.clone()))
.with_state(state.clone());
let mut r = self.router.write().await;
*r = if path == "/" {
std::mem::take(&mut *r).merge(route)
} else {
std::mem::take(&mut *r).nest(path, route)
};
}
2025-10-07 15:03:52 +02:00
/// Ajoute un répertoire statique
pub async fn add_dir<E>(&mut self, path: &str)
where
E: RustEmbed + Clone + Send + Sync + 'static,
{
let serve = ServeEmbed::<E>::new();
let mut r = self.router.write().await;
2025-10-07 08:33:06 +02:00
2025-10-07 15:03:52 +02:00
let route = Router::new().fallback_service(serve);
*r = if path == "/" {
std::mem::take(&mut *r).merge(route)
} else {
2025-10-07 15:03:52 +02:00
std::mem::take(&mut *r).nest(path, route)
};
}
/// Ajoute une Single Page Application (SPA)
///
/// Sert une application JavaScript moderne (Vue.js, React, etc.) avec support
/// du routage côté client. Tous les chemins non trouvés renvoient `index.html`
/// pour permettre au routeur JavaScript de gérer la navigation.
///
/// # Arguments
///
/// * `path` - Chemin où monter l'application (souvent "/" ou "/app")
///
/// # Type Parameter
///
/// * `E` - Type RustEmbed contenant les fichiers de la SPA
///
/// # Exemple avec Vue.js
///
2025-10-09 22:36:33 +02:00
/// ```rust,ignore
/// # use pmoserver::Server;
/// # use rust_embed::RustEmbed;
/// #[derive(RustEmbed, Clone)]
/// #[folder = "webapp/dist"] // Build output de Vue.js
/// struct WebApp;
///
/// # #[tokio::main]
/// # async fn main() {
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
/// server.add_spa::<WebApp>("/").await;
/// // L'app Vue.js gère toutes les routes comme /about, /users, etc.
/// # }
/// ```
///
/// # Note
///
/// Pour Vue.js/Vite, configure le `base` dans `vite.config.js` si tu montes
/// sur un sous-chemin :
/// ```javascript
/// export default {
/// base: '/app/'
/// }
/// ```
pub async fn add_spa<E>(&mut self, path: &str)
where
E: RustEmbed + Clone + Send + Sync + 'static,
{
let serve = ServeEmbed::<E>::with_parameters(
Some("index.html".to_string()),
axum_embed::FallbackBehavior::Ok,
Some("index.html".to_string()),
);
2025-10-07 08:33:06 +02:00
let mut r = self.router.write().await;
2025-10-07 08:33:06 +02:00
2025-10-07 15:03:52 +02:00
let route = Router::new().fallback_service(serve);
*r = if path == "/" {
std::mem::take(&mut *r).merge(route)
} else {
2025-10-07 15:03:52 +02:00
std::mem::take(&mut *r).nest(path, route)
};
}
/// Ajoute une redirection HTTP
///
/// Redirige automatiquement les requêtes d'un chemin vers un autre avec un code 308 (permanent).
///
/// # Arguments
///
/// * `from` - Chemin source (peut être "/" pour la racine)
/// * `to` - Chemin de destination
///
/// # Exemple
///
2025-10-09 22:36:33 +02:00
/// ```rust,ignore
/// # use pmoserver::Server;
/// # #[tokio::main]
/// # async fn main() {
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
/// // Rediriger la racine vers /app
/// server.add_redirect("/", "/app").await;
/// # }
/// ```
2025-10-07 15:03:52 +02:00
pub async fn add_redirect(&mut self, from: &str, to: &str) {
let to = to.to_string();
2025-10-07 15:03:52 +02:00
let make_handler = || {
let target = to.clone();
get(move || async move { Redirect::permanent(&target) })
};
let mut r = self.router.write().await;
2025-10-07 15:03:52 +02:00
*r = if from == "/" {
std::mem::take(&mut *r).merge(Router::new().route("/", make_handler()))
} else {
2025-10-07 15:03:52 +02:00
std::mem::take(&mut *r).nest(from, Router::new().route("/", make_handler()))
};
}
2025-10-07 08:33:06 +02:00
/// Ajoute une API documentée avec OpenAPI et Swagger UI
2025-10-01 07:42:16 +02:00
///
2025-10-07 08:33:06 +02:00
/// Cette méthode fusionne le `api_router` fourni avec le router principal du serveur.
/// Chaque appel peut ajouter une nouvelle API distincte, avec sa propre documentation Swagger.
2025-10-01 07:42:16 +02:00
///
/// # Arguments
///
/// * `api_router` - Router Axum contenant les routes API
2025-10-07 08:33:06 +02:00
/// * `openapi` - Spécification OpenAPI générée par `utoipa`
/// * `name` - Nom unique pour cette API, utilisé pour différencier le chemin Swagger UI et le JSON OpenAPI
2025-10-01 07:42:16 +02:00
///
/// # Exemple
///
2025-10-01 20:49:50 +02:00
/// ```ignore
2025-10-01 07:42:16 +02:00
/// use utoipa::OpenApi;
/// use axum::{Router, Json, routing::get};
2025-10-01 20:49:50 +02:00
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize, utoipa::ToSchema)]
/// struct User {
/// id: u64,
/// name: String,
/// }
2025-10-01 07:42:16 +02:00
///
/// #[derive(utoipa::OpenApi)]
/// #[openapi(
/// paths(get_users),
/// components(schemas(User))
/// )]
2025-10-07 08:33:06 +02:00
/// struct ApiDoc1;
2025-10-01 07:42:16 +02:00
///
/// #[utoipa::path(
/// get,
/// path = "/users",
/// responses((status = 200, description = "List users"))
/// )]
/// async fn get_users() -> Json<Vec<User>> {
/// Json(vec![])
/// }
///
2025-10-07 08:33:06 +02:00
/// #[derive(utoipa::OpenApi)]
/// #[openapi(
/// paths(get_products),
/// components(schemas(Product))
/// )]
/// struct ApiDoc2;
2025-10-01 07:42:16 +02:00
///
2025-10-07 08:33:06 +02:00
/// #[utoipa::path(
/// get,
/// path = "/products",
/// responses((status = 200, description = "List products"))
/// )]
/// async fn get_products() -> Json<Vec<Product>> {
/// Json(vec![])
/// }
///
/// let api_router1 = Router::new().route("/users", get(get_users));
/// let api_router2 = Router::new().route("/products", get(get_products));
///
/// // Ajouter les deux API au serveur, chacune avec son nom unique
/// server.add_openapi(api_router1, ApiDoc1::openapi(), "api1").await;
/// server.add_openapi(api_router2, ApiDoc2::openapi(), "api2").await;
2025-10-01 07:42:16 +02:00
/// ```
2025-10-07 08:33:06 +02:00
///
/// Résultat :
///
2025-10-07 13:12:44 +02:00
/// - `/api/api1/users` et `/api/api2/products` sont accessibles via Axum.
2025-10-07 08:33:06 +02:00
/// - `/swagger-ui/api1` et `/swagger-ui/api2` affichent la documentation Swagger correspondante.
/// - `/api-docs/api1.json` et `/api-docs/api2.json` fournissent les spécifications OpenAPI respectives.
pub async fn add_openapi(
&mut self,
api_router: Router,
openapi: utoipa::openapi::OpenApi,
2025-10-07 15:03:52 +02:00
name: &str,
2025-10-07 08:33:06 +02:00
) {
2025-10-01 07:42:16 +02:00
let mut api_r = self.api_router.write().await;
2025-10-07 08:33:06 +02:00
*api_r = Some(api_router.clone());
2025-10-07 15:03:52 +02:00
drop(api_r);
2025-10-07 08:33:06 +02:00
let swagger_path = format!("/swagger-ui/{}", name);
let swagger_path_static: &'static str = Box::leak(swagger_path.clone().into_boxed_str());
2025-10-01 07:42:16 +02:00
2025-10-07 08:33:06 +02:00
let openapi_json_path = format!("/api-docs/{}.json", name);
2025-10-19 13:42:29 +02:00
let openapi_json_path_static: &'static str =
Box::leak(openapi_json_path.clone().into_boxed_str());
// Compter le nombre d'endpoints dans l'OpenAPI spec
let endpoint_count = openapi.paths.paths.len();
// Extraire les informations de l'API depuis la spec OpenAPI
let version = openapi.info.version.clone();
let description = openapi.info.description.clone();
let title = openapi.info.title.clone();
// Enregistrer l'API dans le registre
let registry_entry = ApiRegistryEntry {
name: name.to_string(),
path: format!("/api/{}", name),
swagger_ui_path: swagger_path,
openapi_json_path,
endpoint_count,
version,
description,
title,
};
let mut registry = self.api_registry.write().await;
registry.push(registry_entry);
drop(registry);
2025-10-01 07:42:16 +02:00
2025-10-07 08:33:06 +02:00
let swagger = SwaggerUi::new(swagger_path_static).url(openapi_json_path_static, openapi);
2025-10-07 13:12:44 +02:00
let base_path = format!("/api/{}", name);
let nested_router = Router::new().nest(&base_path, api_router);
2025-10-01 07:42:16 +02:00
let mut r = self.router.write().await;
2025-10-07 15:03:52 +02:00
*r = std::mem::take(&mut *r).merge(nested_router).merge(swagger);
}
/// Ajoute un sous-router au serveur
///
/// - Si `path` est "/", merge directement au router principal
/// - Sinon, nest le router sous le chemin donné
pub async fn add_router(&mut self, path: &str, sub_router: Router) {
let mut r = self.router.write().await;
let combined = if path == "/" {
// Merge directement à la racine
r.clone().merge(sub_router)
} else {
// Sous-chemin => nest
let normalized = format!("/{}", path.trim_start_matches('/'));
r.clone().nest(&normalized, sub_router)
};
2025-10-07 08:33:06 +02:00
*r = combined;
2025-10-01 07:42:16 +02:00
}
2025-10-07 15:03:52 +02:00
/// Démarre le serveur HTTP
///
/// Lance le serveur sur le port configuré et met en place la gestion
/// de Ctrl+C pour un arrêt gracieux.
///
/// # Exemple
///
2025-10-09 22:36:33 +02:00
/// ```rust,ignore
/// # use pmoserver::Server;
/// # #[tokio::main]
/// # async fn main() {
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
/// server.start().await;
/// server.wait().await; // Attend Ctrl+C
/// # }
/// ```
pub async fn start(&mut self) {
let addr = SocketAddr::from(([0, 0, 0, 0], self.http_port));
2025-10-07 08:33:06 +02:00
info!(
"Server {} running at [http://{}:{}](http://{}:{})",
self.name, self.base_url, self.http_port, self.base_url, self.http_port
);
2025-10-01 07:42:16 +02:00
let router = self.router.clone();
2025-12-17 07:25:47 +01:00
let shutdown_token = self.shutdown_token.clone();
2025-12-05 07:08:03 +01:00
// Créer un channel pour signaler l'arrêt gracieux
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
self.join_handle = Some(tokio::spawn(async move {
2025-12-17 07:25:47 +01:00
let server_future = async {
2025-12-17 21:41:24 +01:00
let listener = match tokio::net::TcpListener::bind(addr).await {
Ok(l) => l,
Err(e) => {
error!("Failed to bind to {}: {}", addr, e);
panic!("Cannot start server: {}", e);
}
};
2025-12-17 07:25:47 +01:00
// Utiliser un router dynamique qui relit le router à chaque requête.
// Cela permet d'enregistrer de nouvelles routes après le démarrage du serveur
// (ex: WebRenderer dynamique).
let dynamic_router = axum::Router::new().fallback(move |req: axum::extract::Request| {
let router = router.clone();
async move {
use tower::ServiceExt;
let r = router.read().await.clone();
r.into_service::<axum::body::Body>().oneshot(req).await
}
});
axum::serve(listener, dynamic_router.into_make_service())
2025-12-17 07:25:47 +01:00
.with_graceful_shutdown(async move {
let _ = shutdown_rx.await;
})
.await
};
tokio::pin!(server_future);
let ctrl_c = signal::ctrl_c();
tokio::pin!(ctrl_c);
tokio::select! {
result = &mut server_future => {
if let Err(err) = result {
error!("Serveur HTTP arrêté avec une erreur: {}", err);
} else {
info!("Serveur HTTP arrêté proprement");
}
}
_ = &mut ctrl_c => {
info!("Ctrl+C reçu, arrêt gracieux");
shutdown_token.cancel();
let _ = shutdown_tx.send(());
if tokio::time::timeout(std::time::Duration::from_secs(5), &mut server_future).await.is_err() {
warn!("Arrêt gracieux trop long, fermeture forcée du serveur HTTP");
}
}
}
}));
}
/// Attend la fin du serveur
pub async fn wait(&mut self) {
if let Some(h) = self.join_handle.take() {
let _ = h.await;
}
}
/// Extrait le JoinHandle du serveur HTTP pour pouvoir l'awaiter
/// sans tenir le write lock du serveur global.
pub fn take_join_handle(&mut self) -> Option<JoinHandle<()>> {
self.join_handle.take()
}
/// Retourne l'URL de base vue par le client pour une requête donnée.
///
/// Priorité : headers `X-Forwarded-Proto` + `X-Forwarded-Host` (présents
/// quand la requête passe par un reverse proxy comme NPM), sinon fallback
/// sur `self.base_url()`. Cela permet de générer des URLs correctes aussi
/// bien en accès local direct qu'en accès public via proxy.
pub fn request_base_url(&self, headers: &axum::http::HeaderMap) -> String {
let proto = headers
.get("x-forwarded-proto")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.split(',').next())
.map(str::trim);
let host = headers
.get("x-forwarded-host")
.or_else(|| headers.get("host"))
.and_then(|v| v.to_str().ok())
.map(str::trim);
match (proto, host) {
(Some(proto), Some(host)) => format!("{}://{}", proto, host),
_ => self.base_url(),
}
}
2025-10-19 13:42:29 +02:00
/// Retourne l'URL de base complète du serveur (schéma + hôte + port).
///
/// La valeur configurable peut omettre le schéma ou le port ; cette méthode
/// s'assure donc que les clients reçoivent toujours une URL exploitable comme
/// `http://192.168.0.10:8080`.
pub fn base_url(&self) -> String {
let mut base = self.base_url.trim_end_matches('/').to_string();
if !base.contains("://") {
base = format!("http://{}", base);
}
let has_port = base
.rsplit_once(':')
.and_then(|(_, port)| port.parse::<u16>().ok())
.is_some();
if has_port {
base
} else {
format!("{}:{}", base, self.http_port)
}
}
/// Récupère les infos du serveur
pub fn info(&self) -> ServerInfo {
ServerInfo {
name: self.name.clone(),
2025-10-19 13:42:29 +02:00
base_url: self.base_url(),
http_port: self.http_port,
}
}
2025-10-06 20:24:04 +02:00
/// Initialise le système de logging et enregistre les routes de logs
///
/// Cette méthode configure le système de tracing avec SSE et optionnellement la console,
/// puis enregistre automatiquement les routes `/log-sse` et `/log-dump`.
///
/// # Arguments
///
/// * `options` - Options de configuration du logging
///
/// # Exemple
///
2025-10-09 22:36:33 +02:00
/// ```rust,ignore
2025-10-06 20:24:04 +02:00
/// # use pmoserver::{ServerBuilder, logs::LoggingOptions};
/// # #[tokio::main]
/// # async fn main() {
/// let mut server = ServerBuilder::new_configured().build();
///
2025-10-13 11:35:07 +02:00
/// // Initialiser les logs
/// server.init_logging().await;
2025-10-06 20:24:04 +02:00
///
/// server.start().await;
/// # }
/// ```
2025-10-13 11:35:07 +02:00
pub async fn init_logging(&mut self) {
let log_state = init_logging();
2025-10-06 20:24:04 +02:00
2025-10-18 22:14:41 +02:00
// Enregistrer automatiquement les routes de logging SSE
2025-10-07 08:33:06 +02:00
self.add_handler_with_state("/log-sse", log_sse, log_state.clone())
.await;
self.add_handler_with_state("/log-dump", log_dump, log_state.clone())
.await;
2025-10-06 20:24:04 +02:00
2025-10-18 22:14:41 +02:00
// Enregistrer l'API REST de configuration des logs via OpenAPI
self.add_openapi(
crate::logs::create_logs_router(log_state.clone()),
crate::logs::LogsApiDoc::openapi(),
2025-10-19 13:42:29 +02:00
"logs",
)
.await;
2025-10-18 22:14:41 +02:00
2025-10-06 20:24:04 +02:00
self.log_state = Some(log_state);
}
}
/// Builder pattern
pub struct ServerBuilder {
name: String,
base_url: String,
http_port: u16,
}
impl ServerBuilder {
/// Crée un nouveau builder
///
/// # Arguments
///
/// * `name` - Nom du serveur
/// * `base_url` - URL de base (ex: "http://localhost:3000")
/// * `http_port` - Port HTTP
pub fn new(name: impl Into<String>, base_url: impl Into<String>, http_port: u16) -> Self {
Self {
name: name.into(),
base_url: base_url.into(),
http_port,
}
}
pub fn new_configured() -> Self {
let config = get_config();
Self {
name: "PMO-Music-Server".to_string(),
base_url: config.get_base_url(),
2025-10-07 08:33:06 +02:00
http_port: config.get_http_port(),
}
}
/// Construit le serveur
///
/// Consomme le builder et retourne une instance de `Server` prête à l'emploi.
///
/// # Exemple
///
/// ```rust
2025-10-09 22:36:33 +02:00
/// # use pmoserver::ServerBuilder;
/// let mut server = ServerBuilder::new("MyAPI", "http://localhost:3000", 3000)
/// .build();
/// ```
pub fn build(self) -> Server {
Server::new(self.name, self.base_url, self.http_port)
}
2025-10-07 08:33:06 +02:00
}