diff --git a/.gitignore b/.gitignore index 84d34c96..b586d73f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .ollama +.ollamacode /bin/ /pkg/ /vendor/ diff --git a/pmoupnp/src/actions/action_handler.rs b/pmoupnp/src/actions/action_handler.rs new file mode 100644 index 00000000..1007a205 --- /dev/null +++ b/pmoupnp/src/actions/action_handler.rs @@ -0,0 +1,314 @@ +//! Types et utilitaires pour les handlers d'actions UPnP. +//! +//! Ce module définit les types fondamentaux pour gérer l'exécution +//! asynchrone des actions UPnP. +//! +//! # Architecture +//! +//! Les actions UPnP sont exécutées de manière asynchrone via des handlers +//! qui prennent des données en entrée et retournent des données en sortie. +//! +//! ```text +//! ActionData (input) +//! ↓ +//! ActionHandler (async processing) +//! ↓ +//! ActionData (output) +//! ``` +//! +//! # Examples +//! +//! ```rust +//! use pmoupnp::action_handler; +//! use std::collections::HashMap; +//! use std::sync::Arc; +//! +//! // Créer un handler avec la macro +//! let handler = action_handler!(|data| { +//! // Traiter les données +//! data +//! }); +//! +//! // Ou manuellement +//! use pmoupnp::actions::{ActionData, ActionHandler}; +//! let manual_handler: ActionHandler = Arc::new(|data| { +//! Box::pin(async move { +//! data +//! }) +//! }); +//! ``` + +use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; + +use crate::variable_types::StateValue; + +/// Données d'une action UPnP. +/// +/// Représente un ensemble de paramètres clé-valeur pour une action UPnP, +/// partagé via `Arc` pour permettre un clonage efficace. +/// +/// # Structure +/// +/// - **Clé** : Nom du paramètre (ex: "InstanceID", "TransportURI") +/// - **Valeur** : Valeur typée du paramètre ([`StateValue`]) +/// +/// # Exemples +/// +/// ```rust +/// use pmoupnp::actions::ActionData; +/// use pmoupnp::variable_types::StateValue; +/// use std::collections::HashMap; +/// use std::sync::Arc; +/// +/// let mut data = HashMap::new(); +/// data.insert("InstanceID".to_string(), StateValue::UI4(0)); +/// data.insert("Speed".to_string(), StateValue::String("1".to_string())); +/// +/// let action_data: ActionData = Arc::new(data); +/// +/// // Le Arc permet un clonage efficace +/// let cloned = action_data.clone(); +/// ``` +/// +/// # Notes +/// +/// - Utilise `Arc` pour éviter les copies coûteuses +/// - Thread-safe grâce à `Arc` +/// - Les valeurs sont immuables une fois créées +pub type ActionData = Arc>; + +/// Future retourné par un [`ActionHandler`]. +/// +/// Ce type représente le résultat asynchrone d'un handler d'action. +/// Il est boxé et pinné pour permettre le polymorphisme et la manipulation +/// sûre des futures. +/// +/// # Type complet +/// +/// ```ignore +/// Pin> + Send>> +/// ``` +/// +/// # Composants +/// +/// - `Pin>` : Permet de déplacer le future en mémoire sans invalidation +/// - `dyn Future>` : Future retournant un Result +/// - `+ Send` : Le future peut être envoyé entre threads +/// +/// # Notes +/// +/// - Les handlers retournent `Ok(())` en cas de succès ou `Err(ActionError)` en cas d'erreur +/// - Ils modifient les variables d'instance et [`ActionInstance::run()`](crate::actions::ActionInstance::run) +/// collecte automatiquement les valeurs OUT si le handler réussit +/// - Rarement utilisé directement (la macro `action_handler!` s'en charge) +/// - Nécessaire pour la compatibilité avec les trait objects +pub type ActionFuture = Pin> + Send>>; + +/// Handler d'action UPnP asynchrone. +/// +/// Un `ActionHandler` est une fonction asynchrone partageable qui exécute +/// la logique métier d'une action sans retourner de valeur. +/// +/// # Signature +/// +/// ```ignore +/// Fn(Arc, ActionData) -> ActionFuture +/// ``` +/// +/// Prend : +/// - [`Arc`](crate::actions::ActionInstance) : L'instance de l'action avec accès aux variables liées +/// - [`ActionData`] : Les données d'entrée (arguments IN) +/// +/// Retourne un [`ActionFuture`] qui se résout en `Result<(), ActionError>`. +/// +/// # Responsabilités +/// +/// Le handler est responsable de : +/// - Lire les arguments d'entrée depuis `data` +/// - Exécuter la logique métier +/// - Modifier les variables d'instance selon les besoins +/// - Retourner `Ok(())` en cas de succès ou `Err(ActionError)` en cas d'erreur +/// +/// La méthode [`ActionInstance::run()`](crate::actions::ActionInstance::run) s'occupe +/// automatiquement de collecter les valeurs OUT si le handler retourne `Ok(())`. +/// +/// # Traits requis +/// +/// - `Send` : Le handler peut être envoyé entre threads +/// - `Sync` : Le handler peut être partagé entre threads +/// - `Arc` : Permet le partage sans copie +/// +/// # Création +/// +/// ## Avec la macro (recommandé) +/// +/// ```rust +/// use pmoupnp::action_handler; +/// +/// let handler = action_handler!(|instance, data| { +/// // Logique métier - pas besoin de retourner quoi que ce soit +/// }); +/// ``` +/// +/// ## Manuellement +/// +/// ```rust,no_run +/// use pmoupnp::actions::{ActionData, ActionHandler, ActionInstance}; +/// use std::sync::Arc; +/// +/// let handler: ActionHandler = Arc::new(|instance, data| { +/// Box::pin(async move { +/// // Votre logique async +/// // Pas de return nécessaire +/// }) +/// }); +/// ``` +/// +/// # Notes d'implémentation +/// +/// - Le handler ne retourne rien - il modifie les variables d'instance +/// - [`ActionInstance::run()`](crate::actions::ActionInstance::run) collecte automatiquement les OUT +/// - Le handler capture les variables par `move` +/// - Le future est automatiquement `Send` si les captures le sont +/// - Utilisez la macro `action_handler!` pour simplifier la création +pub type ActionHandler = Arc, ActionData) -> ActionFuture + Send + Sync>; + +/// Macro pour créer facilement un ActionHandler. +/// +/// Cette macro simplifie la création d'handlers asynchrones en cachant +/// la complexité de `Arc`, `Box::pin`, et `async move`. +/// +/// # Syntaxe +/// +/// ```ignore +/// action_handler!(|instance, data| { +/// // votre logique async (automatiquement dans un bloc async move) +/// data +/// }) +/// ``` +/// +/// # Arguments +/// +/// - `instance` : Paramètre de type `Arc<`[`ActionInstance`](crate::actions::ActionInstance)`>` - L'instance de l'action +/// - `data` : Paramètre de type [`ActionData`] (Arc>) - Les données d'entrée +/// - Le corps du bloc peut contenir du code asynchrone (`.await`) +/// +/// # Type de retour +/// +/// La macro retourne un [`ActionHandler`] prêt à l'emploi. +/// +/// # Examples +/// +/// ## Exemple 1 : Handler simple (ne fait rien) +/// +/// ```ignore +/// use pmoupnp::action_handler; +/// +/// // Handler minimal - run() collectera automatiquement les OUT +/// let handler = action_handler!(|instance, data| { +/// Ok(()) // Succès, pas d'erreur +/// }); +/// ``` +/// +/// ## Exemple 2 : Handler qui modifie une variable avec gestion d'erreur +/// +/// ```ignore +/// use pmoupnp::action_handler; +/// use pmoupnp::actions::ActionError; +/// +/// let handler = action_handler!(|instance, data| { +/// // Lire un argument d'entrée +/// let volume = data.get("DesiredVolume") +/// .ok_or_else(|| ActionError::MissingArgument("DesiredVolume".to_string()))?; +/// +/// // Modifier la variable d'instance +/// let arg = instance.argument("CurrentVolume") +/// .ok_or_else(|| ActionError::ArgumentNotFound("CurrentVolume".to_string()))?; +/// +/// let var = arg.get_variable_instance() +/// .ok_or_else(|| ActionError::VariableNotBound)?; +/// +/// var.set_value(volume.clone()); +/// +/// Ok(()) // Succès - run() collectera CurrentVolume dans les OUT +/// }); +/// ``` +/// +/// ## Exemple 3 : Handler avec logique métier asynchrone et gestion d'erreur +/// +/// ```ignore +/// use pmoupnp::action_handler; +/// use pmoupnp::actions::ActionError; +/// +/// let handler = action_handler!(|instance, data| { +/// // Appel asynchrone à un service externe +/// let response = external_service::fetch_data().await +/// .map_err(|e| ActionError::ExternalError(e.to_string()))?; +/// +/// // Mettre à jour les variables selon la réponse +/// if let Some(arg) = instance.argument("Status") { +/// if let Some(var) = arg.get_variable_instance() { +/// var.set_value(StateValue::String(response.status)); +/// } +/// } +/// +/// if let Some(arg) = instance.argument("Message") { +/// if let Some(var) = arg.get_variable_instance() { +/// var.set_value(StateValue::String(response.message)); +/// } +/// } +/// +/// Ok(()) +/// }); +/// ``` +/// +/// ## Exemple 4 : Handler avec capture de contexte et validation +/// +/// ```ignore +/// use pmoupnp::action_handler; +/// use pmoupnp::actions::ActionError; +/// use std::sync::Arc; +/// use tokio::sync::Mutex; +/// +/// // Contexte partagé (ex: état d'un lecteur média) +/// let player_state = Arc::new(Mutex::new(PlayerState::Stopped)); +/// +/// let handler = action_handler!(|instance, data| { +/// // Vérifier l'état actuel +/// { +/// let state = player_state.lock().await; +/// if *state == PlayerState::Error { +/// return Err(ActionError::InvalidState("Player in error state".to_string())); +/// } +/// } +/// +/// // Modifier l'état du lecteur +/// { +/// let mut state = player_state.lock().await; +/// *state = PlayerState::Playing; +/// } +/// +/// // Mettre à jour la variable TransportState +/// if let Some(arg) = instance.argument("CurrentTransportState") { +/// if let Some(var) = arg.get_variable_instance() { +/// var.set_value(StateValue::String("PLAYING".to_string())); +/// } +/// } +/// +/// Ok(()) +/// }); +/// ``` +/// +/// # Notes d'implémentation +/// +/// - Le bloc est automatiquement wrappé dans `async move` +/// - Les captures de variables sont déplacées (`move`) +/// - Le résultat est automatiquement boxé et arcé +#[macro_export] +macro_rules! action_handler { + (|$instance:ident, $data:ident| $body:block) => { + std::sync::Arc::new(|$instance: std::sync::Arc<$crate::actions::ActionInstance>, $data: $crate::actions::ActionData| { + Box::pin(async move $body) + }) + }; +} \ No newline at end of file diff --git a/pmoupnp/src/actions/action_instance.rs b/pmoupnp/src/actions/action_instance.rs index 02e95fd7..fb973ea8 100644 --- a/pmoupnp/src/actions/action_instance.rs +++ b/pmoupnp/src/actions/action_instance.rs @@ -1,15 +1,21 @@ use std::sync::Arc; +use tracing::debug; use xmltree::{Element, XMLNode}; -use crate::actions::Action; -use crate::actions::ArgInstanceSet; -use crate::actions::ActionInstance; -use crate::UpnpInstance; -use crate::UpnpObject; -use crate::UpnpTyped; -use crate::UpnpTypedInstance; -use crate::UpnpObjectType; +use crate::{ + UpnpInstance, + UpnpObject, + UpnpObjectType, + UpnpTyped, + UpnpTypedInstance, +}; +use crate::actions::{ + Action, + ActionData, + ActionInstance, + ArgInstanceSet, +}; impl UpnpObject for ActionInstance { fn to_xml_element(&self) -> Element { @@ -102,6 +108,136 @@ impl ActionInstance { pub fn arguments_set(&self) -> &ArgInstanceSet { &self.arguments // ⬅️ Retourne les INSTANCES, pas les modèles ! } + + /// Récupère les valeurs de tous les arguments de sortie (OUT). + /// + /// Cette méthode collecte automatiquement les valeurs actuelles de toutes + /// les variables d'état liées aux arguments OUT et les retourne dans un + /// [`ActionData`] indexé par le nom de chaque argument. + /// + /// # Returns + /// + /// Un [`ActionData`] contenant les paires (nom_argument, valeur_variable) pour + /// tous les arguments de sortie qui ont une variable d'instance liée. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use pmoupnp::actions::Action; + /// # use pmoupnp::UpnpInstance; + /// # use std::sync::Arc; + /// let action = Action::new("GetVolume".to_string()); + /// let instance = Arc::new(ActionInstance::new(&action)); + /// + /// // Récupérer automatiquement toutes les valeurs OUT + /// let output = instance.get_out_values(); + /// + /// // Afficher les résultats + /// for (arg_name, value) in output.iter() { + /// println!("{} = {:?}", arg_name, value); + /// } + /// ``` + /// + /// # Notes + /// + /// - Seuls les arguments marqués comme OUT sont inclus + /// - Les arguments sans variable d'instance liée sont ignorés + /// - Le nom de l'argument (pas le nom de la variable) est utilisé comme clé + /// - Cette méthode est utilisée par le handler par défaut + pub fn get_out_values(&self) -> ActionData { + use std::collections::HashMap; + use crate::UpnpTypedInstance; + + let mut result = HashMap::new(); + + for arg_inst in self.arguments.all() { + let arg_model = arg_inst.as_ref().get_model(); + if arg_model.is_out() { + if let Some(var_inst) = arg_inst.get_variable_instance() { + result.insert(arg_inst.get_name().to_string(), var_inst.value()); + } + } + } + + Arc::new(result) + } + + /// Exécute l'action avec les données fournies. + /// + /// Cette méthode : + /// 1. Exécute le handler avec les données d'entrée + /// 2. Collecte automatiquement les valeurs OUT via [`get_out_values()`](Self::get_out_values) + /// 3. Retourne les résultats + /// + /// # Arguments + /// + /// * `data` - Données d'entrée de l'action (arguments IN) + /// + /// # Returns + /// + /// Un `Future` qui se résout en `Result` : + /// - `Ok(ActionData)` contenant les résultats (arguments OUT) si le handler réussit + /// - `Err(ActionError)` si le handler échoue + /// + /// # Errors + /// + /// Retourne une erreur si le handler retourne `Err(ActionError)`. + /// + /// # Fonctionnement + /// + /// Le handler n'a pas besoin de retourner les valeurs OUT - il modifie simplement + /// les variables d'instance et retourne `Ok(())`. La méthode `run()` collecte automatiquement + /// toutes les valeurs des arguments marqués comme OUT si le handler réussit. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use pmoupnp::actions::{Action, ActionData}; + /// # use pmoupnp::UpnpInstance; + /// # use std::collections::HashMap; + /// # use std::sync::Arc; + /// # async fn example() { + /// let action = Action::new("SetVolume".to_string()); + /// let instance = Arc::new(ActionInstance::new(&action)); + /// + /// // Préparer les données d'entrée + /// let mut input = HashMap::new(); + /// input.insert("DesiredVolume".to_string(), + /// pmoupnp::variable_types::StateValue::UI2(50)); + /// let input_data = Arc::new(input); + /// + /// // Exécuter l'action - le handler modifie CurrentVolume + /// // run() retourne automatiquement CurrentVolume dans les OUT + /// match instance.run(input_data).await { + /// Ok(output_data) => { + /// // Traiter les résultats + /// for (key, value) in output_data.iter() { + /// println!("{} = {:?}", key, value); + /// } + /// } + /// Err(e) => { + /// eprintln!("Action failed: {:?}", e); + /// } + /// } + /// # } + /// ``` + /// + /// # Notes + /// + /// - Le handler modifie les variables et retourne `Ok(())` ou `Err(ActionError)` + /// - `run()` collecte automatiquement les OUT si le handler retourne `Ok(())` + /// - L'instance doit être wrappée dans un `Arc` pour être passée au handler + pub async fn run(self: Arc, data: ActionData) -> Result { + let handler = self.model.handler().clone(); + let instance_clone = self.clone(); + + // Exécuter le handler + handler(instance_clone, data).await?; + + // Collecter automatiquement les valeurs OUT si succès + debug!("✅ Action '{}' completed successfully, collecting outputs", self.get_name()); + Ok(self.get_out_values()) + } } #[cfg(test)] diff --git a/pmoupnp/src/actions/action_methods.rs b/pmoupnp/src/actions/action_methods.rs index 2ac95758..fb6d2c65 100644 --- a/pmoupnp/src/actions/action_methods.rs +++ b/pmoupnp/src/actions/action_methods.rs @@ -1,16 +1,26 @@ +use std::collections::HashMap; use std::sync::Arc; +use tracing::{debug, trace}; use xmltree::{Element, XMLNode}; -use crate::UpnpModel; -use crate::UpnpObject; -use crate::UpnpObjectSetError; -use crate::UpnpObjectType; -use crate::UpnpTyped; -use crate::actions::Action; -use crate::actions::ActionInstance; -use crate::actions::Argument; -use crate::actions::ArgumentSet; +use crate::{ + action_handler, + UpnpModel, + UpnpObject, + UpnpObjectSetError, + UpnpObjectType, + UpnpTyped, + UpnpTypedInstance, +}; +use crate::actions::{ + Action, + ActionData, + ActionHandler, + ActionInstance, + Argument, + ArgumentSet, +}; impl UpnpObject for Action { fn to_xml_element(&self) -> Element { @@ -42,6 +52,61 @@ impl UpnpTyped for Action { } impl Action { + /// Crée un handler par défaut pour une action. + /// + /// Ce handler logge simplement l'appel et les arguments d'entrée. + /// La méthode [`ActionInstance::run()`](crate::actions::ActionInstance::run) s'occupe + /// automatiquement de collecter les valeurs OUT après l'exécution. + /// + /// # Returns + /// + /// Un [`ActionHandler`] qui logge les entrées. + /// + /// # Comportement + /// + /// - Logge le nom de l'action + /// - Logge les arguments IN avec leurs valeurs + /// - Ne fait aucune modification (handler passif) + /// + /// # Note + /// + /// Ce handler est automatiquement assigné lors de la création d'une action. + /// Il peut être remplacé via [`set_handler`](Self::set_handler). + fn default_handler() -> ActionHandler { + action_handler!(|instance, data| { + use crate::UpnpTypedInstance; + + debug!("🎬 Action '{}' called", instance.get_name()); + + // Logger les arguments d'entrée + for arg_inst in instance.arguments_set().all() { + let arg_model = arg_inst.as_ref().get_model(); + if arg_model.is_in() { + if let Some(value) = data.get(arg_inst.get_name()) { + trace!(" IN {} = {:?}", arg_inst.get_name(), value); + } + } + } + + Ok(()) // Succès - handler par défaut ne fait rien d'autre + }) + } + + /// Crée une nouvelle action UPnP. + /// + /// L'action est initialisée avec un handler par défaut qui logge les entrées + /// et retourne les valeurs des variables d'instance pour les arguments de sortie. + /// + /// # Arguments + /// + /// * `name` - Nom de l'action + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::actions::Action; + /// let mut action = Action::new("Play".to_string()); + /// ``` pub fn new(name: String) -> Action { Self { object: UpnpObjectType { @@ -49,14 +114,56 @@ impl Action { object_type: "Action".to_string(), }, arguments: ArgumentSet::new(), + handle: Self::default_handler(), } } + /// Ajoute un argument à l'action. + /// + /// # Arguments + /// + /// * `arg` - Argument à ajouter + /// + /// # Errors + /// + /// Retourne une erreur si un argument avec le même nom existe déjà. pub fn add_argument(&mut self, arg: Arc) -> Result<(), UpnpObjectSetError> { self.arguments.insert(arg) } + /// Retourne les arguments de l'action. pub fn arguments(&self) -> &ArgumentSet { &self.arguments } + + /// Définit un handler personnalisé pour cette action. + /// + /// Remplace le handler par défaut par un handler personnalisé. + /// + /// # Arguments + /// + /// * `handler` - Le nouveau handler à utiliser + /// + /// # Examples + /// + /// ```rust,no_run + /// # use pmoupnp::actions::Action; + /// # use pmoupnp::action_handler; + /// let mut action = Action::new("Play".to_string()); + /// + /// let custom_handler = action_handler!(|instance, data| { + /// // Logique personnalisée + /// data + /// }); + /// + /// action.set_handler(custom_handler); + /// ``` + pub fn set_handler(&mut self, handler: ActionHandler) { + self.handle = handler; + } + + /// Retourne le handler de l'action. + pub fn handler(&self) -> &ActionHandler { + &self.handle + } } diff --git a/pmoupnp/src/actions/macros.rs b/pmoupnp/src/actions/macros.rs index 987e1885..5642588f 100644 --- a/pmoupnp/src/actions/macros.rs +++ b/pmoupnp/src/actions/macros.rs @@ -24,6 +24,21 @@ /// } /// ``` /// +/// ## Action avec handler personnalisé +/// +/// ```ignore +/// define_action! { +/// pub static ACTION_NAME = "ActionName" { +/// in "ParamName" => VARIABLE_REF, +/// out "ResultParam" => RESULT_VAR, +/// } +/// with handler action_handler!(|instance, data| { +/// // Logique personnalisée +/// Ok(()) +/// }) +/// } +/// ``` +/// /// # Arguments /// /// - `ACTION_NAME` : Nom de la constante statique Rust @@ -89,30 +104,42 @@ /// - Initialisation paresseuse via `Lazy` (thread-safe) #[macro_export] macro_rules! define_action { - // Variante sans arguments - (pub static $name:ident = $action_name:literal) => { - pub static $name: once_cell::sync::Lazy> = + // Variante sans arguments avec handler optionnel + (pub static $name:ident = $action_name:literal $(with handler $handler:expr)?) => { + pub static $name: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { - std::sync::Arc::new($crate::actions::Action::new($action_name.to_string())) + let mut ac = $crate::actions::Action::new($action_name.to_string()); + + $( + ac.set_handler($handler); + )? + + std::sync::Arc::new(ac) }); }; - // Variante avec arguments + // Variante avec arguments et handler optionnel (pub static $name:ident = $action_name:literal { $( $direction:ident $arg_name:literal => $var_ref:expr ),* $(,)? - }) => { - pub static $name: once_cell::sync::Lazy> = + } + $(with handler $handler:expr)? + ) => { + pub static $name: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { let mut ac = $crate::actions::Action::new($action_name.to_string()); - + $( ac.add_argument( define_action!(@arg $direction $arg_name, $var_ref) ); )* - + + $( + ac.set_handler($handler); + )? + std::sync::Arc::new(ac) }); }; diff --git a/pmoupnp/src/actions/mod.rs b/pmoupnp/src/actions/mod.rs index c58fa696..6de16cd4 100644 --- a/pmoupnp/src/actions/mod.rs +++ b/pmoupnp/src/actions/mod.rs @@ -3,6 +3,7 @@ mod errors; mod action_instance; mod action_instance_set; mod action_methods; +mod action_handler; mod action_set_methods; mod arg_inst_set_methods; mod arg_instance_methods; @@ -18,11 +19,58 @@ use crate::{ use std::sync::{Arc, RwLock}; pub use errors::ActionError; +pub use action_handler::{ActionData, ActionFuture, ActionHandler}; -#[derive(Debug, Clone)] +/// Action UPnP. +/// +/// Représente une opération invocable sur un service UPnP avec ses arguments +/// et son handler d'exécution. +/// +/// # Structure +/// +/// - **Arguments** : Liste d'arguments d'entrée (IN) et de sortie (OUT) +/// - **Handler** : Fonction asynchrone qui exécute l'action +/// +/// # Handler par défaut +/// +/// Chaque action est créée avec un handler par défaut qui : +/// - Logge les valeurs des arguments d'entrée +/// - Retourne les valeurs par défaut des arguments de sortie +/// +/// # Examples +/// +/// ```rust +/// use pmoupnp::actions::Action; +/// use pmoupnp::actions::Argument; +/// use pmoupnp::state_variables::StateVariable; +/// use pmoupnp::variable_types::StateVarType; +/// use std::sync::Arc; +/// +/// let mut action = Action::new("Play".to_string()); +/// +/// // Ajouter des arguments +/// let instance_id = Arc::new(StateVariable::new( +/// StateVarType::UI4, +/// "InstanceID".to_string() +/// )); +/// let arg = Arc::new(Argument::new_in("InstanceID".to_string(), instance_id)); +/// action.add_argument(arg); +/// ``` +#[derive(Clone)] pub struct Action { object: UpnpObjectType, arguments: ArgumentSet, + handle: ActionHandler, +} + +impl std::fmt::Debug for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Action") + .field("object", &self.object) + .field("arguments", &self.arguments) + .field("handle", &"") + .finish() + } } pub type ActionSet = UpnpObjectSet; diff --git a/pmoupnp/src/devices/device_instance.rs b/pmoupnp/src/devices/device_instance.rs index 8a9695ad..35647cec 100644 --- a/pmoupnp/src/devices/device_instance.rs +++ b/pmoupnp/src/devices/device_instance.rs @@ -310,6 +310,8 @@ impl DeviceInstance { /// Handler HTTP pour la description du device. async fn description_handler(&self) -> Response { + tracing::info!("📋 Device description requested for {}", self.get_name()); + let elem = self.description_element(); let config = EmitterConfig::new() @@ -318,12 +320,14 @@ impl DeviceInstance { let mut xml_output = Vec::new(); if let Err(e) = elem.write_with_config(&mut xml_output, config) { - tracing::error!("Failed to serialize device description XML: {}", e); + tracing::error!("❌ Failed to serialize device description XML: {}", e); return StatusCode::INTERNAL_SERVER_ERROR.into_response(); } let xml = String::from_utf8_lossy(&xml_output).to_string(); + tracing::debug!("✅ Device description generated ({} bytes)", xml.len()); + ( StatusCode::OK, [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], diff --git a/pmoupnp/src/devices/device_methods.rs b/pmoupnp/src/devices/device_methods.rs index cf4b76e8..515cf3c1 100644 --- a/pmoupnp/src/devices/device_methods.rs +++ b/pmoupnp/src/devices/device_methods.rs @@ -5,7 +5,7 @@ use xmltree::{Element, XMLNode}; use crate::{ devices::{Device, DeviceInstance}, - UpnpObject, UpnpModel, UpnpInstance, + UpnpObject, UpnpModel, UpnpInstance, UpnpTyped, }; impl UpnpObject for Device { @@ -119,14 +119,20 @@ impl UpnpModel for Device { /// Crée une instance du device avec ses services déjà instanciés. /// - /// Les services sont créés dans DeviceInstance::new(), cette méthode - /// établit uniquement les liens bidirectionnels parent-enfant. + /// Cette méthode : + /// 1. Crée l'instance du device + /// 2. Instancie tous les services du modèle + /// 3. Établit les liens bidirectionnels parent-enfant fn create_instance(&self) -> Arc { let instance = Arc::new(DeviceInstance::new(self)); - // Établir le lien parent pour chaque service - for service in instance.services() { - service.set_device(Arc::clone(&instance)); + // Créer les instances de services depuis le modèle + for service_model in self.services() { + let service_instance = service_model.create_instance(); + service_instance.set_device(Arc::clone(&instance)); + if let Err(e) = instance.add_service(service_instance) { + tracing::error!("Failed to add service instance: {:?}", e); + } } instance diff --git a/pmoupnp/src/services/service_instance.rs b/pmoupnp/src/services/service_instance.rs index d3267bc7..c76d2b0c 100644 --- a/pmoupnp/src/services/service_instance.rs +++ b/pmoupnp/src/services/service_instance.rs @@ -42,7 +42,7 @@ use std::{ time::Duration, }; use tokio::time; -use tracing::{error, info, warn}; +use tracing::{debug, error, info, warn}; use xmltree::{Element, EmitterConfig, XMLNode}; use crate::{ @@ -499,6 +499,19 @@ impl ServiceInstance { &self.actions } + /// Retourne une action par son nom. + /// + /// # Arguments + /// + /// * `name` - Nom de l'action + /// + /// # Returns + /// + /// `Some(Arc)` si trouvée, `None` sinon. + pub fn action(&self, name: &str) -> Option> { + self.actions.get_by_name(name) + } + /// Enregistre les routes UPnP dans le serveur. /// /// # Errors @@ -534,7 +547,7 @@ impl ServiceInstance { .await; // Handler control - let instance_control = self.clone(); + let instance_control = Arc::new(self.clone()); server .add_post_handler_with_state(&self.control_route(), control_handler, instance_control) .await; @@ -615,6 +628,8 @@ impl ServiceInstance { /// - Content-Type: `text/xml; charset="utf-8"` /// - Body: Document SCPD formaté avec indentation async fn scpd_handler(&self) -> Response { + info!("📋 SCPD requested for service {}", self.get_name()); + let elem = self.scpd_element(); let config = EmitterConfig::new() @@ -623,12 +638,14 @@ impl ServiceInstance { let mut xml_output = Vec::new(); if let Err(e) = elem.write_with_config(&mut xml_output, config) { - error!("Failed to serialize SCPD XML: {}", e); + error!("❌ Failed to serialize SCPD XML: {}", e); return StatusCode::INTERNAL_SERVER_ERROR.into_response(); } let xml = String::from_utf8_lossy(&xml_output).to_string(); + debug!("✅ SCPD generated for {} ({} bytes)", self.get_name(), xml.len()); + ( StatusCode::OK, [( @@ -1047,43 +1064,148 @@ async fn event_sub_handler( /// /// # Arguments /// -/// * `instance` - L'instance du service -/// * `_body` - Corps de la requête SOAP (actuellement non utilisé) +/// * `instance` - L'instance du service (Arc-wrapped) +/// * `body` - Corps de la requête SOAP /// /// # Returns /// -/// Une réponse SOAP avec le résultat de l'action. +/// Une réponse SOAP avec le résultat de l'action, ou un SOAP fault en cas d'erreur. /// -/// # Note +/// # Erreurs /// -/// Cette implémentation est actuellement un stub et retourne une réponse vide. -/// Le parsing SOAP et l'exécution des actions doivent être implémentés. -async fn control_handler(State(instance): State, _body: String) -> Response { +/// Retourne un SOAP fault dans les cas suivants : +/// - Parsing SOAP invalide +/// - Action non trouvée +/// - Arguments invalides +/// - Échec de l'exécution de l'action +async fn control_handler(State(instance): State>, body: String) -> Response { + use crate::{ + soap::{parse_soap_action, build_soap_response, build_soap_fault, error_codes}, + variable_types::{StateValue, UpnpVarType}, + UpnpTypedInstance, + }; + use std::collections::HashMap; + use tracing::debug; + info!("📡 Control request for {}", instance.get_name()); - // TODO: Parser le SOAP et appeler l'action correspondante + // Parser le SOAP pour extraire l'action et ses arguments + let soap_action = match parse_soap_action(body.as_bytes()) { + Ok(action) => action, + Err(e) => { + error!("❌ Failed to parse SOAP: {:?}", e); + let fault_xml = build_soap_fault( + "s:Client", + "Invalid SOAP request", + Some(error_codes::INVALID_ACTION), + Some("The SOAP request could not be parsed") + ).unwrap_or_else(|_| String::from("s:ServerInternal Error")); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], + fault_xml, + ).into_response(); + } + }; - let response_xml = format!( - r#" - - - - - -"#, - instance.service_type() - ); + debug!("🎬 Received SOAP action: {}", soap_action.name); - ( - StatusCode::OK, - [( - axum::http::header::CONTENT_TYPE, - "text/xml; charset=\"utf-8\"", - )], - response_xml, - ) - .into_response() + // Trouver l'action correspondante dans l'instance + let action_instance = match instance.action(&soap_action.name) { + Some(action_inst) => action_inst, + None => { + error!("❌ Action not found: {}", soap_action.name); + let fault_xml = build_soap_fault( + "s:Client", + "Invalid Action", + Some(error_codes::INVALID_ACTION), + Some(&format!("Action '{}' not found", soap_action.name)) + ).unwrap_or_else(|_| String::from("s:ServerInternal Error")); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], + fault_xml, + ).into_response(); + } + }; + + // Convertir les arguments SOAP (String) en ActionData (StateValue) + let mut action_data = HashMap::new(); + for (arg_name, arg_value) in soap_action.args { + // Trouver l'argument correspondant pour obtenir son type + if let Some(arg_inst) = action_instance.argument(&arg_name) { + if let Some(var_inst) = arg_inst.get_variable_instance() { + let var_model = var_inst.as_ref().get_model(); + // Parser la valeur selon le type de la variable + match StateValue::from_string(&arg_value, &var_model.as_state_var_type()) { + Ok(value) => { + action_data.insert(arg_name, value); + } + Err(e) => { + error!("❌ Failed to parse argument '{}': {:?}", arg_name, e); + let fault_xml = build_soap_fault( + "s:Client", + "Invalid Arguments", + Some(error_codes::ARGUMENT_VALUE_INVALID), + Some(&format!("Invalid value for argument '{}'", arg_name)) + ).unwrap_or_else(|_| String::from("s:ServerInternal Error")); + return ( + StatusCode::BAD_REQUEST, + [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], + fault_xml, + ).into_response(); + } + } + } + } + } + + let action_data = Arc::new(action_data); + + // Exécuter l'action + match action_instance.run(action_data).await { + Ok(output_data) => { + // Convertir les StateValue en String pour SOAP + let mut soap_values = HashMap::new(); + for (key, value) in output_data.iter() { + soap_values.insert(key.clone(), value.to_string()); + } + + // Construire la réponse SOAP + let response_xml = build_soap_response( + &instance.service_type(), + &soap_action.name, + soap_values + ).unwrap_or_else(|_| { + build_soap_fault( + "s:Server", + "Action Failed", + Some(error_codes::ACTION_FAILED), + Some("Failed to build SOAP response") + ).unwrap_or_else(|_| String::from("s:ServerInternal Error")) + }); + + ( + StatusCode::OK, + [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], + response_xml, + ).into_response() + } + Err(e) => { + error!("❌ Action execution failed: {:?}", e); + let fault_xml = build_soap_fault( + "s:Server", + "Action Failed", + Some(error_codes::ACTION_FAILED), + Some(&format!("Action execution failed: {:?}", e)) + ).unwrap_or_else(|_| String::from("s:ServerInternal Error")); + ( + StatusCode::INTERNAL_SERVER_ERROR, + [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], + fault_xml, + ).into_response() + } + } } #[cfg(test)] diff --git a/pmoupnp/src/variable_types/value_methods.rs b/pmoupnp/src/variable_types/value_methods.rs index d7ef8f67..c4bbec51 100644 --- a/pmoupnp/src/variable_types/value_methods.rs +++ b/pmoupnp/src/variable_types/value_methods.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use crate::variable_types::{StateValue, StateVarType, type_trait::UpnpVarType}; +use crate::variable_types::{StateValue, StateValueError, StateVarType, type_trait::UpnpVarType}; impl UpnpVarType for StateValue { fn as_state_var_type(&self) -> StateVarType { @@ -86,3 +86,95 @@ impl PartialOrd for StateValue { } } } + +impl StateValue { + /// Parse une chaîne de caractères en StateValue selon le type spécifié. + /// + /// # Arguments + /// + /// * `s` - La chaîne à parser + /// * `var_type` - Le type de variable attendu + /// + /// # Returns + /// + /// `Ok(StateValue)` si le parsing réussit, `Err(StateValueError)` sinon. + /// + /// # Examples + /// + /// ```ignore + /// use pmoupnp::variable_types::{StateValue, StateVarType}; + /// + /// let value = StateValue::from_string("42", &StateVarType::UI4).unwrap(); + /// assert_eq!(value, StateValue::UI4(42)); + /// + /// let value = StateValue::from_string("true", &StateVarType::Boolean).unwrap(); + /// assert_eq!(value, StateValue::Boolean(true)); + /// ``` + pub fn from_string(s: &str, var_type: &StateVarType) -> Result { + use chrono::NaiveDate; + use url::Url; + use uuid::Uuid; + + match var_type { + StateVarType::UI1 => s.parse::() + .map(StateValue::UI1) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse UI1: {}", e))), + StateVarType::UI2 => s.parse::() + .map(StateValue::UI2) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse UI2: {}", e))), + StateVarType::UI4 => s.parse::() + .map(StateValue::UI4) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse UI4: {}", e))), + StateVarType::I1 => s.parse::() + .map(StateValue::I1) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse I1: {}", e))), + StateVarType::I2 => s.parse::() + .map(StateValue::I2) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse I2: {}", e))), + StateVarType::I4 | StateVarType::Int => s.parse::() + .map(StateValue::I4) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse I4/Int: {}", e))), + StateVarType::R4 => s.parse::() + .map(StateValue::R4) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse R4: {}", e))), + StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => s.parse::() + .map(StateValue::R8) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse R8/Number: {}", e))), + StateVarType::Char => s.chars().next() + .ok_or_else(|| StateValueError::ParseError("Empty string for Char".to_string())) + .map(StateValue::Char), + StateVarType::String => Ok(StateValue::String(s.to_string())), + StateVarType::Boolean => { + match s.to_lowercase().as_str() { + "true" | "1" | "yes" => Ok(StateValue::Boolean(true)), + "false" | "0" | "no" => Ok(StateValue::Boolean(false)), + _ => Err(StateValueError::ParseError(format!("Invalid boolean value: {}", s))), + } + } + StateVarType::BinBase64 => Ok(StateValue::BinBase64(s.to_string())), + StateVarType::BinHex => Ok(StateValue::BinHex(s.to_string())), + StateVarType::Date => NaiveDate::parse_from_str(s, "%Y-%m-%d") + .map(StateValue::Date) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse Date: {}", e))), + StateVarType::DateTime => chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") + .or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")) + .map(StateValue::DateTime) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse DateTime: {}", e))), + StateVarType::DateTimeTZ => chrono::DateTime::parse_from_rfc3339(s) + .map(|dt| StateValue::DateTimeTZ(dt.into())) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse DateTimeTZ: {}", e))), + StateVarType::Time => chrono::NaiveTime::parse_from_str(s, "%H:%M:%S") + .map(StateValue::Time) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse Time: {}", e))), + StateVarType::TimeTZ => chrono::DateTime::parse_from_rfc3339(&format!("1970-01-01T{}", s)) + .map(|dt| StateValue::TimeTZ(dt.into())) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse TimeTZ: {}", e))), + StateVarType::UUID => Uuid::parse_str(s) + .map(StateValue::UUID) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse UUID: {}", e))), + StateVarType::URI => Url::parse(s) + .map(StateValue::URI) + .map_err(|e| StateValueError::ParseError(format!("Failed to parse URI: {}", e))), + } + } +}