test ollama code
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.ollama
|
||||
.ollamacode
|
||||
/bin/
|
||||
/pkg/
|
||||
/vendor/
|
||||
|
||||
314
pmoupnp/src/actions/action_handler.rs
Normal file
314
pmoupnp/src/actions/action_handler.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
//! Types et utilitaires pour les handlers d'actions UPnP.
|
||||
//!
|
||||
//! Ce module définit les types fondamentaux pour gérer l'exécution
|
||||
//! asynchrone des actions UPnP.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! Les actions UPnP sont exécutées de manière asynchrone via des handlers
|
||||
//! qui prennent des données en entrée et retournent des données en sortie.
|
||||
//!
|
||||
//! ```text
|
||||
//! ActionData (input)
|
||||
//! ↓
|
||||
//! ActionHandler (async processing)
|
||||
//! ↓
|
||||
//! ActionData (output)
|
||||
//! ```
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pmoupnp::action_handler;
|
||||
//! use std::collections::HashMap;
|
||||
//! use std::sync::Arc;
|
||||
//!
|
||||
//! // Créer un handler avec la macro
|
||||
//! let handler = action_handler!(|data| {
|
||||
//! // Traiter les données
|
||||
//! data
|
||||
//! });
|
||||
//!
|
||||
//! // Ou manuellement
|
||||
//! use pmoupnp::actions::{ActionData, ActionHandler};
|
||||
//! let manual_handler: ActionHandler = Arc::new(|data| {
|
||||
//! Box::pin(async move {
|
||||
//! data
|
||||
//! })
|
||||
//! });
|
||||
//! ```
|
||||
|
||||
use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
|
||||
|
||||
use crate::variable_types::StateValue;
|
||||
|
||||
/// Données d'une action UPnP.
|
||||
///
|
||||
/// Représente un ensemble de paramètres clé-valeur pour une action UPnP,
|
||||
/// partagé via `Arc` pour permettre un clonage efficace.
|
||||
///
|
||||
/// # Structure
|
||||
///
|
||||
/// - **Clé** : Nom du paramètre (ex: "InstanceID", "TransportURI")
|
||||
/// - **Valeur** : Valeur typée du paramètre ([`StateValue`])
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```rust
|
||||
/// use pmoupnp::actions::ActionData;
|
||||
/// use pmoupnp::variable_types::StateValue;
|
||||
/// use std::collections::HashMap;
|
||||
/// use std::sync::Arc;
|
||||
///
|
||||
/// let mut data = HashMap::new();
|
||||
/// data.insert("InstanceID".to_string(), StateValue::UI4(0));
|
||||
/// data.insert("Speed".to_string(), StateValue::String("1".to_string()));
|
||||
///
|
||||
/// let action_data: ActionData = Arc::new(data);
|
||||
///
|
||||
/// // Le Arc permet un clonage efficace
|
||||
/// let cloned = action_data.clone();
|
||||
/// ```
|
||||
///
|
||||
/// # 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>>;
|
||||
|
||||
/// Future retourné par un [`ActionHandler`].
|
||||
///
|
||||
/// Ce type représente le résultat asynchrone d'un handler d'action.
|
||||
/// Il est boxé et pinné pour permettre le polymorphisme et la manipulation
|
||||
/// sûre des futures.
|
||||
///
|
||||
/// # Type complet
|
||||
///
|
||||
/// ```ignore
|
||||
/// Pin<Box<dyn Future<Output = Result<(), 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
|
||||
/// - `+ 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
|
||||
/// - 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>>;
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// # Signature
|
||||
///
|
||||
/// ```ignore
|
||||
/// Fn(Arc<ActionInstance>, ActionData) -> 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)
|
||||
///
|
||||
/// 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`
|
||||
/// - 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(())`.
|
||||
///
|
||||
/// # Traits requis
|
||||
///
|
||||
/// - `Send` : Le handler peut être envoyé entre threads
|
||||
/// - `Sync` : Le handler peut être partagé entre threads
|
||||
/// - `Arc` : Permet le partage sans copie
|
||||
///
|
||||
/// # Création
|
||||
///
|
||||
/// ## Avec la macro (recommandé)
|
||||
///
|
||||
/// ```rust
|
||||
/// use pmoupnp::action_handler;
|
||||
///
|
||||
/// let handler = action_handler!(|instance, data| {
|
||||
/// // Logique métier - pas besoin de retourner quoi que ce soit
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ## Manuellement
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pmoupnp::actions::{ActionData, ActionHandler, ActionInstance};
|
||||
/// use std::sync::Arc;
|
||||
///
|
||||
/// let handler: ActionHandler = Arc::new(|instance, data| {
|
||||
/// Box::pin(async move {
|
||||
/// // Votre logique async
|
||||
/// // Pas de return nécessaire
|
||||
/// })
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// # 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 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>;
|
||||
|
||||
/// Macro pour créer facilement un ActionHandler.
|
||||
///
|
||||
/// Cette macro simplifie la création d'handlers asynchrones en cachant
|
||||
/// la complexité de `Arc`, `Box::pin`, et `async move`.
|
||||
///
|
||||
/// # Syntaxe
|
||||
///
|
||||
/// ```ignore
|
||||
/// action_handler!(|instance, data| {
|
||||
/// // votre logique async (automatiquement dans un bloc async move)
|
||||
/// data
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// # 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
|
||||
/// - Le corps du bloc peut contenir du code asynchrone (`.await`)
|
||||
///
|
||||
/// # Type de retour
|
||||
///
|
||||
/// La macro retourne un [`ActionHandler`] prêt à l'emploi.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Exemple 1 : Handler simple (ne fait rien)
|
||||
///
|
||||
/// ```ignore
|
||||
/// use pmoupnp::action_handler;
|
||||
///
|
||||
/// // Handler minimal - run() collectera automatiquement les OUT
|
||||
/// let handler = action_handler!(|instance, data| {
|
||||
/// Ok(()) // Succès, pas d'erreur
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ## Exemple 2 : Handler qui modifie une variable avec gestion d'erreur
|
||||
///
|
||||
/// ```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 var = arg.get_variable_instance()
|
||||
/// .ok_or_else(|| ActionError::VariableNotBound)?;
|
||||
///
|
||||
/// var.set_value(volume.clone());
|
||||
///
|
||||
/// Ok(()) // Succès - run() collectera CurrentVolume dans les OUT
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ## Exemple 3 : Handler avec logique métier asynchrone et gestion d'erreur
|
||||
///
|
||||
/// ```ignore
|
||||
/// use pmoupnp::action_handler;
|
||||
/// use pmoupnp::actions::ActionError;
|
||||
///
|
||||
/// let handler = action_handler!(|instance, data| {
|
||||
/// // Appel asynchrone à un service externe
|
||||
/// let response = external_service::fetch_data().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(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));
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ## Exemple 4 : Handler avec capture de contexte et validation
|
||||
///
|
||||
/// ```ignore
|
||||
/// use pmoupnp::action_handler;
|
||||
/// use pmoupnp::actions::ActionError;
|
||||
/// use std::sync::Arc;
|
||||
/// use tokio::sync::Mutex;
|
||||
///
|
||||
/// // Contexte partagé (ex: état d'un lecteur média)
|
||||
/// let player_state = Arc::new(Mutex::new(PlayerState::Stopped));
|
||||
///
|
||||
/// let handler = action_handler!(|instance, data| {
|
||||
/// // Vérifier l'état actuel
|
||||
/// {
|
||||
/// let state = player_state.lock().await;
|
||||
/// if *state == PlayerState::Error {
|
||||
/// return Err(ActionError::InvalidState("Player in error state".to_string()));
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Modifier l'état du lecteur
|
||||
/// {
|
||||
/// 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()));
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// # Notes d'implémentation
|
||||
///
|
||||
/// - 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é
|
||||
#[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| {
|
||||
Box::pin(async move $body)
|
||||
})
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use tracing::debug;
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::actions::Action;
|
||||
use crate::actions::ArgInstanceSet;
|
||||
use crate::actions::ActionInstance;
|
||||
use crate::UpnpInstance;
|
||||
use crate::UpnpObject;
|
||||
use crate::UpnpTyped;
|
||||
use crate::UpnpTypedInstance;
|
||||
use crate::UpnpObjectType;
|
||||
use crate::{
|
||||
UpnpInstance,
|
||||
UpnpObject,
|
||||
UpnpObjectType,
|
||||
UpnpTyped,
|
||||
UpnpTypedInstance,
|
||||
};
|
||||
use crate::actions::{
|
||||
Action,
|
||||
ActionData,
|
||||
ActionInstance,
|
||||
ArgInstanceSet,
|
||||
};
|
||||
|
||||
impl UpnpObject for ActionInstance {
|
||||
fn to_xml_element(&self) -> Element {
|
||||
@@ -102,6 +108,136 @@ impl ActionInstance {
|
||||
pub fn arguments_set(&self) -> &ArgInstanceSet {
|
||||
&self.arguments // ⬅️ Retourne les INSTANCES, pas les modèles !
|
||||
}
|
||||
|
||||
/// Récupère les valeurs de tous les arguments de sortie (OUT).
|
||||
///
|
||||
/// Cette méthode collecte automatiquement les valeurs actuelles de toutes
|
||||
/// les variables d'état liées aux arguments OUT et les retourne dans un
|
||||
/// [`ActionData`] indexé par le nom de chaque argument.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Un [`ActionData`] contenant les paires (nom_argument, valeur_variable) pour
|
||||
/// tous les arguments de sortie qui ont une variable d'instance liée.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pmoupnp::actions::Action;
|
||||
/// # use pmoupnp::UpnpInstance;
|
||||
/// # use std::sync::Arc;
|
||||
/// let action = Action::new("GetVolume".to_string());
|
||||
/// let instance = Arc::new(ActionInstance::new(&action));
|
||||
///
|
||||
/// // Récupérer automatiquement toutes les valeurs OUT
|
||||
/// let output = instance.get_out_values();
|
||||
///
|
||||
/// // Afficher les résultats
|
||||
/// for (arg_name, value) in output.iter() {
|
||||
/// println!("{} = {:?}", arg_name, value);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// - Seuls les arguments marqués comme OUT sont inclus
|
||||
/// - Les arguments sans variable d'instance liée sont ignorés
|
||||
/// - Le nom de l'argument (pas le nom de la variable) est utilisé comme clé
|
||||
/// - Cette méthode est utilisée par le handler par défaut
|
||||
pub fn get_out_values(&self) -> ActionData {
|
||||
use std::collections::HashMap;
|
||||
use crate::UpnpTypedInstance;
|
||||
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for arg_inst in self.arguments.all() {
|
||||
let arg_model = arg_inst.as_ref().get_model();
|
||||
if arg_model.is_out() {
|
||||
if let Some(var_inst) = arg_inst.get_variable_instance() {
|
||||
result.insert(arg_inst.get_name().to_string(), var_inst.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Arc::new(result)
|
||||
}
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `data` - Données d'entrée de l'action (arguments IN)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Un `Future` qui se résout en `Result<ActionData, ActionError>` :
|
||||
/// - `Ok(ActionData)` contenant les résultats (arguments OUT) si le handler réussit
|
||||
/// - `Err(ActionError)` si le handler échoue
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Retourne une erreur si le handler retourne `Err(ActionError)`.
|
||||
///
|
||||
/// # 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.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pmoupnp::actions::{Action, ActionData};
|
||||
/// # use pmoupnp::UpnpInstance;
|
||||
/// # use std::collections::HashMap;
|
||||
/// # use std::sync::Arc;
|
||||
/// # async fn example() {
|
||||
/// let action = Action::new("SetVolume".to_string());
|
||||
/// let instance = Arc::new(ActionInstance::new(&action));
|
||||
///
|
||||
/// // Préparer les données d'entrée
|
||||
/// let mut input = HashMap::new();
|
||||
/// input.insert("DesiredVolume".to_string(),
|
||||
/// 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
|
||||
/// match instance.run(input_data).await {
|
||||
/// Ok(output_data) => {
|
||||
/// // Traiter les résultats
|
||||
/// for (key, value) in output_data.iter() {
|
||||
/// println!("{} = {:?}", key, value);
|
||||
/// }
|
||||
/// }
|
||||
/// Err(e) => {
|
||||
/// eprintln!("Action failed: {:?}", e);
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// - 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> {
|
||||
let handler = self.model.handler().clone();
|
||||
let instance_clone = self.clone();
|
||||
|
||||
// Exécuter le handler
|
||||
handler(instance_clone, data).await?;
|
||||
|
||||
// Collecter automatiquement les valeurs OUT si succès
|
||||
debug!("✅ Action '{}' completed successfully, collecting outputs", self.get_name());
|
||||
Ok(self.get_out_values())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tracing::{debug, trace};
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::UpnpModel;
|
||||
use crate::UpnpObject;
|
||||
use crate::UpnpObjectSetError;
|
||||
use crate::UpnpObjectType;
|
||||
use crate::UpnpTyped;
|
||||
use crate::actions::Action;
|
||||
use crate::actions::ActionInstance;
|
||||
use crate::actions::Argument;
|
||||
use crate::actions::ArgumentSet;
|
||||
use crate::{
|
||||
action_handler,
|
||||
UpnpModel,
|
||||
UpnpObject,
|
||||
UpnpObjectSetError,
|
||||
UpnpObjectType,
|
||||
UpnpTyped,
|
||||
UpnpTypedInstance,
|
||||
};
|
||||
use crate::actions::{
|
||||
Action,
|
||||
ActionData,
|
||||
ActionHandler,
|
||||
ActionInstance,
|
||||
Argument,
|
||||
ArgumentSet,
|
||||
};
|
||||
|
||||
impl UpnpObject for Action {
|
||||
fn to_xml_element(&self) -> Element {
|
||||
@@ -42,6 +52,61 @@ 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 collecter les valeurs OUT après l'exécution.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Un [`ActionHandler`] qui logge les entrées.
|
||||
///
|
||||
/// # Comportement
|
||||
///
|
||||
/// - Logge le nom de l'action
|
||||
/// - Logge les arguments IN avec leurs valeurs
|
||||
/// - Ne fait aucune modification (handler passif)
|
||||
///
|
||||
/// # 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, data| {
|
||||
use crate::UpnpTypedInstance;
|
||||
|
||||
debug!("🎬 Action '{}' called", instance.get_name());
|
||||
|
||||
// Logger les arguments d'entrée
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(()) // Succès - handler par défaut ne fait rien d'autre
|
||||
})
|
||||
}
|
||||
|
||||
/// Crée une nouvelle action UPnP.
|
||||
///
|
||||
/// L'action est initialisée avec un handler par défaut qui logge les entrées
|
||||
/// et retourne les valeurs des variables d'instance pour les arguments de sortie.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - Nom de l'action
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pmoupnp::actions::Action;
|
||||
/// let mut action = Action::new("Play".to_string());
|
||||
/// ```
|
||||
pub fn new(name: String) -> Action {
|
||||
Self {
|
||||
object: UpnpObjectType {
|
||||
@@ -49,14 +114,56 @@ impl Action {
|
||||
object_type: "Action".to_string(),
|
||||
},
|
||||
arguments: ArgumentSet::new(),
|
||||
handle: Self::default_handler(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ajoute un argument à l'action.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `arg` - Argument à ajouter
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Retourne une erreur si un argument avec le même nom existe déjà.
|
||||
pub fn add_argument(&mut self, arg: Arc<Argument>) -> Result<(), UpnpObjectSetError> {
|
||||
self.arguments.insert(arg)
|
||||
}
|
||||
|
||||
/// Retourne les arguments de l'action.
|
||||
pub fn arguments(&self) -> &ArgumentSet {
|
||||
&self.arguments
|
||||
}
|
||||
|
||||
/// Définit un handler personnalisé pour cette action.
|
||||
///
|
||||
/// Remplace le handler par défaut par un handler personnalisé.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `handler` - Le nouveau handler à utiliser
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pmoupnp::actions::Action;
|
||||
/// # use pmoupnp::action_handler;
|
||||
/// let mut action = Action::new("Play".to_string());
|
||||
///
|
||||
/// let custom_handler = action_handler!(|instance, data| {
|
||||
/// // Logique personnalisée
|
||||
/// data
|
||||
/// });
|
||||
///
|
||||
/// action.set_handler(custom_handler);
|
||||
/// ```
|
||||
pub fn set_handler(&mut self, handler: ActionHandler) {
|
||||
self.handle = handler;
|
||||
}
|
||||
|
||||
/// Retourne le handler de l'action.
|
||||
pub fn handler(&self) -> &ActionHandler {
|
||||
&self.handle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,21 @@
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Action avec handler personnalisé
|
||||
///
|
||||
/// ```ignore
|
||||
/// define_action! {
|
||||
/// pub static ACTION_NAME = "ActionName" {
|
||||
/// in "ParamName" => VARIABLE_REF,
|
||||
/// out "ResultParam" => RESULT_VAR,
|
||||
/// }
|
||||
/// with handler action_handler!(|instance, data| {
|
||||
/// // Logique personnalisée
|
||||
/// Ok(())
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `ACTION_NAME` : Nom de la constante statique Rust
|
||||
@@ -89,30 +104,42 @@
|
||||
/// - Initialisation paresseuse via `Lazy` (thread-safe)
|
||||
#[macro_export]
|
||||
macro_rules! define_action {
|
||||
// Variante sans arguments
|
||||
(pub static $name:ident = $action_name:literal) => {
|
||||
pub static $name: once_cell::sync::Lazy<std::sync::Arc<$crate::actions::Action>> =
|
||||
// Variante sans arguments avec handler optionnel
|
||||
(pub static $name:ident = $action_name:literal $(with handler $handler:expr)?) => {
|
||||
pub static $name: once_cell::sync::Lazy<std::sync::Arc<$crate::actions::Action>> =
|
||||
once_cell::sync::Lazy::new(|| {
|
||||
std::sync::Arc::new($crate::actions::Action::new($action_name.to_string()))
|
||||
let mut ac = $crate::actions::Action::new($action_name.to_string());
|
||||
|
||||
$(
|
||||
ac.set_handler($handler);
|
||||
)?
|
||||
|
||||
std::sync::Arc::new(ac)
|
||||
});
|
||||
};
|
||||
|
||||
// Variante avec arguments
|
||||
// Variante avec arguments et handler optionnel
|
||||
(pub static $name:ident = $action_name:literal {
|
||||
$(
|
||||
$direction:ident $arg_name:literal => $var_ref:expr
|
||||
),* $(,)?
|
||||
}) => {
|
||||
pub static $name: once_cell::sync::Lazy<std::sync::Arc<$crate::actions::Action>> =
|
||||
}
|
||||
$(with handler $handler:expr)?
|
||||
) => {
|
||||
pub static $name: once_cell::sync::Lazy<std::sync::Arc<$crate::actions::Action>> =
|
||||
once_cell::sync::Lazy::new(|| {
|
||||
let mut ac = $crate::actions::Action::new($action_name.to_string());
|
||||
|
||||
|
||||
$(
|
||||
ac.add_argument(
|
||||
define_action!(@arg $direction $arg_name, $var_ref)
|
||||
);
|
||||
)*
|
||||
|
||||
|
||||
$(
|
||||
ac.set_handler($handler);
|
||||
)?
|
||||
|
||||
std::sync::Arc::new(ac)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ mod errors;
|
||||
mod action_instance;
|
||||
mod action_instance_set;
|
||||
mod action_methods;
|
||||
mod action_handler;
|
||||
mod action_set_methods;
|
||||
mod arg_inst_set_methods;
|
||||
mod arg_instance_methods;
|
||||
@@ -18,11 +19,58 @@ use crate::{
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
pub use errors::ActionError;
|
||||
pub use action_handler::{ActionData, ActionFuture, ActionHandler};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Action UPnP.
|
||||
///
|
||||
/// Représente une opération invocable sur un service UPnP avec ses arguments
|
||||
/// et son handler d'exécution.
|
||||
///
|
||||
/// # Structure
|
||||
///
|
||||
/// - **Arguments** : Liste d'arguments d'entrée (IN) et de sortie (OUT)
|
||||
/// - **Handler** : Fonction asynchrone qui exécute l'action
|
||||
///
|
||||
/// # Handler par défaut
|
||||
///
|
||||
/// Chaque action est créée avec un handler par défaut qui :
|
||||
/// - Logge les valeurs des arguments d'entrée
|
||||
/// - Retourne les valeurs par défaut des arguments de sortie
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use pmoupnp::actions::Action;
|
||||
/// use pmoupnp::actions::Argument;
|
||||
/// use pmoupnp::state_variables::StateVariable;
|
||||
/// use pmoupnp::variable_types::StateVarType;
|
||||
/// use std::sync::Arc;
|
||||
///
|
||||
/// let mut action = Action::new("Play".to_string());
|
||||
///
|
||||
/// // Ajouter des arguments
|
||||
/// let instance_id = Arc::new(StateVariable::new(
|
||||
/// StateVarType::UI4,
|
||||
/// "InstanceID".to_string()
|
||||
/// ));
|
||||
/// let arg = Arc::new(Argument::new_in("InstanceID".to_string(), instance_id));
|
||||
/// action.add_argument(arg);
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct Action {
|
||||
object: UpnpObjectType,
|
||||
arguments: ArgumentSet,
|
||||
handle: ActionHandler,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Action {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Action")
|
||||
.field("object", &self.object)
|
||||
.field("arguments", &self.arguments)
|
||||
.field("handle", &"<ActionHandler>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub type ActionSet = UpnpObjectSet<Action>;
|
||||
|
||||
@@ -310,6 +310,8 @@ impl DeviceInstance {
|
||||
|
||||
/// Handler HTTP pour la description du device.
|
||||
async fn description_handler(&self) -> Response {
|
||||
tracing::info!("📋 Device description requested for {}", self.get_name());
|
||||
|
||||
let elem = self.description_element();
|
||||
|
||||
let config = EmitterConfig::new()
|
||||
@@ -318,12 +320,14 @@ impl DeviceInstance {
|
||||
|
||||
let mut xml_output = Vec::new();
|
||||
if let Err(e) = elem.write_with_config(&mut xml_output, config) {
|
||||
tracing::error!("Failed to serialize device description XML: {}", e);
|
||||
tracing::error!("❌ Failed to serialize device description XML: {}", e);
|
||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
}
|
||||
|
||||
let xml = String::from_utf8_lossy(&xml_output).to_string();
|
||||
|
||||
tracing::debug!("✅ Device description generated ({} bytes)", xml.len());
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
|
||||
@@ -5,7 +5,7 @@ use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::{
|
||||
devices::{Device, DeviceInstance},
|
||||
UpnpObject, UpnpModel, UpnpInstance,
|
||||
UpnpObject, UpnpModel, UpnpInstance, UpnpTyped,
|
||||
};
|
||||
|
||||
impl UpnpObject for Device {
|
||||
@@ -119,14 +119,20 @@ impl UpnpModel for Device {
|
||||
|
||||
/// Crée une instance du device avec ses services déjà instanciés.
|
||||
///
|
||||
/// Les services sont créés dans DeviceInstance::new(), cette méthode
|
||||
/// établit uniquement les liens bidirectionnels parent-enfant.
|
||||
/// Cette méthode :
|
||||
/// 1. Crée l'instance du device
|
||||
/// 2. Instancie tous les services du modèle
|
||||
/// 3. Établit les liens bidirectionnels parent-enfant
|
||||
fn create_instance(&self) -> Arc<DeviceInstance> {
|
||||
let instance = Arc::new(DeviceInstance::new(self));
|
||||
|
||||
// Établir le lien parent pour chaque service
|
||||
for service in instance.services() {
|
||||
service.set_device(Arc::clone(&instance));
|
||||
// Créer les instances de services depuis le modèle
|
||||
for service_model in self.services() {
|
||||
let service_instance = service_model.create_instance();
|
||||
service_instance.set_device(Arc::clone(&instance));
|
||||
if let Err(e) = instance.add_service(service_instance) {
|
||||
tracing::error!("Failed to add service instance: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
instance
|
||||
|
||||
@@ -42,7 +42,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::time;
|
||||
use tracing::{error, info, warn};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use xmltree::{Element, EmitterConfig, XMLNode};
|
||||
|
||||
use crate::{
|
||||
@@ -499,6 +499,19 @@ impl ServiceInstance {
|
||||
&self.actions
|
||||
}
|
||||
|
||||
/// Retourne une action par son nom.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - Nom de l'action
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Some(Arc<ActionInstance>)` si trouvée, `None` sinon.
|
||||
pub fn action(&self, name: &str) -> Option<Arc<crate::actions::ActionInstance>> {
|
||||
self.actions.get_by_name(name)
|
||||
}
|
||||
|
||||
/// Enregistre les routes UPnP dans le serveur.
|
||||
///
|
||||
/// # Errors
|
||||
@@ -534,7 +547,7 @@ impl ServiceInstance {
|
||||
.await;
|
||||
|
||||
// Handler control
|
||||
let instance_control = self.clone();
|
||||
let instance_control = Arc::new(self.clone());
|
||||
server
|
||||
.add_post_handler_with_state(&self.control_route(), control_handler, instance_control)
|
||||
.await;
|
||||
@@ -615,6 +628,8 @@ impl ServiceInstance {
|
||||
/// - Content-Type: `text/xml; charset="utf-8"`
|
||||
/// - Body: Document SCPD formaté avec indentation
|
||||
async fn scpd_handler(&self) -> Response {
|
||||
info!("📋 SCPD requested for service {}", self.get_name());
|
||||
|
||||
let elem = self.scpd_element();
|
||||
|
||||
let config = EmitterConfig::new()
|
||||
@@ -623,12 +638,14 @@ impl ServiceInstance {
|
||||
|
||||
let mut xml_output = Vec::new();
|
||||
if let Err(e) = elem.write_with_config(&mut xml_output, config) {
|
||||
error!("Failed to serialize SCPD XML: {}", e);
|
||||
error!("❌ Failed to serialize SCPD XML: {}", e);
|
||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
}
|
||||
|
||||
let xml = String::from_utf8_lossy(&xml_output).to_string();
|
||||
|
||||
debug!("✅ SCPD generated for {} ({} bytes)", self.get_name(), xml.len());
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(
|
||||
@@ -1047,43 +1064,148 @@ async fn event_sub_handler(
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `instance` - L'instance du service
|
||||
/// * `_body` - Corps de la requête SOAP (actuellement non utilisé)
|
||||
/// * `instance` - L'instance du service (Arc-wrapped)
|
||||
/// * `body` - Corps de la requête SOAP
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Une réponse SOAP avec le résultat de l'action.
|
||||
/// Une réponse SOAP avec le résultat de l'action, ou un SOAP fault en cas d'erreur.
|
||||
///
|
||||
/// # Note
|
||||
/// # Erreurs
|
||||
///
|
||||
/// Cette implémentation est actuellement un stub et retourne une réponse vide.
|
||||
/// Le parsing SOAP et l'exécution des actions doivent être implémentés.
|
||||
async fn control_handler(State(instance): State<ServiceInstance>, _body: String) -> Response {
|
||||
/// Retourne un SOAP fault dans les cas suivants :
|
||||
/// - Parsing SOAP invalide
|
||||
/// - Action non trouvée
|
||||
/// - Arguments invalides
|
||||
/// - Échec de l'exécution de l'action
|
||||
async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: String) -> Response {
|
||||
use crate::{
|
||||
soap::{parse_soap_action, build_soap_response, build_soap_fault, error_codes},
|
||||
variable_types::{StateValue, UpnpVarType},
|
||||
UpnpTypedInstance,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use tracing::debug;
|
||||
|
||||
info!("📡 Control request for {}", instance.get_name());
|
||||
|
||||
// TODO: Parser le SOAP et appeler l'action correspondante
|
||||
// Parser le SOAP pour extraire l'action et ses arguments
|
||||
let soap_action = match parse_soap_action(body.as_bytes()) {
|
||||
Ok(action) => action,
|
||||
Err(e) => {
|
||||
error!("❌ Failed to parse SOAP: {:?}", e);
|
||||
let fault_xml = build_soap_fault(
|
||||
"s:Client",
|
||||
"Invalid SOAP request",
|
||||
Some(error_codes::INVALID_ACTION),
|
||||
Some("The SOAP request could not be parsed")
|
||||
).unwrap_or_else(|_| String::from("<?xml version=\"1.0\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope>"));
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
fault_xml,
|
||||
).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let response_xml = format!(
|
||||
r#"<?xml version="1.0"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
|
||||
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||
<s:Body>
|
||||
<u:Response xmlns:u="{}">
|
||||
</u:Response>
|
||||
</s:Body>
|
||||
</s:Envelope>"#,
|
||||
instance.service_type()
|
||||
);
|
||||
debug!("🎬 Received SOAP action: {}", soap_action.name);
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
"text/xml; charset=\"utf-8\"",
|
||||
)],
|
||||
response_xml,
|
||||
)
|
||||
.into_response()
|
||||
// Trouver l'action correspondante dans l'instance
|
||||
let action_instance = match instance.action(&soap_action.name) {
|
||||
Some(action_inst) => action_inst,
|
||||
None => {
|
||||
error!("❌ Action not found: {}", soap_action.name);
|
||||
let fault_xml = build_soap_fault(
|
||||
"s:Client",
|
||||
"Invalid Action",
|
||||
Some(error_codes::INVALID_ACTION),
|
||||
Some(&format!("Action '{}' not found", soap_action.name))
|
||||
).unwrap_or_else(|_| String::from("<?xml version=\"1.0\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope>"));
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
fault_xml,
|
||||
).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Convertir les arguments SOAP (String) en ActionData (StateValue)
|
||||
let mut action_data = HashMap::new();
|
||||
for (arg_name, arg_value) in soap_action.args {
|
||||
// Trouver l'argument correspondant pour obtenir son type
|
||||
if let Some(arg_inst) = action_instance.argument(&arg_name) {
|
||||
if let Some(var_inst) = arg_inst.get_variable_instance() {
|
||||
let var_model = var_inst.as_ref().get_model();
|
||||
// Parser la valeur selon le type de la variable
|
||||
match StateValue::from_string(&arg_value, &var_model.as_state_var_type()) {
|
||||
Ok(value) => {
|
||||
action_data.insert(arg_name, value);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("❌ Failed to parse argument '{}': {:?}", arg_name, e);
|
||||
let fault_xml = build_soap_fault(
|
||||
"s:Client",
|
||||
"Invalid Arguments",
|
||||
Some(error_codes::ARGUMENT_VALUE_INVALID),
|
||||
Some(&format!("Invalid value for argument '{}'", arg_name))
|
||||
).unwrap_or_else(|_| String::from("<?xml version=\"1.0\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope>"));
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
fault_xml,
|
||||
).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let action_data = Arc::new(action_data);
|
||||
|
||||
// Exécuter l'action
|
||||
match action_instance.run(action_data).await {
|
||||
Ok(output_data) => {
|
||||
// Convertir les StateValue en String pour SOAP
|
||||
let mut soap_values = HashMap::new();
|
||||
for (key, value) in output_data.iter() {
|
||||
soap_values.insert(key.clone(), value.to_string());
|
||||
}
|
||||
|
||||
// Construire la réponse SOAP
|
||||
let response_xml = build_soap_response(
|
||||
&instance.service_type(),
|
||||
&soap_action.name,
|
||||
soap_values
|
||||
).unwrap_or_else(|_| {
|
||||
build_soap_fault(
|
||||
"s:Server",
|
||||
"Action Failed",
|
||||
Some(error_codes::ACTION_FAILED),
|
||||
Some("Failed to build SOAP response")
|
||||
).unwrap_or_else(|_| String::from("<?xml version=\"1.0\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope>"))
|
||||
});
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
response_xml,
|
||||
).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
error!("❌ Action execution failed: {:?}", e);
|
||||
let fault_xml = build_soap_fault(
|
||||
"s:Server",
|
||||
"Action Failed",
|
||||
Some(error_codes::ACTION_FAILED),
|
||||
Some(&format!("Action execution failed: {:?}", e))
|
||||
).unwrap_or_else(|_| String::from("<?xml version=\"1.0\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope>"));
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
fault_xml,
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::variable_types::{StateValue, StateVarType, type_trait::UpnpVarType};
|
||||
use crate::variable_types::{StateValue, StateValueError, StateVarType, type_trait::UpnpVarType};
|
||||
|
||||
impl UpnpVarType for StateValue {
|
||||
fn as_state_var_type(&self) -> StateVarType {
|
||||
@@ -86,3 +86,95 @@ impl PartialOrd for StateValue {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StateValue {
|
||||
/// Parse une chaîne de caractères en StateValue selon le type spécifié.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `s` - La chaîne à parser
|
||||
/// * `var_type` - Le type de variable attendu
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(StateValue)` si le parsing réussit, `Err(StateValueError)` sinon.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// use pmoupnp::variable_types::{StateValue, StateVarType};
|
||||
///
|
||||
/// let value = StateValue::from_string("42", &StateVarType::UI4).unwrap();
|
||||
/// assert_eq!(value, StateValue::UI4(42));
|
||||
///
|
||||
/// let value = StateValue::from_string("true", &StateVarType::Boolean).unwrap();
|
||||
/// assert_eq!(value, StateValue::Boolean(true));
|
||||
/// ```
|
||||
pub fn from_string(s: &str, var_type: &StateVarType) -> Result<Self, StateValueError> {
|
||||
use chrono::NaiveDate;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
match var_type {
|
||||
StateVarType::UI1 => s.parse::<u8>()
|
||||
.map(StateValue::UI1)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse UI1: {}", e))),
|
||||
StateVarType::UI2 => s.parse::<u16>()
|
||||
.map(StateValue::UI2)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse UI2: {}", e))),
|
||||
StateVarType::UI4 => s.parse::<u32>()
|
||||
.map(StateValue::UI4)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse UI4: {}", e))),
|
||||
StateVarType::I1 => s.parse::<i8>()
|
||||
.map(StateValue::I1)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse I1: {}", e))),
|
||||
StateVarType::I2 => s.parse::<i16>()
|
||||
.map(StateValue::I2)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse I2: {}", e))),
|
||||
StateVarType::I4 | StateVarType::Int => s.parse::<i32>()
|
||||
.map(StateValue::I4)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse I4/Int: {}", e))),
|
||||
StateVarType::R4 => s.parse::<f32>()
|
||||
.map(StateValue::R4)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse R4: {}", e))),
|
||||
StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => s.parse::<f64>()
|
||||
.map(StateValue::R8)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse R8/Number: {}", e))),
|
||||
StateVarType::Char => s.chars().next()
|
||||
.ok_or_else(|| StateValueError::ParseError("Empty string for Char".to_string()))
|
||||
.map(StateValue::Char),
|
||||
StateVarType::String => Ok(StateValue::String(s.to_string())),
|
||||
StateVarType::Boolean => {
|
||||
match s.to_lowercase().as_str() {
|
||||
"true" | "1" | "yes" => Ok(StateValue::Boolean(true)),
|
||||
"false" | "0" | "no" => Ok(StateValue::Boolean(false)),
|
||||
_ => Err(StateValueError::ParseError(format!("Invalid boolean value: {}", s))),
|
||||
}
|
||||
}
|
||||
StateVarType::BinBase64 => Ok(StateValue::BinBase64(s.to_string())),
|
||||
StateVarType::BinHex => Ok(StateValue::BinHex(s.to_string())),
|
||||
StateVarType::Date => NaiveDate::parse_from_str(s, "%Y-%m-%d")
|
||||
.map(StateValue::Date)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse Date: {}", e))),
|
||||
StateVarType::DateTime => chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")
|
||||
.or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S"))
|
||||
.map(StateValue::DateTime)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse DateTime: {}", e))),
|
||||
StateVarType::DateTimeTZ => chrono::DateTime::parse_from_rfc3339(s)
|
||||
.map(|dt| StateValue::DateTimeTZ(dt.into()))
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse DateTimeTZ: {}", e))),
|
||||
StateVarType::Time => chrono::NaiveTime::parse_from_str(s, "%H:%M:%S")
|
||||
.map(StateValue::Time)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse Time: {}", e))),
|
||||
StateVarType::TimeTZ => chrono::DateTime::parse_from_rfc3339(&format!("1970-01-01T{}", s))
|
||||
.map(|dt| StateValue::TimeTZ(dt.into()))
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse TimeTZ: {}", e))),
|
||||
StateVarType::UUID => Uuid::parse_str(s)
|
||||
.map(StateValue::UUID)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse UUID: {}", e))),
|
||||
StateVarType::URI => Url::parse(s)
|
||||
.map(StateValue::URI)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse URI: {}", e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user