travail sur les actions notion de service stateless

This commit is contained in:
2025-10-19 07:58:37 +02:00
parent f3560fa4b9
commit ebd66cd981
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 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<dyn Reflect>`
///
/// # 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<HashMap<String, StateValue>>;
/// - Utilise `Box<dyn Reflect>` 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<String, Box<dyn Reflect>>;
/// Future retourné par un [`ActionHandler`].
///
@@ -87,53 +84,54 @@ pub type ActionData = Arc<HashMap<String, StateValue>>;
/// # Type complet
///
/// ```ignore
/// Pin<Box<dyn Future<Output = Result<(), ActionError>> + Send>>
/// Pin<Box<dyn Future<Output = Result<ActionData, ActionError>> + Send>>
/// ```
///
/// # Composants
///
/// - `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
///
/// # 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<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.
///
/// 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<ActionInstance>) -> ActionFuture
/// Fn(ActionData) -> ActionFuture
/// ```
///
/// Prend :
/// - [`Arc<ActionInstance>`](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<dyn Reflect>)
///
/// Retourne un [`ActionFuture`] qui se résout en `Result<(), ActionError>`.
/// Retourne un [`ActionFuture`] qui se résout en `Result<ActionData, ActionError>`.
///
/// # 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 les 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<Box<dyn Future<Output = Result<(), crate::actions::A
/// use pmoupnp::action_handler;
/// use pmoupnp::actions::ActionError;
///
/// let handler = action_handler!(|instance| {
/// // Logique métier - les valeurs IN sont déjà dans les variables
/// Ok::<(), ActionError>(())
/// 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<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.
///
@@ -186,16 +184,16 @@ pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>) -> 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<dyn Fn(Arc<crate::actions::ActionInstance>) -> 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<dyn Fn(Arc<crate::actions::ActionInstance>) -> 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)
})
};

View File

@@ -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

View File

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

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.
///
/// 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_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 {

View File

@@ -233,4 +233,95 @@ impl StateVarInstance {
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
// (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<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)
})
},
}
}
}