test ollama code

This commit is contained in:
2025-10-09 20:37:05 +02:00
parent 913da8a5a3
commit 05aa1634e9
10 changed files with 923 additions and 66 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.ollama
.ollamacode
/bin/
/pkg/
/vendor/

View 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)
})
};
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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\"")],

View File

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

View File

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

View File

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