mise à jours des handlers d'actions

This commit is contained in:
2025-10-16 21:06:29 +02:00
parent e7e6727123
commit 015cc69a31
5 changed files with 95 additions and 59 deletions

View File

@@ -113,25 +113,27 @@ pub type ActionFuture = Pin<Box<dyn Future<Output = Result<(), crate::actions::A
/// # Signature
///
/// ```ignore
/// Fn(Arc<ActionInstance>, ActionData) -> ActionFuture
/// Fn(Arc<ActionInstance>) -> ActionFuture
/// ```
///
/// Prend :
/// - [`Arc<ActionInstance>`](crate::actions::ActionInstance) : L'instance de l'action avec accès aux variables liées
/// - [`ActionData`] : Les données d'entrée (arguments IN)
/// qui contiennent déjà les valeurs des arguments IN
///
/// Retourne un [`ActionFuture`] qui se résout en `Result<(), ActionError>`.
///
/// # Responsabilités
///
/// Le handler est responsable de :
/// - Lire les arguments d'entrée depuis `data`
/// - Lire les arguments d'entrée depuis les variables liées à l'instance
/// - Exécuter la logique métier
/// - Modifier les variables d'instance selon les besoins
/// - Retourner `Ok(())` en cas de succès ou `Err(ActionError)` en cas d'erreur
///
/// La méthode [`ActionInstance::run()`](crate::actions::ActionInstance::run) s'occupe
/// automatiquement de collecter les valeurs OUT si le handler retourne `Ok(())`.
/// 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(())`
///
/// # Traits requis
///
@@ -147,8 +149,8 @@ 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, data| {
/// // Logique métier
/// let handler = action_handler!(|instance| {
/// // Logique métier - les valeurs IN sont déjà dans les variables
/// Ok::<(), ActionError>(())
/// });
/// ```
@@ -156,10 +158,10 @@ pub type ActionFuture = Pin<Box<dyn Future<Output = Result<(), crate::actions::A
/// ## Manuellement
///
/// ```rust
/// use pmoupnp::actions::{ActionData, ActionHandler, ActionInstance, ActionError};
/// use pmoupnp::actions::{ActionHandler, ActionInstance, ActionError};
/// use std::sync::Arc;
///
/// let handler: ActionHandler = Arc::new(|instance, data| {
/// let handler: ActionHandler = Arc::new(|instance| {
/// Box::pin(async move {
/// // Votre logique async
/// Ok::<(), ActionError>(())
@@ -174,7 +176,7 @@ pub type ActionFuture = Pin<Box<dyn Future<Output = Result<(), crate::actions::A
/// - 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>, ActionData) -> ActionFuture + Send + Sync>;
pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>) -> ActionFuture + Send + Sync>;
/// Macro pour créer facilement un ActionHandler.
///
@@ -184,16 +186,16 @@ pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>, ActionD
/// # Syntaxe
///
/// ```ignore
/// action_handler!(|instance, data| {
/// action_handler!(|instance| {
/// // votre logique async (automatiquement dans un bloc async move)
/// data
/// // Les valeurs IN sont déjà disponibles dans les variables liées
/// })
/// ```
///
/// # Arguments
///
/// - `instance` : Paramètre de type `Arc<`[`ActionInstance`](crate::actions::ActionInstance)`>` - L'instance de l'action
/// - `data` : Paramètre de type [`ActionData`] (Arc<HashMap<String, StateValue>>) - Les données d'entrée
/// avec les valeurs IN déjà stockées dans les variables liées
/// - Le corps du bloc peut contenir du code asynchrone (`.await`)
///
/// # Type de retour
@@ -208,56 +210,61 @@ pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>, ActionD
/// use pmoupnp::action_handler;
///
/// // Handler minimal - run() collectera automatiquement les OUT
/// let handler = action_handler!(|instance, data| {
/// let handler = action_handler!(|instance| {
/// Ok(()) // Succès, pas d'erreur
/// });
/// ```
///
/// ## Exemple 2 : Handler qui modifie une variable avec gestion d'erreur
/// ## Exemple 2 : Handler qui lit et modifie des variables
///
/// ```ignore
/// use pmoupnp::action_handler;
/// use pmoupnp::actions::ActionError;
///
/// let handler = action_handler!(|instance, data| {
/// // Lire un argument d'entrée
/// let volume = data.get("DesiredVolume")
/// .ok_or_else(|| ActionError::MissingArgument("DesiredVolume".to_string()))?;
///
/// // Modifier la variable d'instance
/// let arg = instance.argument("CurrentVolume")
/// .ok_or_else(|| ActionError::ArgumentNotFound("CurrentVolume".to_string()))?;
/// let 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 var = arg.get_variable_instance()
/// .ok_or_else(|| ActionError::VariableNotBound)?;
///
/// var.set_value(volume.clone());
/// let volume = var.value();
///
/// // 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
/// });
/// ```
///
/// ## Exemple 3 : Handler avec logique métier asynchrone et gestion d'erreur
/// ## Exemple 3 : Handler avec logique métier asynchrone
///
/// ```ignore
/// use pmoupnp::action_handler;
/// use pmoupnp::actions::ActionError;
///
/// let handler = action_handler!(|instance, data| {
/// 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();
///
/// // Appel asynchrone à un service externe
/// let response = external_service::fetch_data().await
/// let response = 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("Status") {
/// if let Some(arg) = instance.argument("Metadata") {
/// if let Some(var) = arg.get_variable_instance() {
/// var.set_value(StateValue::String(response.status));
/// }
/// }
///
/// if let Some(arg) = instance.argument("Message") {
/// if let Some(var) = arg.get_variable_instance() {
/// var.set_value(StateValue::String(response.message));
/// var.set_value(StateValue::String(response.metadata));
/// }
/// }
///
@@ -276,7 +283,7 @@ pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>, ActionD
/// // Contexte partagé (ex: état d'un lecteur média)
/// let player_state = Arc::new(Mutex::new(PlayerState::Stopped));
///
/// let handler = action_handler!(|instance, data| {
/// let handler = action_handler!(|instance| {
/// // Vérifier l'état actuel
/// {
/// let state = player_state.lock().await;
@@ -309,8 +316,8 @@ pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>, ActionD
/// - Le résultat est automatiquement boxé et arcé
#[macro_export]
macro_rules! action_handler {
(|$instance:ident, $data:ident| $body:block) => {
std::sync::Arc::new(|$instance: std::sync::Arc<$crate::actions::ActionInstance>, $data: $crate::actions::ActionData| {
(|$instance:ident| $body:block) => {
std::sync::Arc::new(|$instance: std::sync::Arc<$crate::actions::ActionInstance>| {
Box::pin(async move $body)
})
};

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use tracing::debug;
use tracing::{debug, trace};
use xmltree::{Element, XMLNode};
use crate::{
@@ -165,9 +165,10 @@ impl ActionInstance {
/// Exécute l'action avec les données fournies.
///
/// Cette méthode :
/// 1. Exécute le handler avec les données d'entrée
/// 2. Collecte automatiquement les valeurs OUT via [`get_out_values()`](Self::get_out_values)
/// 3. Retourne les résultats
/// 1. Stocke les valeurs IN dans les variables liées
/// 2. Exécute le handler (qui peut accéder aux valeurs IN via les variables)
/// 3. Collecte automatiquement les valeurs OUT via [`get_out_values()`](Self::get_out_values)
/// 4. Retourne les résultats
///
/// # Arguments
///
@@ -185,9 +186,13 @@ impl ActionInstance {
///
/// # Fonctionnement
///
/// Le handler n'a pas besoin de retourner les valeurs OUT - il modifie simplement
/// les variables d'instance et retourne `Ok(())`. La méthode `run()` collecte automatiquement
/// toutes les valeurs des arguments marqués comme OUT si le handler réussit.
/// 1. Pour chaque argument IN, la valeur fournie dans `data` est stockée dans la
/// variable d'état liée à cet argument
/// 2. Le handler est appelé avec l'instance (il peut lire les valeurs IN via
/// `argument.get_variable_instance().value()`)
/// 3. Le handler modifie les variables selon ses besoins et retourne `Ok(())` ou `Err(...)`
/// 4. Si le handler réussit, `run()` collecte automatiquement toutes les valeurs
/// des arguments marqués comme OUT
///
/// # Examples
///
@@ -206,8 +211,10 @@ impl ActionInstance {
/// pmoupnp::variable_types::StateValue::UI2(50));
/// let input_data = Arc::new(input);
///
/// // Exécuter l'action - le handler modifie CurrentVolume
/// // run() retourne automatiquement CurrentVolume dans les OUT
/// // Exécuter l'action
/// // 1. run() stocke DesiredVolume=50 dans la variable liée
/// // 2. Le handler lit la valeur et fait son travail
/// // 3. run() retourne automatiquement les valeurs OUT
/// match instance.run(input_data).await {
/// Ok(output_data) => {
/// // Traiter les résultats
@@ -224,15 +231,30 @@ impl ActionInstance {
///
/// # Notes
///
/// - Les valeurs IN sont automatiquement stockées avant l'appel du handler
/// - Le handler n'a plus besoin de recevoir les données en paramètre
/// - Le handler modifie les variables et retourne `Ok(())` ou `Err(ActionError)`
/// - `run()` collecte automatiquement les OUT si le handler retourne `Ok(())`
/// - L'instance doit être wrappée dans un `Arc` pour être passée au handler
pub async fn run(self: Arc<Self>, data: ActionData) -> Result<ActionData, crate::actions::ActionError> {
// Stocker les valeurs IN dans les variables liées
for arg_inst in self.arguments.all() {
let arg_model = arg_inst.as_ref().get_model();
if arg_model.is_in() {
if let Some(value) = data.get(arg_inst.get_name()) {
if let Some(var_inst) = arg_inst.get_variable_instance() {
var_inst.set_value(value.clone());
trace!(" IN {} = {:?}", arg_inst.get_name(), value);
}
}
}
}
let handler = self.model.handler().clone();
let instance_clone = self.clone();
// Exécuter le handler
handler(instance_clone, data).await?;
// Exécuter le handler (il peut maintenant lire les valeurs IN depuis les variables)
handler(instance_clone).await?;
// Collecter automatiquement les valeurs OUT si succès
debug!("✅ Action '{}' completed successfully, collecting outputs", self.get_name());

View File

@@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, trace};
@@ -11,11 +10,9 @@ use crate::{
UpnpObjectSetError,
UpnpObjectType,
UpnpTyped,
UpnpTypedInstance,
};
use crate::actions::{
Action,
ActionData,
ActionHandler,
ActionInstance,
Argument,
@@ -56,7 +53,9 @@ impl Action {
///
/// Ce handler logge simplement l'appel et les arguments d'entrée.
/// La méthode [`ActionInstance::run()`](crate::actions::ActionInstance::run) s'occupe
/// automatiquement de collecter les valeurs OUT après l'exécution.
/// 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
///
@@ -65,7 +64,7 @@ impl Action {
/// # Comportement
///
/// - Logge le nom de l'action
/// - Logge les arguments IN avec leurs valeurs
/// - Logge les arguments IN avec leurs valeurs (lues depuis les variables liées)
/// - Ne fait aucune modification (handler passif)
///
/// # Note
@@ -73,17 +72,17 @@ impl Action {
/// Ce handler est automatiquement assigné lors de la création d'une action.
/// Il peut être remplacé via [`set_handler`](Self::set_handler).
fn default_handler() -> ActionHandler {
action_handler!(|instance, data| {
action_handler!(|instance| {
use crate::UpnpTypedInstance;
debug!("🎬 Action '{}' called", instance.get_name());
// Logger les arguments d'entrée
// 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(value) = data.get(arg_inst.get_name()) {
trace!(" IN {} = {:?}", arg_inst.get_name(), value);
if let Some(var_inst) = arg_inst.get_variable_instance() {
trace!(" IN {} = {:?}", arg_inst.get_name(), var_inst.value());
}
}
}

View File

@@ -137,10 +137,10 @@ impl UpnpInstance for ArgumentInstance {
name: from.get_name().clone(),
object_type: "ArgumentInstance".to_string(),
},
// Clone du modèle pour référence future
model: from.clone(),
// Initialisation à None - sera lié plus tard via bind_variable()
// Arc<RwLock<...>> permet la modification thread-safe post-construction
variable_instance: Arc::new(RwLock::new(None)),

View File

@@ -106,6 +106,7 @@ pub type ArgumentSet = UpnpObjectSet<Argument>;
/// 1. **Création** : Instanciation via [`UpnpInstance::new`] avec `variable_instance = None`
/// 2. **Liaison** : Association à une [`StateVarInstance`] via [`bind_variable`](Self::bind_variable)
/// 3. **Utilisation** : Accès à la valeur runtime via [`get_variable_instance`](Self::get_variable_instance)
/// 4. **Exécution** : Les valeurs IN sont stockées dans les variables liées lors de l'appel à `run()`
///
/// # Pourquoi `variable_instance` est optionnel ?
///
@@ -114,6 +115,13 @@ pub type ArgumentSet = UpnpObjectSet<Argument>;
/// - Les `ActionInstance` sont créées **avant** que toutes les variables soient disponibles
/// - La validation des dépendances se fait en deux phases
///
/// # Stockage des valeurs IN
///
/// Lors de l'exécution d'une action, les valeurs des arguments IN sont automatiquement
/// stockées dans les `StateVarInstance` liées. Les handlers peuvent ensuite y accéder
/// via `argument.get_variable_instance().value()` sans avoir besoin de recevoir les
/// valeurs en paramètre.
///
/// # Thread-safety
///
/// Le champ `variable_instance` est protégé par un `RwLock` pour permettre :