Sort le serveur dans une crate pmoserver

This commit is contained in:
2025-10-06 06:01:41 +02:00
parent 11cfd3e27f
commit 6c13e8cb8e
1229 changed files with 532967 additions and 140 deletions

79
pmoserver/src/lib.rs Normal file
View File

@@ -0,0 +1,79 @@
//! # pmoserver - Serveur web haut niveau basé sur Axum
//!
//! Cette crate fournit une abstraction simple et ergonomique pour créer des serveurs HTTP
//! avec Axum, spécialement conçue pour les applications UPnP et les serveurs multimédia.
//!
//! ## Fonctionnalités
//!
//! - 🚀 **API de haut niveau** : Interface simple pour créer des serveurs HTTP avec Axum
//! - 🎯 **Support UPnP** : Implémentation du trait `UpnpServer` pour connecter des devices UPnP
//! - 📡 **Server-Sent Events (SSE)** : Support intégré pour les logs en temps réel via SSE
//! - ⚛️ **Applications SPA** : Support pour servir des applications Single Page (Vue.js, React, etc.)
//! - 📁 **Fichiers statiques** : Serve de fichiers statiques avec `RustEmbed`
//! - 🔀 **Redirections** : Support pour les redirections HTTP
//! - 📚 **Documentation OpenAPI** : Génération automatique de Swagger UI
//! - ⚡ **Arrêt gracieux** : Gestion propre de l'arrêt sur Ctrl+C
//!
//! ## Architecture
//!
//! La crate est organisée en plusieurs modules :
//!
//! - [`server`] : Implémentation du serveur principal et du builder
//! - [`logs`] : Système de logs SSE pour monitoring en temps réel
//! - `upnp_impl` : Implémentation du trait `pmoupnp::UpnpServer` (privé)
//!
//! ## Exemple d'utilisation
//!
//! ```rust,no_run
//! use pmoserver::{ServerBuilder, logs::{LogState, SseLayer}};
//! use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
//!
//! #[tokio::main]
//! async fn main() {
//! // Configuration des logs avec SSE
//! let log_state = LogState::new();
//! tracing_subscriber::registry()
//! .with(SseLayer::new(log_state.clone()))
//! .init();
//!
//! // Création et démarrage du serveur
//! let mut server = ServerBuilder::new("MyServer")
//! .http_port(8080)
//! .build();
//!
//! // Ajout d'une route JSON
//! server.add_route("/api/status", || async {
//! serde_json::json!({"status": "ok"})
//! }).await;
//!
//! // Démarrage
//! server.start().await;
//! }
//! ```
//!
//! ## Intégration UPnP
//!
//! Le serveur implémente automatiquement le trait `pmoupnp::UpnpServer`, permettant
//! de connecter des devices UPnP :
//!
//! ```rust,no_run
//! use pmoupnp::{UpnpServer, mediarenderer::device::MEDIA_RENDERER};
//! use pmoupnp::devices::DeviceInstance;
//! use pmoserver::ServerBuilder;
//! use std::sync::Arc;
//!
//! # async fn example() {
//! let mut server = ServerBuilder::new("MediaRenderer").build();
//! let device = Arc::new(DeviceInstance::new(&MEDIA_RENDERER));
//!
//! // Le device enregistre automatiquement ses routes
//! device.register_urls(&mut server).await;
//! # }
//! ```
pub mod server;
pub mod logs;
mod upnp_impl;
pub use server::{Server, ServerBuilder, ServerInfo, Webapp};
pub use logs::{LogState, SseLayer, log_sse, log_dump};

159
pmoserver/src/logs/mod.rs Normal file
View File

@@ -0,0 +1,159 @@
// logs.rs
mod sselayer;
pub use sselayer::SseLayer;
use std::{
collections::VecDeque,
sync::{Arc, RwLock},
time::SystemTime,
};
use axum::{
Json,
extract::{Query, State},
response::{
IntoResponse,
sse::{Event, KeepAlive, Sse},
},
};
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
/// Représente une entrée de log
#[derive(Debug, Clone, Serialize)]
pub struct LogEntry {
pub timestamp: SystemTime,
pub level: String,
pub target: String,
pub message: String,
}
/// Buffer circulaire partagé
#[derive(Clone)]
pub struct LogState {
buffer: Arc<RwLock<VecDeque<LogEntry>>>,
tx: broadcast::Sender<LogEntry>,
}
impl LogState {
pub fn new(capacity: usize) -> Self {
Self {
buffer: Arc::new(RwLock::new(VecDeque::with_capacity(capacity))),
tx: broadcast::channel(1000).0,
}
}
fn push(&self, entry: LogEntry) {
let mut buf = self.buffer.write().unwrap();
if buf.len() == buf.capacity() {
buf.pop_front();
}
buf.push_back(entry.clone());
let _ = self.tx.send(entry);
}
pub fn subscribe(&self) -> broadcast::Receiver<LogEntry> {
self.tx.subscribe()
}
pub fn dump(&self) -> Vec<LogEntry> {
self.buffer.read().unwrap().iter().cloned().collect()
}
}
/// Query params pour /log-sse
#[derive(Debug, Deserialize)]
pub struct LogQuery {
#[serde(default)]
pub error: Option<bool>,
#[serde(default)]
pub warn: Option<bool>,
#[serde(default)]
pub info: Option<bool>,
#[serde(default)]
pub debug: Option<bool>,
#[serde(default)]
pub trace: Option<bool>,
#[serde(default)]
pub search: Option<String>,
}
/// Handler SSE
// Dans logs.rs
pub async fn log_sse(
State(state): State<LogState>,
Query(params): Query<LogQuery>,
) -> impl IntoResponse {
let mut rx = state.subscribe();
// Récupérer l'historique du buffer
let history = state.dump();
let stream = async_stream::stream! {
// 1. Envoyer d'abord tous les logs historiques
for entry in history {
if !filter_entry(&entry, &params) {
continue;
}
let json = serde_json::to_string(&entry).unwrap();
yield Ok::<_, axum::Error>(Event::default().data(json));
}
// 2. Puis streamer les nouveaux logs en temps réel
while let Ok(entry) = rx.recv().await {
if !filter_entry(&entry, &params) {
continue;
}
let json = serde_json::to_string(&entry).unwrap();
yield Ok::<_, axum::Error>(Event::default().data(json));
}
};
Sse::new(stream).keep_alive(KeepAlive::default())
}
/// Handler REST (dump JSON du buffer)
pub async fn log_dump(State(state): State<LogState>) -> impl IntoResponse {
Json(state.dump())
}
/// Fonction de filtrage
fn filter_entry(entry: &LogEntry, q: &LogQuery) -> bool {
// Filtrage par niveau
let lvl = entry.level.to_lowercase();
let mut allowed = false;
if let Some(true) = q.error {
allowed |= lvl == "error";
}
if let Some(true) = q.warn {
allowed |= lvl == "warn";
}
if let Some(true) = q.info {
allowed |= lvl == "info";
}
if let Some(true) = q.debug {
allowed |= lvl == "debug";
}
if let Some(true) = q.trace {
allowed |= lvl == "trace";
}
// si aucun flag → tout est autorisé
if !(q.error.unwrap_or(false)
|| q.warn.unwrap_or(false)
|| q.info.unwrap_or(false)
|| q.debug.unwrap_or(false)
|| q.trace.unwrap_or(false))
{
allowed = true;
}
// Filtrage par mot-clé
if let Some(search) = &q.search {
allowed &= entry.message.contains(search) || entry.target.contains(search);
}
allowed
}

View File

@@ -0,0 +1,63 @@
use tracing::field::{Field, Visit};
use tracing::{Event, Subscriber};
use tracing_subscriber::{Layer, layer::Context};
use super::{LogEntry, LogState};
use std::time::SystemTime;
struct LogVisitor {
message: String,
}
impl LogVisitor {
fn new() -> Self {
Self {
message: String::new(),
}
}
}
impl Visit for LogVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
// capture le champ "message" ou concatène les autres
if field.name() == "message" {
self.message = format!("{:?}", value);
} else {
if !self.message.is_empty() {
self.message.push(' ');
}
self.message
.push_str(&format!("{}={:?}", field.name(), value));
}
}
}
/// Layer de tracing qui pousse les events dans le buffer
pub struct SseLayer {
state: LogState,
}
impl SseLayer {
pub fn new(state: LogState) -> Self {
Self { state }
}
}
impl<S> Layer<S> for SseLayer
where
S: Subscriber,
{
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let mut visitor = LogVisitor::new();
event.record(&mut visitor);
let entry = LogEntry {
timestamp: SystemTime::now(),
level: event.metadata().level().to_string(),
target: event.metadata().target().to_string(),
message: visitor.message,
};
self.state.push(entry);
}
}

564
pmoserver/src/server.rs Normal file
View File

@@ -0,0 +1,564 @@
//! # 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()`
//! - 📚 **Documentation API** : OpenAPI/Swagger automatique avec `add_openapi()`
//! - ⚡ **Gestion gracieuse** : Arrêt propre sur Ctrl+C
use axum::handler::Handler;
use axum::response::Redirect;
use axum::routing::get;
use axum::{Json, Router};
use axum_embed::ServeEmbed;
use pmoconfig::get_config;
use rust_embed::RustEmbed;
use serde::Serialize;
use std::{net::SocketAddr, sync::Arc};
use tokio::{signal, sync::RwLock, task::JoinHandle};
use tracing::{info, warn, debug, error};
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
/// Info serveur sérialisable
#[derive(Clone, Serialize, utoipa::ToSchema)]
pub struct ServerInfo {
/// Nom du serveur
pub name: String,
/// URL de base
pub base_url: String,
/// Port HTTP
pub http_port: u16,
}
/// Serveur principal
pub struct Server {
name: String,
base_url: String,
http_port: u16,
router: Arc<RwLock<Router>>,
api_router: Arc<RwLock<Option<Router>>>,
join_handle: Option<JoinHandle<()>>,
}
#[derive(RustEmbed, Clone)]
#[folder = "webapp/dist"]
pub struct Webapp;
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
/// # use pmoupnp::server::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 {
Self {
name: name.into(),
base_url: base_url.into(),
http_port,
router: Arc::new(RwLock::new(Router::new())),
api_router: Arc::new(RwLock::new(None)),
join_handle: None,
}
}
pub fn new_configured() -> Self {
let config = get_config();
let url = config.get_base_url();
let port = config.get_http_port();
return Self::new("PMO-Music-Server", url, port);
}
/// 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
///
/// ```rust,no_run
/// # use pmoupnp::server::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,
Fut: std::future::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;
*r = std::mem::take(&mut *r).nest(path, route);
}
/// Ajoute un répertoire de fichiers statiques
///
/// Sert des fichiers embarqués via `RustEmbed`. Les fichiers sont compilés
/// dans le binaire à la compilation.
///
/// # Arguments
///
/// * `path` - Chemin où monter les fichiers statiques
///
/// # Type Parameter
///
/// * `E` - Type RustEmbed définissant le répertoire à servir
///
/// # Exemple
///
/// ```ignore
/// use pmoupnp::server::Server;
/// use rust_embed::RustEmbed;
///
/// #[derive(RustEmbed, Clone)]
/// #[folder = "static/"]
/// struct Assets;
///
/// # #[tokio::main]
/// # async fn main() {
/// let mut server = Server::new("Test", "http://localhost:3000", 3000);
/// server.add_dir::<Assets>("/assets").await;
/// // Les fichiers de static/ sont accessibles via /assets/*
/// # }
/// ```
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;
if path == "/" {
*r = std::mem::take(&mut *r).fallback_service(serve);
} else {
let route = Router::new().fallback_service(serve);
*r = 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
///
/// ```rust,no_run
/// # use pmoupnp::server::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()),
);
let mut r = self.router.write().await;
if path == "/" {
*r = std::mem::take(&mut *r).fallback_service(serve);
} else {
let route = Router::new().fallback_service(serve);
*r = std::mem::take(&mut *r).nest(path, route);
}
}
/// Ajoute un handler Axum personnalisé
///
/// Pour des cas d'usage avancés nécessitant un contrôle complet sur le handler.
///
/// # Arguments
///
/// * `path` - Chemin de la route
/// * `handler` - Handler Axum
///
/// # Exemple
///
/// ```rust,no_run
/// # use pmoupnp::server::Server;
/// # use axum::response::Html;
/// # #[tokio::main]
/// # async fn main() {
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
/// async fn custom_handler() -> Html<&'static str> {
/// Html("<h1>Custom Response</h1>")
/// }
///
/// server.add_handler("/custom", custom_handler).await;
/// # }
/// ```
pub async fn add_handler<H, T>(&mut self, path: &str, handler: H)
where
H: Handler<T, ()>,
T: 'static,
{
let route = Router::new().route("/", get(handler));
let mut r = self.router.write().await;
*r = std::mem::take(&mut *r).nest(path, route);
}
/// Ajoute un handler avec state (pour SSE, extracteurs, etc.)
///
/// Permet d'utiliser des extracteurs Axum comme `State`, `Query`, etc.
/// Idéal pour Server-Sent Events (SSE), WebSockets ou tout handler nécessitant un état partagé.
///
/// # Arguments
///
/// * `path` - Chemin de la route
/// * `handler` - Handler Axum avec extracteurs
/// * `state` - État partagé (doit être Clone + Send + Sync)
///
/// # Exemple avec SSE
///
/// ```ignore
/// use pmoupnp::server::Server;
/// use axum::extract::State;
/// use axum::response::sse::{Event, Sse, KeepAlive};
/// use tokio::sync::broadcast;
///
/// #[derive(Clone)]
/// struct LogState {
/// tx: broadcast::Sender<String>
/// }
///
/// impl LogState {
/// fn subscribe(&self) -> broadcast::Receiver<String> {
/// self.tx.subscribe()
/// }
/// }
///
/// async fn log_sse(State(state): State<LogState>) -> Sse<impl futures::Stream<Item = Result<Event, std::convert::Infallible>>> {
/// let mut rx = state.subscribe();
/// let stream = async_stream::stream! {
/// while let Ok(msg) = rx.recv().await {
/// yield Ok(Event::default().data(msg));
/// }
/// };
/// Sse::new(stream).keep_alive(KeepAlive::default())
/// }
///
/// let log_state = LogState { tx: broadcast::channel(100).0 };
/// server.add_handler_with_state("/logs", log_sse, log_state).await;
/// ```
pub async fn add_handler_with_state<H, T, S>(&mut self, path: &str, handler: H, state: S)
where
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static,
{
let route = Router::new()
.route("/", get(handler))
.with_state(state);
let mut r = self.router.write().await;
*r = std::mem::take(&mut *r).nest(path, route);
}
/// Ajoute un handler POST avec state
///
/// Similaire à `add_handler_with_state` mais pour les requêtes POST.
///
/// # Arguments
///
/// * `path` - Chemin de la route
/// * `handler` - Handler Axum pour POST
/// * `state` - État partagé
pub async fn add_post_handler_with_state<H, T, S>(&mut self, path: &str, handler: H, state: S)
where
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static,
{
let route = Router::new()
.route("/", axum::routing::post(handler))
.with_state(state);
let mut r = self.router.write().await;
*r = 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
///
/// ```rust,no_run
/// # use pmoupnp::server::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;
/// # }
/// ```
pub async fn add_redirect(&mut self, from: &str, to: &str) {
let to = to.to_string();
let handler = move || {
let to = to.clone();
async move { Redirect::permanent(&to) }
};
let mut r = self.router.write().await;
if from == "/" {
// Pour la racine, utiliser merge au lieu de nest
let route = Router::new().route("/", get(handler));
*r = std::mem::take(&mut *r).merge(route);
} else {
let route = Router::new().route("/", get(handler));
*r = std::mem::take(&mut *r).nest(from, route);
}
}
/// Ajoute une API documentée avec OpenAPI
///
/// Monte un routeur d'API sous `/api` et active Swagger UI sur `/swagger-ui`
///
/// # Arguments
///
/// * `api_router` - Router Axum contenant les routes API
/// * `openapi` - Spécification OpenAPI générée par utoipa
///
/// # Exemple
///
/// ```ignore
/// use utoipa::OpenApi;
/// use axum::{Router, Json, routing::get};
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize, utoipa::ToSchema)]
/// struct User {
/// id: u64,
/// name: String,
/// }
///
/// #[derive(utoipa::OpenApi)]
/// #[openapi(
/// paths(get_users),
/// components(schemas(User))
/// )]
/// struct ApiDoc;
///
/// #[utoipa::path(
/// get,
/// path = "/users",
/// responses((status = 200, description = "List users"))
/// )]
/// async fn get_users() -> Json<Vec<User>> {
/// Json(vec![])
/// }
///
/// let api_router = Router::new()
/// .route("/users", get(get_users));
///
/// server.add_openapi(api_router, ApiDoc::openapi()).await;
/// ```
pub async fn add_openapi(&mut self, api_router: Router, openapi: utoipa::openapi::OpenApi) {
// Stocker le routeur API
let mut api_r = self.api_router.write().await;
*api_r = Some(api_router);
// Ajouter Swagger UI
let swagger = SwaggerUi::new("/swagger-ui")
.url("/api-docs/openapi.json", openapi);
let mut r = self.router.write().await;
*r = std::mem::take(&mut *r).merge(swagger);
}
/// 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
///
/// ```rust,no_run
/// # use pmoupnp::server::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));
info!("Server {} running at [http://{}:{}](http://{}:{})", self.name, self.base_url, self.http_port, self.base_url, self.http_port);
// Merger le routeur API si présent
let api_router = self.api_router.read().await;
if let Some(api_r) = api_router.as_ref() {
let mut r = self.router.write().await;
*r = std::mem::take(&mut *r).nest("/api", api_r.clone());
}
drop(api_router);
let router = self.router.clone();
let server_task = tokio::spawn(async move {
let r = router.read().await.clone();
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, r.into_make_service()).await.unwrap();
});
let shutdown_task = tokio::spawn(async move {
signal::ctrl_c().await.expect("failed to listen for ctrl_c");
info!("Ctrl+C reçu, arrêt gracieux");
});
self.join_handle = Some(tokio::spawn(async move {
tokio::select! {
_ = server_task => {},
_ = shutdown_task => {},
}
}));
}
/// Attend la fin du serveur
pub async fn wait(&mut self) {
if let Some(h) = self.join_handle.take() {
let _ = h.await;
}
}
/// Récupère les infos du serveur
pub fn info(&self) -> ServerInfo {
ServerInfo {
name: self.name.clone(),
base_url: self.base_url.clone(),
http_port: self.http_port,
}
}
}
/// 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(),
http_port: config.get_http_port()
}
}
/// Construit le serveur
///
/// Consomme le builder et retourne une instance de `Server` prête à l'emploi.
///
/// # Exemple
///
/// ```rust
/// # use pmoupnp::server::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)
}
}

View File

@@ -0,0 +1,94 @@
//! Implémentation du trait UpnpServer pour le serveur pmoserver
//!
//! Ce module fournit l'implémentation du trait [`pmoupnp::UpnpServer`] pour
//! le [`Server`](crate::server::Server) de pmoserver, permettant aux devices
//! et services UPnP d'enregistrer automatiquement leurs endpoints HTTP.
//!
//! ## Architecture
//!
//! L'implémentation fait le pont entre :
//! - Les pointeurs de fonction du trait `UpnpServer` (agnostiques du framework web)
//! - Les handlers Axum (spécifiques à l'implémentation `pmoserver`)
//!
//! Chaque méthode du trait crée un wrapper qui :
//! 1. Convertit les pointeurs de fonction en closures compatibles Axum
//! 2. Délègue l'enregistrement aux méthodes internes du `Server`
//! 3. Retourne une future qui se résout une fois le handler enregistré
//!
//! ## Exemple d'utilisation
//!
//! ```rust,no_run
//! use pmoupnp::{UpnpServer, mediarenderer::device::MEDIA_RENDERER};
//! use pmoupnp::devices::DeviceInstance;
//! use pmoserver::ServerBuilder;
//! use std::sync::Arc;
//!
//! # async fn example() {
//! let mut server = ServerBuilder::new("MyRenderer").build();
//! let device = Arc::new(DeviceInstance::new(&MEDIA_RENDERER));
//!
//! // Le trait UpnpServer est automatiquement disponible
//! device.register_urls(&mut server).await;
//! # }
//! ```
use crate::server::Server;
use pmoupnp::{UpnpServer, server::{Response, HeaderMap, Request}};
use std::future::Future;
use std::pin::Pin;
use axum::extract::State;
impl UpnpServer for Server {
fn add_handler<F, Fut>(&mut self, path: &str, handler: F) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
F: Fn() -> Fut + Send + Sync + 'static + Clone,
Fut: Future<Output = Response> + Send + 'static,
{
let path = path.to_string();
Box::pin(async move {
Self::add_handler(self, &path, handler).await;
})
}
fn add_post_handler_with_state<S>(
&mut self,
path: &str,
handler: fn(State<S>, String) -> Pin<Box<dyn Future<Output = Response> + Send>>,
state: S,
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
S: Clone + Send + Sync + 'static,
{
let path = path.to_string();
// Créer un wrapper qui convertit le fn pointer en handler Axum
let wrapper = move |State(s): State<S>, body: String| -> Pin<Box<dyn Future<Output = Response> + Send>> {
handler(State(s), body)
};
Box::pin(async move {
Self::add_post_handler_with_state(self, &path, wrapper, state).await;
})
}
fn add_handler_with_state<S>(
&mut self,
path: &str,
handler: fn(State<S>, HeaderMap, Request) -> Pin<Box<dyn Future<Output = Response> + Send>>,
state: S,
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
S: Clone + Send + Sync + 'static,
{
let path = path.to_string();
// Créer un wrapper qui convertit le fn pointer en handler Axum
let wrapper = move |State(s): State<S>, headers: HeaderMap, req: Request| -> Pin<Box<dyn Future<Output = Response> + Send>> {
handler(State(s), headers, req)
};
Box::pin(async move {
Self::add_handler_with_state(self, &path, wrapper, state).await;
})
}
}