travail sur les actions notion de service stateless

This commit is contained in:
2025-10-19 07:58:37 +02:00
parent 34822bef1e
commit efa4855555
9 changed files with 645 additions and 124 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -41,42 +41,39 @@
use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; 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, /// 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 /// # Structure
/// ///
/// - **Clé** : Nom du paramètre (ex: "InstanceID", "TransportURI") /// - **Clé** : Nom du paramètre (ex: "InstanceID", "TransportURI")
/// - **Valeur** : Valeur typée du paramètre ([`StateValue`]) /// - **Valeur** : Valeur dynamique via `Box<dyn Reflect>`
/// ///
/// # Exemples /// # Exemples
/// ///
/// ```rust /// ```rust
/// use pmoupnp::actions::ActionData; /// use pmoupnp::actions::ActionData;
/// use pmoupnp::variable_types::StateValue;
/// use std::collections::HashMap; /// use std::collections::HashMap;
/// use std::sync::Arc; /// use bevy_reflect::Reflect;
/// ///
/// let mut data = HashMap::new(); /// let mut data: ActionData = HashMap::new();
/// data.insert("InstanceID".to_string(), StateValue::UI4(0)); /// data.insert("InstanceID".to_string(), Box::new(0u32));
/// data.insert("Speed".to_string(), StateValue::String("1".to_string())); /// data.insert("Speed".to_string(), Box::new("1".to_string()));
/// ///
/// let action_data: ActionData = Arc::new(data); /// // Les valeurs peuvent être modifiées
/// /// data.insert("InstanceID".to_string(), Box::new(1u32));
/// // Le Arc permet un clonage efficace
/// let cloned = action_data.clone();
/// ``` /// ```
/// ///
/// # Notes /// # Notes
/// ///
/// - Utilise `Arc` pour éviter les copies coûteuses /// - Utilise `Box<dyn Reflect>` pour la flexibilité de typage
/// - Thread-safe grâce à `Arc` /// - Même type pour les entrées et sorties du handler
/// - Les valeurs sont immuables une fois créées /// - Le handler peut modifier directement les données
pub type ActionData = Arc<HashMap<String, StateValue>>; pub type ActionData = HashMap<String, Box<dyn Reflect>>;
/// Future retourné par un [`ActionHandler`]. /// Future retourné par un [`ActionHandler`].
/// ///
@@ -87,53 +84,54 @@ pub type ActionData = Arc<HashMap<String, StateValue>>;
/// # Type complet /// # Type complet
/// ///
/// ```ignore /// ```ignore
/// Pin<Box<dyn Future<Output = Result<(), ActionError>> + Send>> /// Pin<Box<dyn Future<Output = Result<ActionData, ActionError>> + Send>>
/// ``` /// ```
/// ///
/// # Composants /// # Composants
/// ///
/// - `Pin<Box<...>>` : Permet de déplacer le future en mémoire sans invalidation /// - `Pin<Box<...>>` : Permet de déplacer le future en mémoire sans invalidation
/// - `dyn Future<Output = Result<(), ActionError>>` : Future retournant un Result /// - `dyn Future<Output = Result<ActionData, ActionError>>` : Future retournant un Result avec les données modifiées
/// - `+ Send` : Le future peut être envoyé entre threads /// - `+ Send` : Le future peut être envoyé entre threads
/// ///
/// # Notes /// # Notes
/// ///
/// - Les handlers retournent `Ok(())` en cas de succès ou `Err(ActionError)` en cas d'erreur /// - Les handlers retournent `Ok(ActionData)` 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) /// - Le handler retourne les données modifiées (ActionData unifié pour entrée/sortie)
/// collecte automatiquement les valeurs OUT si le handler réussit
/// - Rarement utilisé directement (la macro `action_handler!` s'en charge) /// - Rarement utilisé directement (la macro `action_handler!` s'en charge)
/// - Nécessaire pour la compatibilité avec les trait objects /// - Nécessaire pour la compatibilité avec les trait objects
pub type ActionFuture = Pin<Box<dyn Future<Output = Result<(), crate::actions::ActionError>> + Send>>; pub type ActionFuture = Pin<Box<dyn Future<Output = Result<ActionData, crate::actions::ActionError>> + Send>>;
/// Handler d'action UPnP asynchrone. /// Handler d'action UPnP asynchrone.
/// ///
/// Un `ActionHandler` est une fonction asynchrone partageable qui exécute /// 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 /// # Signature
/// ///
/// ```ignore /// ```ignore
/// Fn(Arc<ActionInstance>) -> ActionFuture /// Fn(ActionData) -> ActionFuture
/// ``` /// ```
/// ///
/// Prend : /// Prend :
/// - [`Arc<ActionInstance>`](crate::actions::ActionInstance) : L'instance de l'action avec accès aux variables liées /// - [`ActionData`] : HashMap contenant les valeurs des arguments (Box<dyn Reflect>)
/// qui contiennent déjà les valeurs des arguments IN
/// ///
/// Retourne un [`ActionFuture`] qui se résout en `Result<(), ActionError>`. /// Retourne un [`ActionFuture`] qui se résout en `Result<ActionData, ActionError>`.
/// ///
/// # Responsabilités /// # Responsabilités
/// ///
/// Le handler est responsable de : /// 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 /// - Exécuter la logique métier
/// - Modifier les variables d'instance selon les besoins /// - Modifier les données selon les besoins
/// - Retourner `Ok(())` en cas de succès ou `Err(ActionError)` en cas d'erreur /// - 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 /// La méthode [`ActionInstance::run()`](crate::actions::ActionInstance::run) s'occupe
/// automatiquement de : /// automatiquement de :
/// 1. Stocker les valeurs IN dans les variables les avant d'appeler le handler /// 1. Construire ActionData depuis les StateVarInstance
/// 2. Collecter les valeurs OUT si le handler retourne `Ok(())` /// 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 /// # Traits requis
/// ///
@@ -149,34 +147,34 @@ pub type ActionFuture = Pin<Box<dyn Future<Output = Result<(), crate::actions::A
/// use pmoupnp::action_handler; /// use pmoupnp::action_handler;
/// use pmoupnp::actions::ActionError; /// use pmoupnp::actions::ActionError;
/// ///
/// let handler = action_handler!(|instance| { /// let handler = action_handler!(|data| {
/// // Logique métier - les valeurs IN sont déjà dans les variables /// // Logique métier avec ActionData
/// Ok::<(), ActionError>(()) /// Ok(data)
/// }); /// });
/// ``` /// ```
/// ///
/// ## Manuellement /// ## Manuellement
/// ///
/// ```rust /// ```rust
/// use pmoupnp::actions::{ActionHandler, ActionInstance, ActionError}; /// use pmoupnp::actions::{ActionHandler, ActionData, ActionError};
/// use std::sync::Arc; /// use std::sync::Arc;
/// ///
/// let handler: ActionHandler = Arc::new(|instance| { /// let handler: ActionHandler = Arc::new(|data| {
/// Box::pin(async move { /// Box::pin(async move {
/// // Votre logique async /// // Votre logique async
/// Ok::<(), ActionError>(()) /// Ok(data)
/// }) /// })
/// }); /// });
/// ``` /// ```
/// ///
/// # Notes d'implémentation /// # Notes d'implémentation
/// ///
/// - Le handler ne retourne rien - il modifie les variables d'instance /// - Le handler reçoit et retourne ActionData (type unifié entrée/sortie)
/// - [`ActionInstance::run()`](crate::actions::ActionInstance::run) collecte automatiquement les OUT /// - Le handler peut modifier directement les données reçues
/// - Le handler capture les variables par `move` /// - Le handler capture les variables par `move`
/// - Le future est automatiquement `Send` si les captures le sont /// - Le future est automatiquement `Send` si les captures le sont
/// - Utilisez la macro `action_handler!` pour simplifier la création /// - Utilisez la macro `action_handler!` pour simplifier la création
pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>) -> ActionFuture + Send + Sync>; pub type ActionHandler = Arc<dyn Fn(ActionData) -> ActionFuture + Send + Sync>;
/// Macro pour créer facilement un ActionHandler. /// Macro pour créer facilement un ActionHandler.
/// ///
@@ -186,16 +184,16 @@ pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>) -> Acti
/// # Syntaxe /// # Syntaxe
/// ///
/// ```ignore /// ```ignore
/// action_handler!(|instance| { /// action_handler!(|data| {
/// // votre logique async (automatiquement dans un bloc async move) /// // votre logique async avec ActionData
/// // Les valeurs IN sont déjà disponibles dans les variables liées /// // Modifier les données et les retourner
/// Ok(data)
/// }) /// })
/// ``` /// ```
/// ///
/// # Arguments /// # Arguments
/// ///
/// - `instance` : Paramètre de type `Arc<`[`ActionInstance`](crate::actions::ActionInstance)`>` - L'instance de l'action /// - `data` : Paramètre de type [`ActionData`] - HashMap contenant les valeurs des arguments
/// avec les valeurs IN déjà stockées dans les variables liées
/// - Le corps du bloc peut contenir du code asynchrone (`.await`) /// - Le corps du bloc peut contenir du code asynchrone (`.await`)
/// ///
/// # Type de retour /// # Type de retour
@@ -204,108 +202,87 @@ pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>) -> Acti
/// ///
/// # Examples /// # Examples
/// ///
/// ## Exemple 1 : Handler simple (ne fait rien) /// ## Exemple 1 : Handler simple (retourne les données telles quelles)
/// ///
/// ```ignore /// ```ignore
/// use pmoupnp::action_handler; /// use pmoupnp::action_handler;
/// ///
/// // Handler minimal - run() collectera automatiquement les OUT /// let handler = action_handler!(|data| {
/// let handler = action_handler!(|instance| { /// Ok(data) // Retourne les données non modifiées
/// Ok(()) // Succès, pas d'erreur
/// }); /// });
/// ``` /// ```
/// ///
/// ## Exemple 2 : Handler qui lit et modifie des variables /// ## Exemple 2 : Handler qui calcule et modifie les données
/// ///
/// ```ignore /// ```ignore
/// use pmoupnp::action_handler; /// use pmoupnp::{action_handler, get, set};
/// use pmoupnp::actions::ActionError; /// use pmoupnp::actions::ActionError;
/// ///
/// let handler = action_handler!(|instance| { /// let handler = action_handler!(|mut data| {
/// // Lire un argument d'entrée depuis la variable liée /// // Extraire les valeurs avec la macro get!
/// let arg = instance.argument("DesiredVolume") /// let celsius: f64 = get!(data, "Celsius", f64);
/// .ok_or_else(|| ActionError::ArgumentNotFound("DesiredVolume".to_string()))?;
/// ///
/// let var = arg.get_variable_instance() /// // Calculer
/// .ok_or_else(|| ActionError::VariableNotBound)?; /// 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 /// Ok(data) // Retourner les données modifiées
/// 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
/// }); /// });
/// ``` /// ```
/// ///
/// ## Exemple 3 : Handler avec logique métier asynchrone /// ## Exemple 3 : Handler avec logique métier asynchrone
/// ///
/// ```ignore /// ```ignore
/// use pmoupnp::action_handler; /// use pmoupnp::{action_handler, get, set};
/// use pmoupnp::actions::ActionError; /// use pmoupnp::actions::ActionError;
/// ///
/// let handler = action_handler!(|instance| { /// let handler = action_handler!(|mut data| {
/// // Lire les paramètres depuis les variables liées /// // Lire l'URI
/// let uri_var = instance.argument("CurrentURI") /// let uri: String = get!(data, "URI", String);
/// .and_then(|a| a.get_variable_instance())
/// .ok_or_else(|| ActionError::VariableNotBound)?;
///
/// let uri = uri_var.value();
/// ///
/// // Appel asynchrone à un service externe /// // 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()))?; /// .map_err(|e| ActionError::ExternalError(e.to_string()))?;
/// ///
/// // Mettre à jour les variables selon la réponse /// // Mettre à jour les données
/// if let Some(arg) = instance.argument("Metadata") { /// set!(data, "Metadata", metadata);
/// if let Some(var) = arg.get_variable_instance() {
/// var.set_value(StateValue::String(response.metadata));
/// }
/// }
/// ///
/// Ok(()) /// Ok(data)
/// }); /// });
/// ``` /// ```
/// ///
/// ## Exemple 4 : Handler avec capture de contexte et validation /// ## Exemple 4 : Handler avec capture de contexte
/// ///
/// ```ignore /// ```ignore
/// use pmoupnp::action_handler; /// use pmoupnp::{action_handler, get, set};
/// use pmoupnp::actions::ActionError; /// use pmoupnp::actions::ActionError;
/// use std::sync::Arc; /// use std::sync::Arc;
/// use tokio::sync::Mutex; /// 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 player_state = Arc::new(Mutex::new(PlayerState::Stopped));
/// ///
/// let handler = action_handler!(|instance| { /// let handler = action_handler!(|mut data| {
/// // Vérifier l'état actuel /// // Vérifier l'état
/// { /// {
/// let state = player_state.lock().await; /// let state = player_state.lock().await;
/// if *state == PlayerState::Error { /// 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; /// let mut state = player_state.lock().await;
/// *state = PlayerState::Playing; /// *state = PlayerState::Playing;
/// } /// }
/// ///
/// // Mettre à jour la variable TransportState /// // Mettre à jour les données
/// if let Some(arg) = instance.argument("CurrentTransportState") { /// set!(data, "TransportState", "PLAYING".to_string());
/// if let Some(var) = arg.get_variable_instance() {
/// var.set_value(StateValue::String("PLAYING".to_string()));
/// }
/// }
/// ///
/// Ok(()) /// Ok(data)
/// }); /// });
/// ``` /// ```
/// ///
@@ -314,10 +291,11 @@ pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>) -> Acti
/// - Le bloc est automatiquement wrappé dans `async move` /// - Le bloc est automatiquement wrappé dans `async move`
/// - Les captures de variables sont déplacées (`move`) /// - Les captures de variables sont déplacées (`move`)
/// - Le résultat est automatiquement boxé et arcé /// - Le résultat est automatiquement boxé et arcé
/// - Utilisez les macros `get!` et `set!` pour manipuler facilement les données
#[macro_export] #[macro_export]
macro_rules! action_handler { macro_rules! action_handler {
(|$instance:ident| $body:block) => { (|$data:ident| $body:block) => {
std::sync::Arc::new(|$instance: std::sync::Arc<$crate::actions::ActionInstance>| { std::sync::Arc::new(|$data: $crate::actions::ActionData| {
Box::pin(async move $body) Box::pin(async move $body)
}) })
}; };

View File

@@ -76,6 +76,27 @@ impl UpnpTypedInstance for ActionInstance {
} }
impl 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. /// Retourne une instance d'argument par son nom.
/// ///
/// # Arguments /// # Arguments

View File

@@ -51,43 +51,33 @@ impl UpnpTyped for Action {
impl Action { impl Action {
/// Crée un handler par défaut pour une action. /// Crée un handler par défaut pour une action.
/// ///
/// Ce handler logge simplement l'appel et les arguments d'entrée. /// Ce handler logge simplement l'appel et les arguments.
/// 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
/// ///
/// # Returns /// # Returns
/// ///
/// Un [`ActionHandler`] qui logge les entrées. /// Un [`ActionHandler`] qui logge les entrées et retourne les données telles quelles.
/// ///
/// # Comportement /// # Comportement
/// ///
/// - Logge le nom de l'action /// - Logge les arguments avec leurs valeurs
/// - Logge les arguments IN avec leurs valeurs (lues depuis les variables liées)
/// - Ne fait aucune modification (handler passif) /// - Ne fait aucune modification (handler passif)
/// - Retourne les données telles quelles
/// ///
/// # Note /// # Note
/// ///
/// Ce handler est automatiquement assigné lors de la création d'une action. /// Ce handler est automatiquement assigné lors de la création d'une action.
/// Il peut être remplacé via [`set_handler`](Self::set_handler). /// Il peut être remplacé via [`set_handler`](Self::set_handler).
fn default_handler() -> ActionHandler { fn default_handler() -> ActionHandler {
action_handler!(|instance| { action_handler!(|data| {
use crate::UpnpTypedInstance; info!("🎬 Action called with default handler");
info!("🎬 Action '{}' called", instance.get_name()); // Logger les arguments
for (key, value) in data.iter() {
// Logger les arguments d'entrée (déjà stockés dans les variables par run()) trace!(" {} = {:?}", key, value);
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());
}
}
} }
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(), arguments: ArgumentSet::new(),
handle: Self::default_handler(), handle: Self::default_handler(),
stateful: true, // Par défaut, les actions sont stateful
} }
} }
@@ -165,4 +156,53 @@ impl Action {
pub fn handler(&self) -> &ActionHandler { pub fn handler(&self) -> &ActionHandler {
&self.handle &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
}
} }

View File

@@ -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<dyn Reflect>` 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<T: Reflect + Clone>(
data: &ActionData,
key: &str
) -> Result<T, ActionError> {
data.get(key)
.and_then(|boxed| boxed.as_any().downcast_ref::<T>())
.cloned()
.ok_or_else(|| ActionError::ArgumentNotFound(key.to_string()))
}
/// Insère une valeur dans ActionData.
///
/// Cette fonction convertit automatiquement la valeur en `Box<dyn Reflect>`
/// 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<T: Reflect + 'static>(
data: &mut ActionData,
key: impl Into<String>,
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)
};
}

View File

@@ -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. /// Macro pour définir facilement une action UPnP.
/// ///
/// Cette macro simplifie la création d'actions UPnP statiques en générant /// Cette macro simplifie la création d'actions UPnP statiques en générant

View File

@@ -9,6 +9,7 @@ mod arg_inst_set_methods;
mod arg_instance_methods; mod arg_instance_methods;
mod arg_set_methods; mod arg_set_methods;
mod argument_methods; mod argument_methods;
mod handler_helpers;
mod macros; mod macros;
@@ -20,6 +21,7 @@ use std::sync::{Arc, RwLock};
pub use errors::ActionError; pub use errors::ActionError;
pub use action_handler::{ActionData, ActionFuture, ActionHandler}; pub use action_handler::{ActionData, ActionFuture, ActionHandler};
pub use handler_helpers::{get_value, set_value};
/// Action UPnP. /// Action UPnP.
/// ///
@@ -61,6 +63,7 @@ pub struct Action {
object: UpnpObjectType, object: UpnpObjectType,
arguments: ArgumentSet, arguments: ArgumentSet,
handle: ActionHandler, handle: ActionHandler,
stateful: bool,
} }
impl std::fmt::Debug for Action { impl std::fmt::Debug for Action {

View File

@@ -233,4 +233,95 @@ impl StateVarInstance {
Ok(arc_reflect) Ok(arc_reflect)
} }
/// Convertit la valeur actuelle en Box<dyn Reflect>
///
/// - Si type String ET parser défini : utilise le parser
/// - Sinon : utilise StateValue::to_reflect() directement
///
/// # Returns
///
/// Un `Box<dyn Reflect>` contenant la valeur actuelle
pub fn to_reflect(&self) -> Box<dyn Reflect> {
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<dyn Reflect>
///
/// - 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<dyn Reflect>) -> 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
}
} }

View File

@@ -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 // É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 // (comme Uuid, Url, et certains types chrono), nous fournissons des méthodes de conversion
// vers des types primitifs qui supportent Reflect. // vers des types primitifs qui supportent Reflect.
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
use crate::variable_types::StateValue; use crate::variable_types::{StateValue, StateValueError, StateVarType};
use std::any::Any;
impl StateValue { impl StateValue {
/// Convertit la StateValue en une valeur Reflect. /// Convertit la StateValue en une valeur Reflect.
@@ -41,4 +42,85 @@ impl StateValue {
StateValue::URI(v) => Box::new(v.to_string()), 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<StateValue, StateValueError> {
match expected_type {
StateVarType::UI1 => {
value.as_any().downcast_ref::<u8>()
.map(|v| StateValue::UI1(*v))
.ok_or_else(|| StateValueError::TypeError("Expected u8".into()))
},
StateVarType::UI2 => {
value.as_any().downcast_ref::<u16>()
.map(|v| StateValue::UI2(*v))
.ok_or_else(|| StateValueError::TypeError("Expected u16".into()))
},
StateVarType::UI4 => {
value.as_any().downcast_ref::<u32>()
.map(|v| StateValue::UI4(*v))
.ok_or_else(|| StateValueError::TypeError("Expected u32".into()))
},
StateVarType::I1 => {
value.as_any().downcast_ref::<i8>()
.map(|v| StateValue::I1(*v))
.ok_or_else(|| StateValueError::TypeError("Expected i8".into()))
},
StateVarType::I2 => {
value.as_any().downcast_ref::<i16>()
.map(|v| StateValue::I2(*v))
.ok_or_else(|| StateValueError::TypeError("Expected i16".into()))
},
StateVarType::I4 | StateVarType::Int => {
value.as_any().downcast_ref::<i32>()
.map(|v| StateValue::I4(*v))
.ok_or_else(|| StateValueError::TypeError("Expected i32".into()))
},
StateVarType::R4 => {
value.as_any().downcast_ref::<f32>()
.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::<f64>()
.map(|v| StateValue::R8(*v))
.ok_or_else(|| StateValueError::TypeError("Expected f64".into()))
},
StateVarType::String | StateVarType::BinBase64 | StateVarType::BinHex => {
value.as_any().downcast_ref::<String>()
.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::<bool>()
.map(|v| StateValue::Boolean(*v))
.ok_or_else(|| StateValueError::TypeError("Expected bool".into()))
},
StateVarType::Char => {
value.as_any().downcast_ref::<char>()
.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::<String>()
.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)
})
},
}
}
} }