Sort le serveur dans une crate pmoserver

This commit is contained in:
2025-10-06 06:01:41 +02:00
parent a648931960
commit 87859397df
20 changed files with 606 additions and 140 deletions

133
Cargo.lock generated
View File

@@ -8,6 +8,7 @@ version = "0.1.0"
dependencies = [
"axum",
"pmoconfig",
"pmoserver",
"pmoupnp",
"serde_json",
"tokio",
@@ -467,41 +468,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
@@ -1129,12 +1095,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.1.0"
@@ -1511,41 +1471,50 @@ dependencies = [
]
[[package]]
name = "pmoupnp"
name = "pmoserver"
version = "0.1.0"
dependencies = [
"anyhow",
"async-stream",
"axum",
"axum-embed",
"axum-server",
"futures",
"futures-util",
"pmoconfig",
"pmoupnp",
"rust-embed",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tracing",
"tracing-subscriber",
"utoipa",
"utoipa-swagger-ui",
]
[[package]]
name = "pmoupnp"
version = "0.1.0"
dependencies = [
"axum",
"base64",
"bevy_reflect",
"bevy_reflect_derive",
"chrono",
"futures",
"futures-util",
"get_if_addrs",
"hex",
"log",
"once_cell",
"parking_lot",
"pmoconfig",
"pmodidl",
"reqwest",
"rust-embed",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
"tracing-subscriber",
"url",
"utoipa",
"utoipa-swagger-ui",
"uuid",
"validator",
"xmltree",
]
@@ -1580,28 +1549,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
@@ -2050,12 +1997,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
@@ -2492,36 +2433,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "validator"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
dependencies = [
"idna",
"once_cell",
"regex",
"serde",
"serde_derive",
"serde_json",
"url",
"validator_derive",
]
[[package]]
name = "validator_derive"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
dependencies = [
"darling",
"once_cell",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "valuable"
version = "0.1.1"

View File

@@ -1,3 +1,3 @@
[workspace]
resolver = "3"
members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils", "pmodidl"]
members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils", "pmodidl", "pmoserver"]

View File

@@ -6,6 +6,7 @@ edition = "2024"
[dependencies]
pmoconfig = { path = "../pmoconfig" }
pmoupnp = { path = "../pmoupnp"}
pmoserver = { path = "../pmoserver" }
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time","signal"] }

View File

@@ -1,6 +1,8 @@
use pmoupnp::{mediarenderer::avtransport::{actions::SETAVTRANSPORTURI, AVTTRANSPORT}, server::{
logs::{log_dump, log_sse, LogState, SseLayer}, ServerBuilder, Webapp
}, UpnpObject}; // ton module pmoupnp::server
use pmoupnp::{mediarenderer::avtransport::AVTTRANSPORT, UpnpObject};
use pmoserver::{
logs::{log_dump, log_sse, LogState, SseLayer},
ServerBuilder, Webapp
};
use tracing_subscriber::Registry;
use tracing_subscriber::prelude::*;
use tracing::info;

27
pmoserver/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "pmoserver"
version = "0.1.0"
edition = "2024"
[dependencies]
pmoconfig = { path = "../pmoconfig" }
axum = "0.8.4"
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time", "signal"] }
tokio-stream = "0.1"
futures-util = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
futures = "0.3"
async-stream = "0.3.6"
axum-server = "0.7.2"
axum-embed = "0.1.0"
rust-embed = "8.7.2"
utoipa = { version = "5.4.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
[dependencies.pmoupnp]
path = "../pmoupnp"
default-features = false

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

View File

@@ -13,8 +13,6 @@
//! - 📚 **Documentation API** : OpenAPI/Swagger automatique avec `add_openapi()`
//! - ⚡ **Gestion gracieuse** : Arrêt propre sur Ctrl+C
pub mod logs;
use axum::handler::Handler;
use axum::response::Redirect;
use axum::routing::get;

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;
})
}
}

0
pmoserver/webapp/dist/.gitkeep vendored Normal file
View File

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

14
pmoserver/webapp/dist/index.html vendored Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/app/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webapp</title>
<script type="module" crossorigin src="/app/assets/index-B3fd9aU-.js"></script>
<link rel="stylesheet" crossorigin href="/app/assets/index-Qw8Jzbm-.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

1
pmoserver/webapp/dist/vite.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -13,28 +13,14 @@ hex = "0.4.3"
base64 = "0.22.1"
thiserror = "2.0.16"
xmltree = "0.11.0"
get_if_addrs = "0.5.3"
axum = "0.8.4"
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time", "signal"] }
tokio-stream = "0.1"
futures-util = "0.3"
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4.42", features = ["serde"] }
log = "0.4.28"
once_cell = "1.20"
parking_lot = "0.12"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
futures = "0.3"
async-stream = "0.3.6"
axum-server = "0.7.2"
axum-embed = "0.1.0"
rust-embed = "8.7.2"
anyhow = "1.0"
utoipa = { version = "5.4.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
validator = { version = "0.20.0", features = ["derive"] }
bevy_reflect = "0.17.1"
bevy_reflect_derive = "0.17.1"
reqwest = "0.12.23"

View File

@@ -245,7 +245,7 @@ impl DeviceInstance {
}
/// Enregistre toutes les URLs du device et de ses services dans le serveur.
pub fn register_urls<'a>(&'a self, server: &'a mut crate::server::Server) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), DeviceError>> + 'a>> {
pub fn register_urls<'a, S: crate::UpnpServer + ?Sized>(&'a self, server: &'a mut S) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), DeviceError>> + 'a>> {
Box::pin(async move {
info!(
"✅ Device description for {} available at: {}{}",

View File

@@ -10,6 +10,9 @@ pub mod state_variables;
pub mod value_ranges;
pub mod variable_types;
// Re-exports
pub use server::UpnpServer;
use std::{collections::HashMap, sync::Arc};

262
pmoupnp/src/server.rs Normal file
View File

@@ -0,0 +1,262 @@
//! Trait pour les serveurs UPnP
//!
//! Ce module définit le trait [`UpnpServer`] qui permet de connecter
//! des devices UPnP à n'importe quelle implémentation de serveur web.
//!
//! ## Architecture
//!
//! Le trait `UpnpServer` définit une interface minimale permettant aux devices
//! et services UPnP d'enregistrer leurs endpoints HTTP sans dépendre d'une
//! implémentation de serveur spécifique.
//!
//! ## Séparation des responsabilités
//!
//! - **pmoupnp** : Définit le trait `UpnpServer` et l'utilise via des contraintes génériques
//! - **pmoserver** : Fournit une implémentation concrète basée sur Axum
//! - **Autres crates** : Peuvent fournir leurs propres implémentations (actix-web, warp, etc.)
//!
//! ## Exemple d'utilisation
//!
//! ```rust,no_run
//! use pmoupnp::{UpnpServer, devices::{Device, DeviceInstance}};
//! use std::sync::Arc;
//!
//! # async fn example<S: UpnpServer>(mut server: S) {
//! // Créer un device
//! let device = Device::new(
//! "MyDevice".to_string(),
//! "MyDeviceType".to_string(),
//! "Friendly Name".to_string(),
//! );
//! let device_instance = Arc::new(DeviceInstance::new(&device));
//!
//! // Le device enregistre automatiquement ses routes UPnP
//! device_instance.register_urls(&mut server).await;
//! # }
//! ```
//!
//! ## Implémentation
//!
//! Pour implémenter ce trait, votre serveur doit fournir trois méthodes
//! pour enregistrer des handlers HTTP asynchrones :
//!
//! ```rust,no_run
//! use pmoupnp::UpnpServer;
//! use std::future::Future;
//! use std::pin::Pin;
//!
//! struct MyServer {
//! // votre implémentation
//! }
//!
//! impl UpnpServer for MyServer {
//! 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 = pmoupnp::server::Response> + Send + 'static,
//! {
//! // Enregistrer le handler pour GET requests
//! # todo!()
//! }
//!
//! fn add_post_handler_with_state<S>(
//! &mut self,
//! path: &str,
//! handler: fn(axum::extract::State<S>, String)
//! -> Pin<Box<dyn Future<Output = pmoupnp::server::Response> + Send>>,
//! state: S,
//! ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
//! where
//! S: Clone + Send + Sync + 'static,
//! {
//! // Enregistrer le handler pour POST avec body
//! # todo!()
//! }
//!
//! fn add_handler_with_state<S>(
//! &mut self,
//! path: &str,
//! handler: fn(axum::extract::State<S>,
//! pmoupnp::server::HeaderMap,
//! pmoupnp::server::Request)
//! -> Pin<Box<dyn Future<Output = pmoupnp::server::Response> + Send>>,
//! state: S,
//! ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
//! where
//! S: Clone + Send + Sync + 'static,
//! {
//! // Enregistrer le handler avec accès complet à la requête
//! # todo!()
//! }
//! }
//! ```
use std::future::Future;
use std::pin::Pin;
/// Type alias pour la réponse HTTP (basé sur Axum).
///
/// Utilisé pour éviter une dépendance directe sur axum dans les signatures de trait,
/// tout en restant compatible avec les types Axum.
pub type Response = axum::response::Response;
/// Type alias pour les en-têtes HTTP (basé sur Axum).
pub type HeaderMap = axum::http::HeaderMap;
/// Type alias pour la requête HTTP (basé sur Axum).
pub type Request = axum::extract::Request<axum::body::Body>;
/// Trait pour les serveurs compatibles UPnP.
///
/// Ce trait définit l'interface minimale qu'un serveur web doit implémenter
/// pour supporter l'enregistrement automatique des endpoints UPnP par les
/// [`DeviceInstance`](crate::devices::DeviceInstance) et
/// [`ServiceInstance`](crate::services::ServiceInstance).
///
/// ## Contraintes
///
/// - `Send + Sync` : Le serveur doit être partageable entre threads
///
/// ## Méthodes
///
/// Les trois méthodes permettent d'enregistrer différents types de handlers :
///
/// 1. **`add_handler`** : Handler GET simple sans état
/// 2. **`add_post_handler_with_state`** : Handler POST avec état et body texte (pour SOAP)
/// 3. **`add_handler_with_state`** : Handler générique avec accès complet (pour SUBSCRIBE/UNSUBSCRIBE)
///
/// ## Implémentations
///
/// - **pmoserver::Server** : Implémentation basée sur Axum (fournie par la crate `pmoserver`)
pub trait UpnpServer: Send + Sync {
/// Ajoute un handler GET pour un chemin donné.
///
/// Utilisé principalement pour servir les descripteurs XML des devices et services.
///
/// # Arguments
///
/// * `path` - Le chemin HTTP (ex: `/device/MediaRenderer/description.xml`)
/// * `handler` - Une closure asynchrone qui génère la réponse
///
/// # Retour
///
/// Une future qui se résout quand le handler est enregistré.
///
/// # Exemple
///
/// ```rust,no_run
/// use pmoupnp::UpnpServer;
/// use axum::response::IntoResponse;
///
/// # async fn example<S: UpnpServer>(mut server: S) {
/// server.add_handler("/description.xml", || async {
/// "<?xml version=\"1.0\"?><root></root>".into_response()
/// }).await;
/// # }
/// ```
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;
/// Ajoute un handler POST avec état pour un chemin donné.
///
/// Utilisé pour les endpoints de contrôle SOAP des services UPnP.
///
/// # Arguments
///
/// * `path` - Le chemin HTTP (ex: `/service/AVTransport/control`)
/// * `handler` - Un pointeur de fonction qui traite la requête SOAP
/// * `state` - L'état partagé (typiquement une `ServiceInstance`)
///
/// # Retour
///
/// Une future qui se résout quand le handler est enregistré.
///
/// # Exemple
///
/// ```rust,no_run
/// use pmoupnp::{UpnpServer, server::Response};
/// use axum::extract::State;
/// use std::pin::Pin;
/// use std::future::Future;
///
/// fn soap_handler(
/// State(service): State<String>,
/// body: String,
/// ) -> Pin<Box<dyn Future<Output = Response> + Send>> {
/// Box::pin(async move {
/// // Traiter la requête SOAP
/// axum::response::Response::default()
/// })
/// }
///
/// # async fn example<S: UpnpServer>(mut server: S) {
/// server.add_post_handler_with_state(
/// "/control",
/// soap_handler,
/// "ServiceName".to_string(),
/// ).await;
/// # }
/// ```
fn add_post_handler_with_state<S>(
&mut self,
path: &str,
handler: fn(axum::extract::State<S>, String) -> Pin<Box<dyn Future<Output = Response> + Send>>,
state: S,
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
S: Clone + Send + Sync + 'static;
/// Ajoute un handler avec état et accès complet à la requête.
///
/// Utilisé pour les endpoints d'événements (SUBSCRIBE/UNSUBSCRIBE) qui nécessitent
/// un accès aux en-têtes HTTP et à la méthode HTTP.
///
/// # Arguments
///
/// * `path` - Le chemin HTTP (ex: `/service/AVTransport/event`)
/// * `handler` - Un pointeur de fonction avec accès complet à la requête
/// * `state` - L'état partagé (typiquement une `ServiceInstance`)
///
/// # Retour
///
/// Une future qui se résout quand le handler est enregistré.
///
/// # Exemple
///
/// ```rust,no_run
/// use pmoupnp::{UpnpServer, server::{Response, HeaderMap, Request}};
/// use axum::extract::State;
/// use std::pin::Pin;
/// use std::future::Future;
///
/// fn event_handler(
/// State(service): State<String>,
/// headers: HeaderMap,
/// req: Request,
/// ) -> Pin<Box<dyn Future<Output = Response> + Send>> {
/// Box::pin(async move {
/// // Traiter SUBSCRIBE/UNSUBSCRIBE
/// axum::response::Response::default()
/// })
/// }
///
/// # async fn example<S: UpnpServer>(mut server: S) {
/// server.add_handler_with_state(
/// "/event",
/// event_handler,
/// "ServiceName".to_string(),
/// ).await;
/// # }
/// ```
fn add_handler_with_state<S>(
&mut self,
path: &str,
handler: fn(axum::extract::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;
}

View File

@@ -4,6 +4,8 @@ use std::{
collections::HashMap,
sync::{Arc, Mutex, RwLock},
time::Duration,
pin::Pin,
future::Future,
};
use axum::{
extract::{Request, State},
@@ -297,12 +299,12 @@ impl ServiceInstance {
&self.actions
}
/// Enregistre les routes UPnP dans le serveur Axum.
/// Enregistre les routes UPnP dans le serveur.
///
/// # Errors
///
/// Retourne une erreur si l'enregistrement des routes échoue.
pub async fn register_urls(&self, server: &mut crate::server::Server) -> Result<(), ServiceError> {
pub async fn register_urls<S: crate::UpnpServer + ?Sized>(&self, server: &mut S) -> Result<(), ServiceError> {
let device = self.device.read().unwrap();
let device_name = device.as_ref().map(|d| d.get_name().clone()).unwrap_or_else(|| "unknown".to_string());
let server_url = device.as_ref().map(|d| d.base_url().to_string()).unwrap_or_default();
@@ -566,11 +568,12 @@ impl ServiceInstance {
}
/// Handler Axum pour les événements (SUBSCRIBE/UNSUBSCRIBE).
async fn event_sub_handler(
fn event_sub_handler(
State(instance): State<ServiceInstance>,
headers: HeaderMap,
req: Request<Body>,
) -> Response {
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
Box::pin(async move {
info!("📡 Event Subscription request for {}", instance.get_name());
let method = req.method().as_str();
@@ -633,13 +636,15 @@ async fn event_sub_handler(
StatusCode::METHOD_NOT_ALLOWED.into_response()
}
}
})
}
/// Handler Axum pour le contrôle SOAP.
async fn control_handler(
fn control_handler(
State(instance): State<ServiceInstance>,
body: String,
) -> Response {
_body: String,
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
Box::pin(async move {
info!("📡 Control request for {}", instance.get_name());
// TODO: Parser le SOAP et appeler l'action correspondante
@@ -661,6 +666,7 @@ async fn control_handler(
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
response_xml,
).into_response()
})
}
#[cfg(test)]