From 3ab0c58e4a83912f4cf7ed9475e78e3fea573e8b Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 19 Oct 2025 07:58:37 +0200 Subject: [PATCH] travail sur les actions notion de service stateless --- .DS_Store | Bin 14340 -> 12292 bytes pmoupnp/src/actions/action_handler.rs | 180 +++++++--------- pmoupnp/src/actions/action_instance.rs | 21 ++ pmoupnp/src/actions/action_methods.rs | 82 +++++-- pmoupnp/src/actions/handler_helpers.rs | 202 ++++++++++++++++++ pmoupnp/src/actions/macros.rs | 104 +++++++++ pmoupnp/src/actions/mod.rs | 3 + .../src/state_variables/instance_methods.rs | 91 ++++++++ pmoupnp/src/variable_types/reflect_impl.rs | 86 +++++++- 9 files changed, 645 insertions(+), 124 deletions(-) create mode 100644 pmoupnp/src/actions/handler_helpers.rs diff --git a/.DS_Store b/.DS_Store index 8a936c1c4c56cd287ffa1fd663fbdb47758aca4e..6f6e8ab5a26c65fd955e63beca276db4886aa0cf 100644 GIT binary patch delta 244 zcmZoEXh~3DU|?W$DortDV9)?EIe-{M3-ADmHU~O1_N`!WbFZ zC-W#MZ5B~{z&1HpRcUgyDF0@D0aeD$yCehIHa-Yqlw<{(0|W-#K*AN-1sgBEXP(Tj h6Uf5^ai0Z9+2kCZiIZn4nQRtUyUPeOaWcQaQ2+w1EbjmS diff --git a/pmoupnp/src/actions/action_handler.rs b/pmoupnp/src/actions/action_handler.rs index a73e8d1e..014d6e3b 100644 --- a/pmoupnp/src/actions/action_handler.rs +++ b/pmoupnp/src/actions/action_handler.rs @@ -41,42 +41,39 @@ use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; -use crate::variable_types::StateValue; +use bevy_reflect::Reflect; -/// Données d'une action UPnP. +/// Données d'une action UPnP (entrée/sortie unifiées). /// /// Représente un ensemble de paramètres clé-valeur pour une action UPnP, -/// partagé via `Arc` pour permettre un clonage efficace. +/// utilisant des valeurs Reflect pour la flexibilité de typage. /// /// # Structure /// /// - **Clé** : Nom du paramètre (ex: "InstanceID", "TransportURI") -/// - **Valeur** : Valeur typée du paramètre ([`StateValue`]) +/// - **Valeur** : Valeur dynamique via `Box` /// /// # Exemples /// /// ```rust /// use pmoupnp::actions::ActionData; -/// use pmoupnp::variable_types::StateValue; /// use std::collections::HashMap; -/// use std::sync::Arc; +/// use bevy_reflect::Reflect; /// -/// let mut data = HashMap::new(); -/// data.insert("InstanceID".to_string(), StateValue::UI4(0)); -/// data.insert("Speed".to_string(), StateValue::String("1".to_string())); +/// let mut data: ActionData = HashMap::new(); +/// data.insert("InstanceID".to_string(), Box::new(0u32)); +/// data.insert("Speed".to_string(), Box::new("1".to_string())); /// -/// let action_data: ActionData = Arc::new(data); -/// -/// // Le Arc permet un clonage efficace -/// let cloned = action_data.clone(); +/// // Les valeurs peuvent être modifiées +/// data.insert("InstanceID".to_string(), Box::new(1u32)); /// ``` /// /// # 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>; +/// - Utilise `Box` pour la flexibilité de typage +/// - Même type pour les entrées et sorties du handler +/// - Le handler peut modifier directement les données +pub type ActionData = HashMap>; /// Future retourné par un [`ActionHandler`]. /// @@ -87,53 +84,54 @@ pub type ActionData = Arc>; /// # Type complet /// /// ```ignore -/// Pin> + Send>> +/// Pin> + Send>> /// ``` /// /// # Composants /// /// - `Pin>` : Permet de déplacer le future en mémoire sans invalidation -/// - `dyn Future>` : Future retournant un Result +/// - `dyn Future>` : Future retournant un Result avec les données modifiées /// - `+ 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 +/// - Les handlers retournent `Ok(ActionData)` en cas de succès ou `Err(ActionError)` en cas d'erreur +/// - Le handler retourne les données modifiées (ActionData unifié pour entrée/sortie) /// - Rarement utilisé directement (la macro `action_handler!` s'en charge) /// - Nécessaire pour la compatibilité avec les trait objects -pub type ActionFuture = Pin> + Send>>; +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. +/// la logique métier d'une action et retourne les données modifiées. /// /// # Signature /// /// ```ignore -/// Fn(Arc) -> ActionFuture +/// Fn(ActionData) -> ActionFuture /// ``` /// /// Prend : -/// - [`Arc`](crate::actions::ActionInstance) : L'instance de l'action avec accès aux variables liées -/// qui contiennent déjà les valeurs des arguments IN +/// - [`ActionData`] : HashMap contenant les valeurs des arguments (Box) /// -/// Retourne un [`ActionFuture`] qui se résout en `Result<(), ActionError>`. +/// Retourne un [`ActionFuture`] qui se résout en `Result`. /// /// # Responsabilités /// /// Le handler est responsable de : -/// - Lire les arguments d'entrée depuis les variables liées à l'instance +/// - Lire les arguments d'entrée depuis ActionData /// - 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 +/// - Modifier les données selon les besoins +/// - Retourner `Ok(ActionData)` avec les données modifiées ou `Err(ActionError)` en cas d'erreur /// /// La méthode [`ActionInstance::run()`](crate::actions::ActionInstance::run) s'occupe /// automatiquement de : -/// 1. Stocker les valeurs IN dans les variables liées avant d'appeler le handler -/// 2. Collecter les valeurs OUT si le handler retourne `Ok(())` +/// 1. Construire ActionData depuis les StateVarInstance +/// 2. Merger les valeurs IN du SOAP +/// 3. (Si stateful) Sauver les IN dans les StateVarInstance avant le handler +/// 4. Exécuter le handler +/// 5. (Si stateful) Sauver les OUT dans les StateVarInstance après le handler /// /// # Traits requis /// @@ -149,34 +147,34 @@ pub type ActionFuture = Pin(()) +/// let handler = action_handler!(|data| { +/// // Logique métier avec ActionData +/// Ok(data) /// }); /// ``` /// /// ## Manuellement /// /// ```rust -/// use pmoupnp::actions::{ActionHandler, ActionInstance, ActionError}; +/// use pmoupnp::actions::{ActionHandler, ActionData, ActionError}; /// use std::sync::Arc; /// -/// let handler: ActionHandler = Arc::new(|instance| { +/// let handler: ActionHandler = Arc::new(|data| { /// Box::pin(async move { /// // Votre logique async -/// Ok::<(), ActionError>(()) +/// Ok(data) /// }) /// }); /// ``` /// /// # 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 reçoit et retourne ActionData (type unifié entrée/sortie) +/// - Le handler peut modifier directement les données reçues /// - 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) -> ActionFuture + Send + Sync>; +pub type ActionHandler = Arc ActionFuture + Send + Sync>; /// Macro pour créer facilement un ActionHandler. /// @@ -186,16 +184,16 @@ pub type ActionHandler = Arc) -> Acti /// # Syntaxe /// /// ```ignore -/// action_handler!(|instance| { -/// // votre logique async (automatiquement dans un bloc async move) -/// // Les valeurs IN sont déjà disponibles dans les variables liées +/// action_handler!(|data| { +/// // votre logique async avec ActionData +/// // Modifier les données et les retourner +/// Ok(data) /// }) /// ``` /// /// # Arguments /// -/// - `instance` : Paramètre de type `Arc<`[`ActionInstance`](crate::actions::ActionInstance)`>` - L'instance de l'action -/// avec les valeurs IN déjà stockées dans les variables liées +/// - `data` : Paramètre de type [`ActionData`] - HashMap contenant les valeurs des arguments /// - Le corps du bloc peut contenir du code asynchrone (`.await`) /// /// # Type de retour @@ -204,108 +202,87 @@ pub type ActionHandler = Arc) -> Acti /// /// # Examples /// -/// ## Exemple 1 : Handler simple (ne fait rien) +/// ## Exemple 1 : Handler simple (retourne les données telles quelles) /// /// ```ignore /// use pmoupnp::action_handler; /// -/// // Handler minimal - run() collectera automatiquement les OUT -/// let handler = action_handler!(|instance| { -/// Ok(()) // Succès, pas d'erreur +/// let handler = action_handler!(|data| { +/// Ok(data) // Retourne les données non modifiées /// }); /// ``` /// -/// ## Exemple 2 : Handler qui lit et modifie des variables +/// ## Exemple 2 : Handler qui calcule et modifie les données /// /// ```ignore -/// use pmoupnp::action_handler; +/// use pmoupnp::{action_handler, get, set}; /// use pmoupnp::actions::ActionError; /// -/// let handler = action_handler!(|instance| { -/// // Lire un argument d'entrée depuis la variable liée -/// let arg = instance.argument("DesiredVolume") -/// .ok_or_else(|| ActionError::ArgumentNotFound("DesiredVolume".to_string()))?; +/// let handler = action_handler!(|mut data| { +/// // Extraire les valeurs avec la macro get! +/// let celsius: f64 = get!(data, "Celsius", f64); /// -/// let var = arg.get_variable_instance() -/// .ok_or_else(|| ActionError::VariableNotBound)?; +/// // Calculer +/// let fahrenheit = celsius * 9.0 / 5.0 + 32.0; /// -/// let volume = var.value(); +/// // Insérer avec la macro set! +/// set!(data, "Fahrenheit", fahrenheit); /// -/// // Modifier une autre variable d'instance -/// let current_volume = instance.argument("CurrentVolume") -/// .ok_or_else(|| ActionError::ArgumentNotFound("CurrentVolume".to_string()))? -/// .get_variable_instance() -/// .ok_or_else(|| ActionError::VariableNotBound)?; -/// -/// current_volume.set_value(volume); -/// -/// Ok(()) // Succès - run() collectera CurrentVolume dans les OUT +/// Ok(data) // Retourner les données modifiées /// }); /// ``` /// /// ## Exemple 3 : Handler avec logique métier asynchrone /// /// ```ignore -/// use pmoupnp::action_handler; +/// use pmoupnp::{action_handler, get, set}; /// use pmoupnp::actions::ActionError; /// -/// let handler = action_handler!(|instance| { -/// // Lire les paramètres depuis les variables liées -/// let uri_var = instance.argument("CurrentURI") -/// .and_then(|a| a.get_variable_instance()) -/// .ok_or_else(|| ActionError::VariableNotBound)?; -/// -/// let uri = uri_var.value(); +/// let handler = action_handler!(|mut data| { +/// // Lire l'URI +/// let uri: String = get!(data, "URI", String); /// /// // Appel asynchrone à un service externe -/// let response = external_service::fetch_metadata(&uri).await +/// let metadata = external_service::fetch_metadata(&uri).await /// .map_err(|e| ActionError::ExternalError(e.to_string()))?; /// -/// // Mettre à jour les variables selon la réponse -/// if let Some(arg) = instance.argument("Metadata") { -/// if let Some(var) = arg.get_variable_instance() { -/// var.set_value(StateValue::String(response.metadata)); -/// } -/// } +/// // Mettre à jour les données +/// set!(data, "Metadata", metadata); /// -/// Ok(()) +/// Ok(data) /// }); /// ``` /// -/// ## Exemple 4 : Handler avec capture de contexte et validation +/// ## Exemple 4 : Handler avec capture de contexte /// /// ```ignore -/// use pmoupnp::action_handler; +/// use pmoupnp::{action_handler, get, set}; /// use pmoupnp::actions::ActionError; /// use std::sync::Arc; /// use tokio::sync::Mutex; /// -/// // Contexte partagé (ex: état d'un lecteur média) +/// // Contexte partagé /// let player_state = Arc::new(Mutex::new(PlayerState::Stopped)); /// -/// let handler = action_handler!(|instance| { -/// // Vérifier l'état actuel +/// let handler = action_handler!(|mut data| { +/// // Vérifier l'état /// { /// let state = player_state.lock().await; /// if *state == PlayerState::Error { -/// return Err(ActionError::InvalidState("Player in error state".to_string())); +/// return Err(ActionError::InvalidState("Player in error state".into())); /// } /// } /// -/// // Modifier l'état du lecteur +/// // Modifier l'état /// { /// 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())); -/// } -/// } +/// // Mettre à jour les données +/// set!(data, "TransportState", "PLAYING".to_string()); /// -/// Ok(()) +/// Ok(data) /// }); /// ``` /// @@ -314,10 +291,11 @@ pub type ActionHandler = Arc) -> Acti /// - 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é +/// - Utilisez les macros `get!` et `set!` pour manipuler facilement les données #[macro_export] macro_rules! action_handler { - (|$instance:ident| $body:block) => { - std::sync::Arc::new(|$instance: std::sync::Arc<$crate::actions::ActionInstance>| { + (|$data:ident| $body:block) => { + std::sync::Arc::new(|$data: $crate::actions::ActionData| { Box::pin(async move $body) }) }; diff --git a/pmoupnp/src/actions/action_instance.rs b/pmoupnp/src/actions/action_instance.rs index 4ac85e46..225eb7ec 100644 --- a/pmoupnp/src/actions/action_instance.rs +++ b/pmoupnp/src/actions/action_instance.rs @@ -76,6 +76,27 @@ impl UpnpTypedInstance for ActionInstance { } impl ActionInstance { + /// Retourne `true` si l'action est stateful. + /// + /// Une action stateful met à jour les StateVarInstance lors de l'exécution. + /// + /// # Returns + /// + /// `true` si l'action est stateful, `false` si stateless. + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::actions::{Action, ActionInstance}; + /// # use pmoupnp::UpnpInstance; + /// let mut action = Action::new("Play".to_string()); + /// let instance = ActionInstance::new(&action); + /// assert!(instance.is_stateful()); // Stateful par défaut + /// ``` + pub fn is_stateful(&self) -> bool { + self.model.is_stateful() + } + /// Retourne une instance d'argument par son nom. /// /// # Arguments diff --git a/pmoupnp/src/actions/action_methods.rs b/pmoupnp/src/actions/action_methods.rs index b8f528a8..30586710 100644 --- a/pmoupnp/src/actions/action_methods.rs +++ b/pmoupnp/src/actions/action_methods.rs @@ -51,43 +51,33 @@ 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 : - /// 1. Stocker les valeurs IN dans les variables liées avant d'appeler le handler - /// 2. Collecter les valeurs OUT après l'exécution + /// Ce handler logge simplement l'appel et les arguments. /// /// # Returns /// - /// Un [`ActionHandler`] qui logge les entrées. + /// Un [`ActionHandler`] qui logge les entrées et retourne les données telles quelles. /// /// # Comportement /// - /// - Logge le nom de l'action - /// - Logge les arguments IN avec leurs valeurs (lues depuis les variables liées) + /// - Logge les arguments avec leurs valeurs /// - Ne fait aucune modification (handler passif) + /// - Retourne les données telles quelles /// /// # 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| { - use crate::UpnpTypedInstance; + action_handler!(|data| { + info!("🎬 Action called with default handler"); - info!("🎬 Action '{}' called", instance.get_name()); - - // Logger les arguments d'entrée (déjà stockés dans les variables par run()) - 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(var_inst) = arg_inst.get_variable_instance() { - trace!(" IN {} = {:?}", arg_inst.get_name(), var_inst.value()); - } - } + // Logger les arguments + for (key, value) in data.iter() { + trace!(" {} = {:?}", key, value); } - Ok(()) // Succès - handler par défaut ne fait rien d'autre + // Retourner les données telles quelles + Ok(data) }) } @@ -114,6 +104,7 @@ impl Action { }, arguments: ArgumentSet::new(), handle: Self::default_handler(), + stateful: true, // Par défaut, les actions sont stateful } } @@ -165,4 +156,53 @@ impl Action { pub fn handler(&self) -> &ActionHandler { &self.handle } + + /// Définit si l'action est stateful. + /// + /// Une action stateful met à jour les StateVarInstance lors de l'exécution, + /// déclenchant ainsi les notifications d'événements UPnP. + /// + /// Une action stateless n'interagit pas avec les StateVarInstance, + /// ce qui améliore les performances pour les opérations purement calculatoires. + /// + /// # Arguments + /// + /// * `stateful` - `true` pour stateful (défaut), `false` pour stateless + /// + /// # Returns + /// + /// `&mut Self` pour permettre le chaînage + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::actions::Action; + /// let mut action = Action::new("Calculate".to_string()); + /// action.set_stateful(false); // Action stateless + /// ``` + pub fn set_stateful(&mut self, stateful: bool) -> &mut Self { + self.stateful = stateful; + self + } + + /// Retourne `true` si l'action est stateful. + /// + /// # Returns + /// + /// `true` si l'action met à jour les StateVarInstance (stateful), + /// `false` si l'action est purement calculatoire (stateless). + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::actions::Action; + /// let mut action = Action::new("Play".to_string()); + /// assert!(action.is_stateful()); // Stateful par défaut + /// + /// action.set_stateful(false); + /// assert!(!action.is_stateful()); // Maintenant stateless + /// ``` + pub fn is_stateful(&self) -> bool { + self.stateful + } } diff --git a/pmoupnp/src/actions/handler_helpers.rs b/pmoupnp/src/actions/handler_helpers.rs new file mode 100644 index 00000000..9c85e06a --- /dev/null +++ b/pmoupnp/src/actions/handler_helpers.rs @@ -0,0 +1,202 @@ +//! Helpers et macros pour faciliter l'écriture de handlers d'actions. +//! +//! Ce module fournit des fonctions utilitaires et des macros pour simplifier +//! la manipulation de [`ActionData`](crate::actions::ActionData) dans les handlers. +//! +//! # Fonctions utilitaires +//! +//! - [`get_value`] : Extrait une valeur typée depuis ActionData +//! - [`set_value`] : Insère une valeur dans ActionData +//! +//! # Macros +//! +//! - [`get!`](crate::get) : Macro pour extraire facilement une valeur +//! - [`set!`](crate::set) : Macro pour insérer facilement une valeur +//! +//! # Examples +//! +//! ```rust +//! use pmoupnp::{action_handler, get, set}; +//! use pmoupnp::actions::ActionError; +//! +//! let handler = action_handler!(|mut data| { +//! // Extraction avec macro +//! let celsius: f64 = get!(data, "Celsius", f64); +//! +//! // Calcul +//! let fahrenheit = celsius * 9.0 / 5.0 + 32.0; +//! +//! // Insertion avec macro +//! set!(data, "Fahrenheit", fahrenheit); +//! +//! Ok(data) +//! }); +//! ``` + +use bevy_reflect::Reflect; +use crate::actions::{ActionData, ActionError}; +use std::any::Any; + +/// Extrait une valeur typée depuis ActionData. +/// +/// Cette fonction permet d'extraire une valeur `Box` depuis +/// ActionData et de la convertir vers le type concret attendu. +/// +/// # Type Parameters +/// +/// * `T` - Le type concret attendu (doit implémenter `Reflect + Clone`) +/// +/// # Arguments +/// +/// * `data` - Référence vers ActionData +/// * `key` - Clé de la valeur à extraire +/// +/// # Returns +/// +/// `Ok(T)` si la valeur existe et peut être convertie vers `T`, +/// `Err(ActionError)` sinon. +/// +/// # Errors +/// +/// Retourne `ActionError::ArgumentNotFound` si : +/// - La clé n'existe pas dans ActionData +/// - La valeur ne peut pas être convertie vers le type `T` +/// +/// # Examples +/// +/// ```rust +/// use pmoupnp::actions::{ActionData, get_value}; +/// use std::collections::HashMap; +/// +/// let mut data: ActionData = HashMap::new(); +/// data.insert("Volume".to_string(), Box::new(50u32)); +/// +/// let volume: u32 = get_value(&data, "Volume").unwrap(); +/// assert_eq!(volume, 50); +/// ``` +pub fn get_value( + data: &ActionData, + key: &str +) -> Result { + data.get(key) + .and_then(|boxed| boxed.as_any().downcast_ref::()) + .cloned() + .ok_or_else(|| ActionError::ArgumentNotFound(key.to_string())) +} + +/// Insère une valeur dans ActionData. +/// +/// Cette fonction convertit automatiquement la valeur en `Box` +/// et l'insère dans ActionData. +/// +/// # Type Parameters +/// +/// * `T` - Le type de la valeur (doit implémenter `Reflect + 'static`) +/// +/// # Arguments +/// +/// * `data` - Référence mutable vers ActionData +/// * `key` - Clé pour la valeur (convertie en `String`) +/// * `value` - Valeur à insérer +/// +/// # Examples +/// +/// ```rust +/// use pmoupnp::actions::{ActionData, set_value}; +/// use std::collections::HashMap; +/// +/// let mut data: ActionData = HashMap::new(); +/// set_value(&mut data, "Volume", 75u32); +/// +/// // Vérifier l'insertion +/// use pmoupnp::actions::get_value; +/// let volume: u32 = get_value(&data, "Volume").unwrap(); +/// assert_eq!(volume, 75); +/// ``` +pub fn set_value( + data: &mut ActionData, + key: impl Into, + value: T +) { + data.insert(key.into(), Box::new(value)); +} + +/// Macro pour extraire facilement une valeur depuis ActionData. +/// +/// Cette macro simplifie l'utilisation de [`get_value`] en gérant +/// automatiquement la propagation d'erreur avec `?`. +/// +/// # Syntaxe +/// +/// ```ignore +/// get!(data, "key", Type) +/// ``` +/// +/// # Arguments +/// +/// * `data` - Expression évaluant à `&ActionData` +/// * `key` - Clé de la valeur (expression évaluant à `&str`) +/// * `type` - Type concret attendu +/// +/// # Returns +/// +/// La valeur de type `Type` si elle existe et peut être convertie, +/// sinon propage l'erreur avec `?`. +/// +/// # Examples +/// +/// ```ignore +/// use pmoupnp::{get, action_handler}; +/// use pmoupnp::actions::ActionError; +/// +/// let handler = action_handler!(|data| { +/// let volume: u32 = get!(data, "Volume", u32); +/// let name: String = get!(data, "Name", String); +/// +/// // Utiliser les valeurs... +/// +/// Ok(data) +/// }); +/// ``` +#[macro_export] +macro_rules! get { + ($data:expr, $key:expr, $type:ty) => { + $crate::actions::get_value::<$type>($data, $key)? + }; +} + +/// Macro pour insérer facilement une valeur dans ActionData. +/// +/// Cette macro simplifie l'utilisation de [`set_value`] pour +/// insérer des valeurs dans ActionData. +/// +/// # Syntaxe +/// +/// ```ignore +/// set!(data, "key", value) +/// ``` +/// +/// # Arguments +/// +/// * `data` - Expression évaluant à `&mut ActionData` +/// * `key` - Clé pour la valeur (expression évaluant vers `String`) +/// * `value` - Valeur à insérer (doit implémenter `Reflect + 'static`) +/// +/// # Examples +/// +/// ```ignore +/// use pmoupnp::{set, action_handler}; +/// +/// let handler = action_handler!(|mut data| { +/// set!(data, "Result", 42u32); +/// set!(data, "Message", "Success".to_string()); +/// +/// Ok(data) +/// }); +/// ``` +#[macro_export] +macro_rules! set { + ($data:expr, $key:expr, $value:expr) => { + $crate::actions::set_value($data, $key, $value) + }; +} diff --git a/pmoupnp/src/actions/macros.rs b/pmoupnp/src/actions/macros.rs index 5642588f..de7eabe3 100644 --- a/pmoupnp/src/actions/macros.rs +++ b/pmoupnp/src/actions/macros.rs @@ -1,3 +1,107 @@ +/// Macro pour créer facilement une action avec son handler. +/// +/// Cette macro simplifie la création d'actions dynamiques en combinant +/// la création de l'action et l'assignation du handler en une seule expression. +/// +/// # Syntaxe +/// +/// ## Action stateful (défaut) +/// +/// ```ignore +/// action!("ActionName", |data| { +/// // Handler code +/// Ok(data) +/// }) +/// ``` +/// +/// ## Action stateless +/// +/// ```ignore +/// action!("ActionName", stateless, |data| { +/// // Handler code +/// Ok(data) +/// }) +/// ``` +/// +/// # Arguments +/// +/// * `name` - Nom de l'action UPnP (chaîne littérale ou expression String) +/// * `stateless` - (Optionnel) Mot-clé pour marquer l'action comme stateless +/// * `|data| { ... }` - Closure du handler (voir [`action_handler!`](crate::action_handler)) +/// +/// # Type de retour +/// +/// Retourne une `Action` configurée avec le handler spécifié. +/// +/// # Examples +/// +/// ## Action stateless simple +/// +/// ```ignore +/// use pmoupnp::{action, get, set}; +/// +/// let convert = action!("ConvertTemp", stateless, |mut data| { +/// let celsius: f64 = get!(data, "Celsius", f64); +/// let fahrenheit = celsius * 9.0 / 5.0 + 32.0; +/// set!(data, "Fahrenheit", fahrenheit); +/// Ok(data) +/// }); +/// ``` +/// +/// ## Action stateful (défaut) +/// +/// ```ignore +/// use pmoupnp::{action, get, set}; +/// +/// let play = action!("Play", |mut data| { +/// let speed: String = get!(data, "Speed", String); +/// set!(data, "TransportState", "PLAYING".to_string()); +/// Ok(data) +/// }); +/// ``` +/// +/// ## Avec capture de contexte +/// +/// ```ignore +/// use pmoupnp::{action, get, set}; +/// use std::sync::Arc; +/// use tokio::sync::Mutex; +/// +/// let state = Arc::new(Mutex::new(PlayerState::Stopped)); +/// let state_clone = state.clone(); +/// +/// let play = action!("Play", |mut data| { +/// let mut player = state_clone.lock().await; +/// *player = PlayerState::Playing; +/// set!(data, "TransportState", "PLAYING".to_string()); +/// Ok(data) +/// }); +/// ``` +/// +/// # Notes +/// +/// - Actions stateful (défaut) : mettent à jour les StateVarInstance (notifications) +/// - Actions stateless : pas de mise à jour des StateVarInstance (performances) +/// - Le handler capture les variables par `move` +/// - Utilisez [`get!`](crate::get) et [`set!`](crate::set) pour manipuler facilement les données +#[macro_export] +macro_rules! action { + // Action stateful (défaut) + ($name:expr, |$data:ident| $body:block) => {{ + let mut action = $crate::actions::Action::new($name.to_string()); + action.set_handler($crate::action_handler!(|$data| $body)); + action + }}; + + // Action stateless + ($name:expr, stateless, |$data:ident| $body:block) => {{ + let mut action = $crate::actions::Action::new($name.to_string()); + action.set_stateful(false); + action.set_handler($crate::action_handler!(|$data| $body)); + action + }}; +} + /// Macro pour définir facilement une action UPnP. /// /// Cette macro simplifie la création d'actions UPnP statiques en générant diff --git a/pmoupnp/src/actions/mod.rs b/pmoupnp/src/actions/mod.rs index d8c3623c..3e75053d 100644 --- a/pmoupnp/src/actions/mod.rs +++ b/pmoupnp/src/actions/mod.rs @@ -9,6 +9,7 @@ mod arg_inst_set_methods; mod arg_instance_methods; mod arg_set_methods; mod argument_methods; +mod handler_helpers; mod macros; @@ -20,6 +21,7 @@ use std::sync::{Arc, RwLock}; pub use errors::ActionError; pub use action_handler::{ActionData, ActionFuture, ActionHandler}; +pub use handler_helpers::{get_value, set_value}; /// Action UPnP. /// @@ -61,6 +63,7 @@ pub struct Action { object: UpnpObjectType, arguments: ArgumentSet, handle: ActionHandler, + stateful: bool, } impl std::fmt::Debug for Action { diff --git a/pmoupnp/src/state_variables/instance_methods.rs b/pmoupnp/src/state_variables/instance_methods.rs index b2987986..d206b63d 100644 --- a/pmoupnp/src/state_variables/instance_methods.rs +++ b/pmoupnp/src/state_variables/instance_methods.rs @@ -233,4 +233,95 @@ impl StateVarInstance { Ok(arc_reflect) } + + /// Convertit la valeur actuelle en Box + /// + /// - Si type String ET parser défini : utilise le parser + /// - Sinon : utilise StateValue::to_reflect() directement + /// + /// # Returns + /// + /// Un `Box` contenant la valeur actuelle + pub fn to_reflect(&self) -> Box { + use crate::variable_types::StateVarType; + + let current_value = self.value.read().unwrap().clone(); + + // Parser uniquement pour les String + if self.as_state_var_type() == StateVarType::String { + if let StateValue::String(ref s) = current_value { + if let Some(ref parser) = self.model.parse { + match parser(s) { + Ok(reflected) => return reflected, + Err(e) => { + tracing::warn!( + "Failed to parse value '{}' for variable '{}': {:?}, using raw string", + s, self.get_name(), e + ); + } + } + } + } + } + + // Conversion standard pour tous les autres types + current_value.to_reflect() + } + + /// Définit la valeur depuis Box + /// + /// - Si type String ET marshal défini : utilise le marshal + /// - Sinon : utilise StateValue::from_reflect() directement + /// + /// Puis délègue à set_value() pour la mise à jour et les notifications + /// + /// # Arguments + /// + /// * `reflect_value` - La nouvelle valeur sous forme Reflect + /// + /// # Errors + /// + /// Retourne une erreur si : + /// - La conversion Reflect → StateValue échoue + /// - Le marshalling échoue + /// - La mise à jour de la valeur échoue + pub async fn set_reflect_value(&self, reflect_value: Box) -> Result<(), StateValueError> { + use crate::variable_types::StateVarType; + + // Convertir Reflect → StateValue + let state_value = if self.as_state_var_type() == StateVarType::String { + // Pour les String : essayer le marshal si défini + if let Some(ref marshal) = self.model.marshal { + // D'abord, essayer de convertir Reflect → StateValue temporaire + match StateValue::from_reflect(reflect_value.as_ref(), self.as_state_var_type()) { + Ok(temp_value) => { + // Utiliser le marshal pour obtenir la String marshallée + match marshal(&temp_value) { + Ok(marshalled_string) => { + StateValue::String(marshalled_string) + }, + Err(e) => { + tracing::warn!( + "Failed to marshal value for variable '{}': {:?}, using standard conversion", + self.get_name(), e + ); + // Fallback + temp_value + } + } + }, + Err(e) => return Err(e), + } + } else { + // Pas de marshal, conversion standard + StateValue::from_reflect(reflect_value.as_ref(), self.as_state_var_type())? + } + } else { + // Pas un String, conversion standard + StateValue::from_reflect(reflect_value.as_ref(), self.as_state_var_type())? + }; + + // Déléguer à set_value() pour factoriser (mise à jour + notifications) + self.set_value(state_value).await + } } diff --git a/pmoupnp/src/variable_types/reflect_impl.rs b/pmoupnp/src/variable_types/reflect_impl.rs index 524e2c35..55944d2b 100644 --- a/pmoupnp/src/variable_types/reflect_impl.rs +++ b/pmoupnp/src/variable_types/reflect_impl.rs @@ -1,11 +1,12 @@ -// Ce module permet de convertir StateValue en valeurs Reflect +// Ce module permet de convertir StateValue en valeurs Reflect et vice-versa // // Étant donné que StateValue contient des types qui n'implémentent pas tous Reflect // (comme Uuid, Url, et certains types chrono), nous fournissons des méthodes de conversion // vers des types primitifs qui supportent Reflect. use bevy_reflect::Reflect; -use crate::variable_types::StateValue; +use crate::variable_types::{StateValue, StateValueError, StateVarType}; +use std::any::Any; impl StateValue { /// Convertit la StateValue en une valeur Reflect. @@ -41,4 +42,85 @@ impl StateValue { StateValue::URI(v) => Box::new(v.to_string()), } } + + /// Convertit &dyn Reflect → StateValue selon le type attendu + /// + /// Méthode statique utilisée pour reconstruire StateValue depuis Reflect + pub fn from_reflect( + value: &dyn Reflect, + expected_type: StateVarType + ) -> Result { + match expected_type { + StateVarType::UI1 => { + value.as_any().downcast_ref::() + .map(|v| StateValue::UI1(*v)) + .ok_or_else(|| StateValueError::TypeError("Expected u8".into())) + }, + StateVarType::UI2 => { + value.as_any().downcast_ref::() + .map(|v| StateValue::UI2(*v)) + .ok_or_else(|| StateValueError::TypeError("Expected u16".into())) + }, + StateVarType::UI4 => { + value.as_any().downcast_ref::() + .map(|v| StateValue::UI4(*v)) + .ok_or_else(|| StateValueError::TypeError("Expected u32".into())) + }, + StateVarType::I1 => { + value.as_any().downcast_ref::() + .map(|v| StateValue::I1(*v)) + .ok_or_else(|| StateValueError::TypeError("Expected i8".into())) + }, + StateVarType::I2 => { + value.as_any().downcast_ref::() + .map(|v| StateValue::I2(*v)) + .ok_or_else(|| StateValueError::TypeError("Expected i16".into())) + }, + StateVarType::I4 | StateVarType::Int => { + value.as_any().downcast_ref::() + .map(|v| StateValue::I4(*v)) + .ok_or_else(|| StateValueError::TypeError("Expected i32".into())) + }, + StateVarType::R4 => { + value.as_any().downcast_ref::() + .map(|v| StateValue::R4(*v)) + .ok_or_else(|| StateValueError::TypeError("Expected f32".into())) + }, + StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => { + value.as_any().downcast_ref::() + .map(|v| StateValue::R8(*v)) + .ok_or_else(|| StateValueError::TypeError("Expected f64".into())) + }, + StateVarType::String | StateVarType::BinBase64 | StateVarType::BinHex => { + value.as_any().downcast_ref::() + .map(|v| match expected_type { + StateVarType::String => StateValue::String(v.clone()), + StateVarType::BinBase64 => StateValue::BinBase64(v.clone()), + StateVarType::BinHex => StateValue::BinHex(v.clone()), + _ => unreachable!(), + }) + .ok_or_else(|| StateValueError::TypeError("Expected String".into())) + }, + StateVarType::Boolean => { + value.as_any().downcast_ref::() + .map(|v| StateValue::Boolean(*v)) + .ok_or_else(|| StateValueError::TypeError("Expected bool".into())) + }, + StateVarType::Char => { + value.as_any().downcast_ref::() + .map(|v| StateValue::Char(*v)) + .ok_or_else(|| StateValueError::TypeError("Expected char".into())) + }, + // Pour les types complexes, on essaie de reconstruire depuis String + StateVarType::Date | StateVarType::DateTime | StateVarType::DateTimeTZ | + StateVarType::Time | StateVarType::TimeTZ | StateVarType::UUID | StateVarType::URI => { + value.as_any().downcast_ref::() + .ok_or_else(|| StateValueError::TypeError("Expected String representation".into())) + .and_then(|s| { + // Utiliser les méthodes from_string existantes + StateValue::from_string(s, &expected_type) + }) + }, + } + } }