mise à jours des handlers d'actions

This commit is contained in:
2025-10-16 21:06:29 +02:00
parent 8ccb3a26ec
commit 5eef699a60
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 /// # Signature
/// ///
/// ```ignore /// ```ignore
/// Fn(Arc<ActionInstance>, ActionData) -> ActionFuture /// Fn(Arc<ActionInstance>) -> ActionFuture
/// ``` /// ```
/// ///
/// Prend : /// Prend :
/// - [`Arc<ActionInstance>`](crate::actions::ActionInstance) : L'instance de l'action avec accès aux variables liées /// - [`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>`. /// Retourne un [`ActionFuture`] qui se résout en `Result<(), ActionError>`.
/// ///
/// # Responsabilités /// # Responsabilités
/// ///
/// Le handler est responsable de : /// 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 /// - Exécuter la logique métier
/// - Modifier les variables d'instance selon les besoins /// - Modifier les variables d'instance selon les besoins
/// - Retourner `Ok(())` en cas de succès ou `Err(ActionError)` en cas d'erreur /// - 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 /// 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 /// # Traits requis
/// ///
@@ -147,8 +149,8 @@ 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, data| { /// let handler = action_handler!(|instance| {
/// // Logique métier /// // Logique métier - les valeurs IN sont déjà dans les variables
/// Ok::<(), ActionError>(()) /// Ok::<(), ActionError>(())
/// }); /// });
/// ``` /// ```
@@ -156,10 +158,10 @@ pub type ActionFuture = Pin<Box<dyn Future<Output = Result<(), crate::actions::A
/// ## Manuellement /// ## Manuellement
/// ///
/// ```rust /// ```rust
/// use pmoupnp::actions::{ActionData, ActionHandler, ActionInstance, ActionError}; /// use pmoupnp::actions::{ActionHandler, ActionInstance, ActionError};
/// use std::sync::Arc; /// use std::sync::Arc;
/// ///
/// let handler: ActionHandler = Arc::new(|instance, data| { /// let handler: ActionHandler = Arc::new(|instance| {
/// Box::pin(async move { /// Box::pin(async move {
/// // Votre logique async /// // Votre logique async
/// Ok::<(), ActionError>(()) /// 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 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>, ActionData) -> ActionFuture + Send + Sync>; pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>) -> ActionFuture + Send + Sync>;
/// Macro pour créer facilement un ActionHandler. /// Macro pour créer facilement un ActionHandler.
/// ///
@@ -184,16 +186,16 @@ pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>, ActionD
/// # Syntaxe /// # Syntaxe
/// ///
/// ```ignore /// ```ignore
/// action_handler!(|instance, data| { /// action_handler!(|instance| {
/// // votre logique async (automatiquement dans un bloc async move) /// // votre logique async (automatiquement dans un bloc async move)
/// data /// // Les valeurs IN sont déjà disponibles dans les variables liées
/// }) /// })
/// ``` /// ```
/// ///
/// # Arguments /// # Arguments
/// ///
/// - `instance` : Paramètre de type `Arc<`[`ActionInstance`](crate::actions::ActionInstance)`>` - L'instance de l'action /// - `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`) /// - Le corps du bloc peut contenir du code asynchrone (`.await`)
/// ///
/// # Type de retour /// # Type de retour
@@ -208,56 +210,61 @@ pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>, ActionD
/// use pmoupnp::action_handler; /// use pmoupnp::action_handler;
/// ///
/// // Handler minimal - run() collectera automatiquement les OUT /// // Handler minimal - run() collectera automatiquement les OUT
/// let handler = action_handler!(|instance, data| { /// let handler = action_handler!(|instance| {
/// Ok(()) // Succès, pas d'erreur /// 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 /// ```ignore
/// use pmoupnp::action_handler; /// use pmoupnp::action_handler;
/// use pmoupnp::actions::ActionError; /// use pmoupnp::actions::ActionError;
/// ///
/// let handler = action_handler!(|instance, data| { /// let handler = action_handler!(|instance| {
/// // Lire un argument d'entrée /// // Lire un argument d'entrée depuis la variable liée
/// let volume = data.get("DesiredVolume") /// let arg = instance.argument("DesiredVolume")
/// .ok_or_else(|| ActionError::MissingArgument("DesiredVolume".to_string()))?; /// .ok_or_else(|| ActionError::ArgumentNotFound("DesiredVolume".to_string()))?;
///
/// // Modifier la variable d'instance
/// let arg = instance.argument("CurrentVolume")
/// .ok_or_else(|| ActionError::ArgumentNotFound("CurrentVolume".to_string()))?;
/// ///
/// let var = arg.get_variable_instance() /// let var = arg.get_variable_instance()
/// .ok_or_else(|| ActionError::VariableNotBound)?; /// .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 /// 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 /// ```ignore
/// use pmoupnp::action_handler; /// use pmoupnp::action_handler;
/// use pmoupnp::actions::ActionError; /// 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 /// // 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()))?; /// .map_err(|e| ActionError::ExternalError(e.to_string()))?;
/// ///
/// // Mettre à jour les variables selon la réponse /// // 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() { /// if let Some(var) = arg.get_variable_instance() {
/// var.set_value(StateValue::String(response.status)); /// var.set_value(StateValue::String(response.metadata));
/// }
/// }
///
/// if let Some(arg) = instance.argument("Message") {
/// if let Some(var) = arg.get_variable_instance() {
/// var.set_value(StateValue::String(response.message));
/// } /// }
/// } /// }
/// ///
@@ -276,7 +283,7 @@ pub type ActionHandler = Arc<dyn Fn(Arc<crate::actions::ActionInstance>, ActionD
/// // Contexte partagé (ex: état d'un lecteur média) /// // Contexte partagé (ex: état d'un lecteur média)
/// let player_state = Arc::new(Mutex::new(PlayerState::Stopped)); /// 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 /// // Vérifier l'état actuel
/// { /// {
/// let state = player_state.lock().await; /// 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é /// - Le résultat est automatiquement boxé et arcé
#[macro_export] #[macro_export]
macro_rules! action_handler { macro_rules! action_handler {
(|$instance:ident, $data:ident| $body:block) => { (|$instance:ident| $body:block) => {
std::sync::Arc::new(|$instance: std::sync::Arc<$crate::actions::ActionInstance>, $data: $crate::actions::ActionData| { std::sync::Arc::new(|$instance: std::sync::Arc<$crate::actions::ActionInstance>| {
Box::pin(async move $body) Box::pin(async move $body)
}) })
}; };

View File

@@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use tracing::debug; use tracing::{debug, trace};
use xmltree::{Element, XMLNode}; use xmltree::{Element, XMLNode};
use crate::{ use crate::{
@@ -165,9 +165,10 @@ impl ActionInstance {
/// Exécute l'action avec les données fournies. /// Exécute l'action avec les données fournies.
/// ///
/// Cette méthode : /// Cette méthode :
/// 1. Exécute le handler avec les données d'entrée /// 1. Stocke les valeurs IN dans les variables liées
/// 2. Collecte automatiquement les valeurs OUT via [`get_out_values()`](Self::get_out_values) /// 2. Exécute le handler (qui peut accéder aux valeurs IN via les variables)
/// 3. Retourne les résultats /// 3. Collecte automatiquement les valeurs OUT via [`get_out_values()`](Self::get_out_values)
/// 4. Retourne les résultats
/// ///
/// # Arguments /// # Arguments
/// ///
@@ -185,9 +186,13 @@ impl ActionInstance {
/// ///
/// # Fonctionnement /// # Fonctionnement
/// ///
/// Le handler n'a pas besoin de retourner les valeurs OUT - il modifie simplement /// 1. Pour chaque argument IN, la valeur fournie dans `data` est stockée dans la
/// les variables d'instance et retourne `Ok(())`. La méthode `run()` collecte automatiquement /// variable d'état liée à cet argument
/// toutes les valeurs des arguments marqués comme OUT si le handler réussit. /// 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 /// # Examples
/// ///
@@ -206,8 +211,10 @@ impl ActionInstance {
/// pmoupnp::variable_types::StateValue::UI2(50)); /// pmoupnp::variable_types::StateValue::UI2(50));
/// let input_data = Arc::new(input); /// let input_data = Arc::new(input);
/// ///
/// // Exécuter l'action - le handler modifie CurrentVolume /// // Exécuter l'action
/// // run() retourne automatiquement CurrentVolume dans les OUT /// // 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 { /// match instance.run(input_data).await {
/// Ok(output_data) => { /// Ok(output_data) => {
/// // Traiter les résultats /// // Traiter les résultats
@@ -224,15 +231,30 @@ impl ActionInstance {
/// ///
/// # Notes /// # 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)` /// - Le handler modifie les variables et retourne `Ok(())` ou `Err(ActionError)`
/// - `run()` collecte automatiquement les OUT si le handler retourne `Ok(())` /// - `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 /// - 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> { 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 handler = self.model.handler().clone();
let instance_clone = self.clone(); let instance_clone = self.clone();
// Exécuter le handler // Exécuter le handler (il peut maintenant lire les valeurs IN depuis les variables)
handler(instance_clone, data).await?; handler(instance_clone).await?;
// Collecter automatiquement les valeurs OUT si succès // Collecter automatiquement les valeurs OUT si succès
debug!("✅ Action '{}' completed successfully, collecting outputs", self.get_name()); debug!("✅ Action '{}' completed successfully, collecting outputs", self.get_name());

View File

@@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use tracing::{debug, trace}; use tracing::{debug, trace};
@@ -11,11 +10,9 @@ use crate::{
UpnpObjectSetError, UpnpObjectSetError,
UpnpObjectType, UpnpObjectType,
UpnpTyped, UpnpTyped,
UpnpTypedInstance,
}; };
use crate::actions::{ use crate::actions::{
Action, Action,
ActionData,
ActionHandler, ActionHandler,
ActionInstance, ActionInstance,
Argument, Argument,
@@ -56,7 +53,9 @@ impl Action {
/// ///
/// Ce handler logge simplement l'appel et les arguments d'entrée. /// Ce handler logge simplement l'appel et les arguments d'entrée.
/// La méthode [`ActionInstance::run()`](crate::actions::ActionInstance::run) s'occupe /// 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 /// # Returns
/// ///
@@ -65,7 +64,7 @@ impl Action {
/// # Comportement /// # Comportement
/// ///
/// - Logge le nom de l'action /// - 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) /// - Ne fait aucune modification (handler passif)
/// ///
/// # Note /// # Note
@@ -73,17 +72,17 @@ impl Action {
/// 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, data| { action_handler!(|instance| {
use crate::UpnpTypedInstance; use crate::UpnpTypedInstance;
debug!("🎬 Action '{}' called", instance.get_name()); 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() { for arg_inst in instance.arguments_set().all() {
let arg_model = arg_inst.as_ref().get_model(); let arg_model = arg_inst.as_ref().get_model();
if arg_model.is_in() { 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() {
trace!(" IN {} = {:?}", arg_inst.get_name(), value); trace!(" IN {} = {:?}", arg_inst.get_name(), var_inst.value());
} }
} }
} }

View File

@@ -137,10 +137,10 @@ impl UpnpInstance for ArgumentInstance {
name: from.get_name().clone(), name: from.get_name().clone(),
object_type: "ArgumentInstance".to_string(), object_type: "ArgumentInstance".to_string(),
}, },
// Clone du modèle pour référence future // Clone du modèle pour référence future
model: from.clone(), model: from.clone(),
// Initialisation à None - sera lié plus tard via bind_variable() // Initialisation à None - sera lié plus tard via bind_variable()
// Arc<RwLock<...>> permet la modification thread-safe post-construction // Arc<RwLock<...>> permet la modification thread-safe post-construction
variable_instance: Arc::new(RwLock::new(None)), 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` /// 1. **Création** : Instanciation via [`UpnpInstance::new`] avec `variable_instance = None`
/// 2. **Liaison** : Association à une [`StateVarInstance`] via [`bind_variable`](Self::bind_variable) /// 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) /// 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 ? /// # 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 /// - Les `ActionInstance` sont créées **avant** que toutes les variables soient disponibles
/// - La validation des dépendances se fait en deux phases /// - 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 /// # Thread-safety
/// ///
/// Le champ `variable_instance` est protégé par un `RwLock` pour permettre : /// Le champ `variable_instance` est protégé par un `RwLock` pour permettre :