diff --git a/PMOMusic/src/main.rs b/PMOMusic/src/main.rs
index 7af101bc..e1c7d4ce 100644
--- a/PMOMusic/src/main.rs
+++ b/PMOMusic/src/main.rs
@@ -1,6 +1,6 @@
use pmoupnp::{mediarenderer::avtransport::actions::{SETAVTRANSPORTURI}, server::{
logs::{log_dump, log_sse, LogState, SseLayer}, ServerBuilder, Webapp
-}, UpnpXml}; // ton module pmoupnp::server
+}, UpnpObject}; // ton module pmoupnp::server
use tracing_subscriber::Registry;
use tracing_subscriber::prelude::*;
use tracing::info;
@@ -47,7 +47,7 @@ async fn main() {
server.add_redirect("/", "/app").await;
- info!("{}",SETAVTRANSPORTURI.to_markdown());
+ info!("{}",SETAVTRANSPORTURI.to_markdown().await);
server.start().await;
server.wait().await;
diff --git a/deps.svg b/deps.svg
new file mode 100644
index 00000000..fb45d05d
--- /dev/null
+++ b/deps.svg
@@ -0,0 +1,3168 @@
+
+
+
+
+
diff --git a/doc/traits_object.md b/doc/traits_object.md
new file mode 100644
index 00000000..3b6ce8a4
--- /dev/null
+++ b/doc/traits_object.md
@@ -0,0 +1,46 @@
+```mermaid
+graph TB
+ Clone[Clone
std trait]:::stdTrait
+ Debug[Debug
std trait]:::stdTrait
+
+ UpnpDeepClone[UpnpDeepClone
deep_clone]:::baseTrait
+
+ UpnpObject[UpnpObject
to_xml_element
to_xml
to_markdown]:::baseTrait
+
+ UpnpModel[UpnpModel
create_instance]:::derived1
+ UpnpInstance[UpnpInstance
new]:::derived1
+ UpnpTyped[UpnpTyped
get_name
get_object_type]:::derived1
+ UpnpSet[UpnpSet
is_set]:::derived1
+
+ UpnpTypedObject[UpnpTypedObject
marker]:::derived2
+
+ UpnpTypedInstance[UpnpTypedInstance
marker]:::derived3
+ UpnpModelSet[UpnpModelSet
marker]:::derived3
+ UpnInstanceSet[UpnInstanceSet
marker]:::derived3
+
+ Clone --> UpnpObject
+ Debug --> UpnpObject
+
+ UpnpObject --> UpnpModel
+ UpnpObject --> UpnpInstance
+ UpnpObject --> UpnpTyped
+ UpnpObject --> UpnpSet
+
+ UpnpObject --> UpnpTypedObject
+ UpnpTyped --> UpnpTypedObject
+
+ UpnpTypedObject --> UpnpTypedInstance
+ UpnpInstance --> UpnpTypedInstance
+
+ UpnpSet --> UpnpModelSet
+ UpnpModel --> UpnpModelSet
+
+ UpnpSet --> UpnInstanceSet
+ UpnpInstance --> UpnInstanceSet
+
+ classDef stdTrait fill:#e1f5ff,stroke:#01579b,stroke-width:2px
+ classDef baseTrait fill:#fff3e0,stroke:#e65100,stroke-width:2px
+ classDef derived1 fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
+ classDef derived2 fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
+ classDef derived3 fill:#fce4ec,stroke:#880e4f,stroke-width:2px
+```
\ No newline at end of file
diff --git a/pmoupnp/src/actions/action_instance.rs b/pmoupnp/src/actions/action_instance.rs
index a4de2140..a3c2435f 100644
--- a/pmoupnp/src/actions/action_instance.rs
+++ b/pmoupnp/src/actions/action_instance.rs
@@ -1,14 +1,19 @@
+use std::sync::Arc;
+
use xmltree::{Element, XMLNode};
use crate::actions::Action;
use crate::actions::Argument;
use crate::actions::ArgumentSet;
use crate::actions::ActionInstance;
-use crate::UpnpXml;
-use crate::{UpnpObject, UpnpObjectType};
+use crate::UpnpInstance;
+use crate::UpnpObject;
+use crate::UpnpTyped;
+use crate::UpnpTypedInstance;
+use crate::{UpnpTypedObject, UpnpObjectType};
-impl UpnpXml for ActionInstance {
-fn to_xml_element(&self) -> Element {
+impl UpnpObject for ActionInstance {
+async fn to_xml_element(&self) -> Element {
let mut elem = Element::new("action");
//
@@ -17,21 +22,24 @@ fn to_xml_element(&self) -> Element {
elem.children.push(XMLNode::Element(name_elem));
// déplacer tous les enfants de args_elem dans un nouvel Element
- let args_container = self.arguments_set().to_xml_element();
+ let args_container = self.arguments_set().to_xml_element().await;
elem.children.push(XMLNode::Element(args_container));
elem
}
}
-impl UpnpObject for ActionInstance {
+
+impl UpnpTyped for ActionInstance {
fn as_upnp_object_type(&self) -> &UpnpObjectType {
return &self.object;
}
}
-impl ActionInstance {
+impl UpnpInstance for ActionInstance {
- pub fn new(action: &Action) -> Self {
+ type Model = Action;
+
+ fn new(action: &Action) -> Self {
Self {
object: UpnpObjectType {
name: action.get_name().clone(),
@@ -41,8 +49,21 @@ impl ActionInstance {
}
}
- pub fn arguments(&self, name: &str) -> Option<&Argument> {
- self.model.arguments.get(name)
+}
+
+
+impl UpnpTypedInstance for ActionInstance {
+
+ fn get_model(&self) -> &Self::Model {
+ &self.model
+ }
+}
+
+impl ActionInstance {
+
+
+ pub async fn arguments(&self, name: &str) -> Option> {
+ self.model.arguments.get_by_name(name).await
}
pub fn arguments_set(&self) -> &ArgumentSet {
diff --git a/pmoupnp/src/actions/action_instance_set.rs b/pmoupnp/src/actions/action_instance_set.rs
index 02583e3f..6d5455f3 100644
--- a/pmoupnp/src/actions/action_instance_set.rs
+++ b/pmoupnp/src/actions/action_instance_set.rs
@@ -1,17 +1,17 @@
use crate::{
- UpnpObject, UpnpXml,
- actions::{ActionInstance, ActionInstanceSet},
+ UpnpTypedObject, UpnpObject,
+ actions::{ActionInstanceSet},
};
-use std::collections::HashMap;
+
use xmltree::{Element,XMLNode};
-impl UpnpXml for ActionInstanceSet {
+impl UpnpObject for ActionInstanceSet {
// Méthode pour convertir en XML (à implémenter avec une librairie XML)
- fn to_xml_element(&self) -> Element {
+ async fn to_xml_element(&self) -> Element {
let mut elem = Element::new("actionList");
- for action in self.iter() {
- let action_elem = action.to_xml_element(); // retourne un complet
+ for action in self.all().await {
+ let action_elem = action.to_xml_element().await; // retourne un complet
elem.children.push(XMLNode::Element(action_elem));
}
@@ -19,31 +19,3 @@ impl UpnpXml for ActionInstanceSet {
}
}
-impl ActionInstanceSet {
- pub fn new() -> Self {
- Self {
- instances: HashMap::new(),
- }
- }
-
- pub fn insert(&mut self, instance: ActionInstance) {
- self.instances.insert(instance.get_name().clone(), instance);
- }
-
- pub fn contains(&self, name: &str) -> bool {
- self.instances.contains_key(name)
- }
-
- pub fn get(&self, name: &str) -> Option<&ActionInstance> {
- self.instances.get(name)
- }
-
- pub fn iter(&self) -> impl Iterator- {
- self.instances.values()
- }
-
- pub fn all(&self) -> Vec<&ActionInstance> {
- self.instances.values().collect()
- }
-}
-
diff --git a/pmoupnp/src/actions/action_methods.rs b/pmoupnp/src/actions/action_methods.rs
index 4b9bd0ea..064da331 100644
--- a/pmoupnp/src/actions/action_methods.rs
+++ b/pmoupnp/src/actions/action_methods.rs
@@ -1,13 +1,18 @@
+use std::sync::Arc;
+
use xmltree::{Element,XMLNode};
-use crate::UpnpXml;
+use crate::actions::ActionInstance;
+use crate::UpnpModel;
+use crate::UpnpObject;
use crate::actions::Action;
use crate::actions::Argument;
use crate::actions::ArgumentSet;
-use crate::{UpnpObject, UpnpObjectType};
+use crate::UpnpTyped;
+use crate::{UpnpTypedObject, UpnpObjectType};
-impl UpnpXml for Action {
- fn to_xml_element(&self) -> Element {
+impl UpnpObject for Action {
+ async fn to_xml_element(&self) -> Element {
let mut action_elem = Element::new("action");
//
@@ -18,13 +23,20 @@ impl UpnpXml for Action {
action_elem.children.push(XMLNode::Element(name_elem));
//
- let args_elem = self.arguments.to_xml_element();
+ let args_elem = self.arguments.to_xml_element().await;
action_elem.children.push(XMLNode::Element(args_elem));
action_elem
}
}
-impl UpnpObject for Action {
+
+impl UpnpModel for Action {
+
+ type Instance = ActionInstance;
+}
+
+
+impl UpnpTyped for Action {
fn as_upnp_object_type(&self) -> &UpnpObjectType {
return &self.object;
}
@@ -41,7 +53,7 @@ impl Action {
}
}
- pub fn add_argument(&mut self, arg: Argument) {
+ pub fn add_argument(&mut self, arg: Arc) {
self.arguments.insert(arg);
}
diff --git a/pmoupnp/src/actions/action_set_methods.rs b/pmoupnp/src/actions/action_set_methods.rs
index 3113b1dc..5acafe7b 100644
--- a/pmoupnp/src/actions/action_set_methods.rs
+++ b/pmoupnp/src/actions/action_set_methods.rs
@@ -4,15 +4,15 @@ use xmltree::{Element, XMLNode};
use crate::actions::Action;
use crate::actions::ActionSet;
use crate::actions::errors::ActionError;
+use crate::UpnpTypedObject;
use crate::UpnpObject;
-use crate::UpnpXml;
-impl UpnpXml for ActionSet {
- fn to_xml_element(&self) -> Element {
+impl UpnpObject for ActionSet {
+ async fn to_xml_element(&self) -> Element {
let mut elem = Element::new("actionList");
- for action in self.iter() {
- let action_elem = action.to_xml_element(); // retourne un complet
+ for action in self.all().await {
+ let action_elem = action.to_xml_element().await; // retourne un complet
elem.children.push(XMLNode::Element(action_elem));
}
@@ -21,41 +21,3 @@ impl UpnpXml for ActionSet {
}
-impl ActionSet {
- pub fn new() -> Self {
- Self {
- actions: HashMap::new(),
- }
- }
-
- pub fn insert(&mut self, action: Action) -> Result<(), ActionError> {
- let name = action.get_name();
- if self.actions.contains_key(name) {
- return Err(ActionError::SetError(
- format!("Action {} already exists", name)
- ));
- }
- self.actions.insert(name.clone(), action);
- Ok(())
- }
-
- pub fn insert_or_replace(&mut self, action: Action) {
- self.actions.insert(action.get_name().clone(), action);
- }
-
- pub fn contains(&self, name: &str) -> bool {
- self.actions.contains_key(name)
- }
-
- pub fn get(&self, name: &str) -> Option<&Action> {
- self.actions.get(name)
- }
-
- pub fn iter(&self) -> impl Iterator
- {
- self.actions.values()
- }
-
- pub fn all(&self) -> Vec<&Action> {
- self.actions.values().collect()
- }
-}
diff --git a/pmoupnp/src/actions/arg_inst_set_methods.rs b/pmoupnp/src/actions/arg_inst_set_methods.rs
new file mode 100644
index 00000000..e8bde8bc
--- /dev/null
+++ b/pmoupnp/src/actions/arg_inst_set_methods.rs
@@ -0,0 +1,31 @@
+use std::collections::HashMap;
+
+use tokio::sync::RwLock;
+use xmltree::{Element, XMLNode};
+
+use crate::{actions::{ArgInstanceSet, ArgumentSet}, state_variables::StateVariableSet, UpnpObject};
+
+use crate::UpnpInstance;
+
+impl UpnpObject for ArgInstanceSet {
+ async fn to_xml_element(&self) -> Element {
+ let mut elem = Element::new("serviceStateTable");
+
+ for state_var in self.all().await {
+ let state_var_elem = state_var.to_xml_element().await; // retourne un complet
+ elem.children.push(XMLNode::Element(state_var_elem));
+ }
+
+ elem
+ }
+}
+
+impl UpnpInstance for ArgInstanceSet {
+ type Model = ArgumentSet;
+
+ fn new(_: &ArgumentSet) -> Self {
+ Self { objects: RwLock::new(HashMap::new()) }
+ }
+}
+
+
diff --git a/pmoupnp/src/actions/arg_instance_methods.rs b/pmoupnp/src/actions/arg_instance_methods.rs
new file mode 100644
index 00000000..ca9c4bc6
--- /dev/null
+++ b/pmoupnp/src/actions/arg_instance_methods.rs
@@ -0,0 +1,42 @@
+use xmltree::Element;
+
+use crate::{actions::{Argument, ArgumentInstance}, UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance};
+
+
+impl UpnpObject for ArgumentInstance {
+ async fn to_xml_element(&self) -> Element {
+ self.get_model().to_xml_element().await
+ }
+}
+
+impl UpnpTyped for ArgumentInstance {
+ fn as_upnp_object_type(&self) -> &UpnpObjectType {
+ return &self.object;
+ }
+}
+
+impl UpnpTypedInstance for ArgumentInstance {
+
+ fn get_model(&self) -> &Self::Model {
+ &self.model
+ }
+}
+
+
+
+impl UpnpInstance for ArgumentInstance {
+ type Model = Argument;
+
+ fn new(from: &Argument) -> Self {
+ Self {
+ object: UpnpObjectType {
+ name: from.get_name().clone(),
+ object_type: "UpnpInstance".to_string(),
+ },
+
+ model: from.clone(),
+ variable_instance: None,
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/pmoupnp/src/actions/arg_set_methods.rs b/pmoupnp/src/actions/arg_set_methods.rs
index 0212677f..91c1ed32 100644
--- a/pmoupnp/src/actions/arg_set_methods.rs
+++ b/pmoupnp/src/actions/arg_set_methods.rs
@@ -1,17 +1,19 @@
-use crate::UpnpObject;
+use crate::actions::ArgInstanceSet;
+use crate::{UpnpModel, UpnpTypedObject};
use crate::{
- UpnpXml,
- actions::{Argument, ArgumentSet},
+ UpnpObject,
+ actions::{ArgumentSet},
};
use std::collections::HashMap;
+use std::sync::RwLock;
use xmltree::Element;
-impl UpnpXml for ArgumentSet {
+impl UpnpObject for ArgumentSet {
// Méthode pour convertir en XML (à implémenter avec une librairie XML)
fn to_xml_element(&self) -> Element {
let mut elem = Element::new("argumentList");
- for arg in self.iter() {
+ for arg in self.all() {
let arg_elem = arg.to_xml_element(); // toujours un contenant 1 ou 2
// Pour InOut, on ajoute tous les enfants du généré
@@ -23,30 +25,7 @@ impl UpnpXml for ArgumentSet {
elem
}
}
-impl ArgumentSet {
- pub fn new() -> Self {
- Self {
- arguments: HashMap::new(),
- }
- }
- pub fn insert(&mut self, arg: Argument) {
- self.arguments.insert(arg.get_name().clone(), arg);
- }
-
- pub fn contains(&self, name: &str) -> bool {
- self.arguments.contains_key(name)
- }
-
- pub fn get(&self, name: &str) -> Option<&Argument> {
- self.arguments.get(name)
- }
-
- pub fn iter(&self) -> impl Iterator
- {
- self.arguments.values()
- }
-
- pub fn all(&self) -> Vec<&Argument> {
- self.arguments.values().collect()
- }
+impl UpnpModel for ArgumentSet {
+ type Instance = ArgInstanceSet;
}
diff --git a/pmoupnp/src/actions/argument.rs b/pmoupnp/src/actions/argument_methods.rs
similarity index 79%
rename from pmoupnp/src/actions/argument.rs
rename to pmoupnp/src/actions/argument_methods.rs
index de75c244..e43bf7d0 100644
--- a/pmoupnp/src/actions/argument.rs
+++ b/pmoupnp/src/actions/argument_methods.rs
@@ -1,11 +1,19 @@
+use std::sync::Arc;
+
use xmltree::{Element, XMLNode};
use crate::{
- UpnpObject, UpnpObjectType, UpnpXml, actions::Argument, state_variables::StateVariable,
+ actions::{Argument, ArgumentInstance}, state_variables::StateVariable, UpnpInstance, UpnpModel, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedObject
};
-impl UpnpXml for Argument {
- fn to_xml_element(&self) -> Element {
+impl UpnpTyped for Argument {
+ fn as_upnp_object_type(&self) -> &UpnpObjectType {
+ &self.object
+ }
+}
+
+impl UpnpObject for Argument {
+ async fn to_xml_element(&self) -> Element {
let mut parent = Element::new("argumentList");
if self.is_in() && self.is_out() {
@@ -34,14 +42,14 @@ impl UpnpXml for Argument {
}
}
-impl UpnpObject for Argument {
- fn as_upnp_object_type(&self) -> &UpnpObjectType {
- return &self.object;
- }
+impl UpnpModel for Argument {
+ type Instance = ArgumentInstance;
}
+
+
impl Argument {
- fn new(name: String, state_variable: StateVariable) -> Self {
+ fn new(name: String, state_variable: Arc) -> Self {
Self {
object: UpnpObjectType {
name,
@@ -53,19 +61,19 @@ impl Argument {
}
}
- pub fn new_in(name: String, state_variable: StateVariable) -> Self {
+ pub fn new_in(name: String, state_variable: Arc) -> Self {
let mut arg = Self::new(name, state_variable);
arg.is_in = true;
arg
}
- pub fn new_out(name: String, state_variable: StateVariable) -> Self {
+ pub fn new_out(name: String, state_variable: Arc) -> Self {
let mut arg = Self::new(name, state_variable);
arg.is_out = true;
arg
}
- pub fn new_in_out(name: String, state_variable: StateVariable) -> Self {
+ pub fn new_in_out(name: String, state_variable: Arc) -> Self {
let mut arg = Self::new(name, state_variable);
arg.is_in = true;
arg.is_out = true;
diff --git a/pmoupnp/src/actions/errors.rs b/pmoupnp/src/actions/errors.rs
index c4ab8443..409d5045 100644
--- a/pmoupnp/src/actions/errors.rs
+++ b/pmoupnp/src/actions/errors.rs
@@ -16,4 +16,22 @@ impl From for ActionError {
fn from(err: std::io::Error) -> Self {
ActionError::GeneralError(format!("IO error: {}", err))
}
+}
+
+#[derive(Error, Debug)]
+pub enum ArgumentError {
+ #[error("Argument error: {0}")]
+ GeneralError(String),
+
+ #[error("Argument error: {0}")]
+ ArgumentError(String),
+
+ #[error("Set operation error: {0}")]
+ SetError(String),
+}
+
+impl From for ArgumentError {
+ fn from(err: std::io::Error) -> Self {
+ ArgumentError::GeneralError(format!("IO error: {}", err))
+ }
}
\ No newline at end of file
diff --git a/pmoupnp/src/actions/macros.rs b/pmoupnp/src/actions/macros.rs
index 9d8e2e6b..987e1885 100644
--- a/pmoupnp/src/actions/macros.rs
+++ b/pmoupnp/src/actions/macros.rs
@@ -1,23 +1,99 @@
-/// Macro pour définir facilement des actions UPnP
+/// Macro pour définir facilement une action UPnP.
+///
+/// Cette macro simplifie la création d'actions UPnP statiques en générant
+/// automatiquement le code nécessaire pour initialiser une action avec ses arguments.
///
/// # Syntaxe
///
+/// ## Action avec arguments
+///
/// ```ignore
/// define_action! {
/// pub static ACTION_NAME = "ActionName" {
/// in "ParamName" => VARIABLE_REF,
-/// in "OtherParam" => OTHER_VAR,
/// out "ResultParam" => RESULT_VAR,
/// }
/// }
/// ```
+///
+/// ## Action sans arguments
+///
+/// ```ignore
+/// define_action! {
+/// pub static ACTION_NAME = "ActionName"
+/// }
+/// ```
+///
+/// # Arguments
+///
+/// - `ACTION_NAME` : Nom de la constante statique Rust
+/// - `"ActionName"` : Nom de l'action UPnP (chaîne littérale)
+/// - `in` ou `out` : Direction de l'argument (entrée ou sortie)
+/// - `"ParamName"` : Nom du paramètre UPnP (chaîne littérale)
+/// - `VARIABLE_REF` : Référence vers une `Lazy>`
+///
+/// # Type de retour
+///
+/// La macro génère une `Lazy>` qui sera initialisée lors du premier accès.
+///
+/// # Prérequis
+///
+/// Les variables d'état référencées doivent être définies comme :
+///
+/// ```ignore
+/// pub static MY_VAR: Lazy> = Lazy::new(|| {
+/// Arc::new(StateVariable::new(StateVarType::UI4, "MyVar".to_string()))
+/// });
+/// ```
+///
+/// # Examples
+///
+/// ```ignore
+/// use once_cell::sync::Lazy;
+/// use std::sync::Arc;
+///
+/// // Définir les variables d'état
+/// pub static INSTANCE_ID: Lazy> = Lazy::new(|| {
+/// Arc::new(StateVariable::new(StateVarType::UI4, "InstanceID".to_string()))
+/// });
+///
+/// pub static TRANSPORT_URI: Lazy> = Lazy::new(|| {
+/// Arc::new(StateVariable::new(StateVarType::String, "TransportURI".to_string()))
+/// });
+///
+/// // Définir une action avec arguments
+/// define_action! {
+/// pub static PLAY = "Play" {
+/// in "InstanceID" => INSTANCE_ID,
+/// in "Speed" => TRANSPORT_SPEED,
+/// }
+/// }
+///
+/// // Action sans arguments
+/// define_action! {
+/// pub static PAUSE = "Pause"
+/// }
+///
+/// // Utilisation
+/// fn main() {
+/// let play_action = &*PLAY; // Déréférence la Lazy>
+/// println!("Action: {}", play_action.get_name());
+/// }
+/// ```
+///
+/// # Notes d'implémentation
+///
+/// - Les `Arc` sont clonés (shallow copy du pointeur)
+/// - Chaque `Argument` est wrappé dans un `Arc`
+/// - L'`Action` finale est wrappée dans un `Arc`
+/// - 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<$crate::actions::Action> =
+ pub static $name: once_cell::sync::Lazy> =
once_cell::sync::Lazy::new(|| {
- $crate::actions::Action::new($action_name.to_string())
+ std::sync::Arc::new($crate::actions::Action::new($action_name.to_string()))
});
};
@@ -27,7 +103,7 @@ macro_rules! define_action {
$direction:ident $arg_name:literal => $var_ref:expr
),* $(,)?
}) => {
- pub static $name: once_cell::sync::Lazy<$crate::actions::Action> =
+ pub static $name: once_cell::sync::Lazy> =
once_cell::sync::Lazy::new(|| {
let mut ac = $crate::actions::Action::new($action_name.to_string());
@@ -37,105 +113,125 @@ macro_rules! define_action {
);
)*
- ac
+ std::sync::Arc::new(ac)
});
};
- // Helpers internes pour créer les arguments
+ // Helper interne pour créer un argument d'entrée
(@arg in $name:literal, $var:expr) => {
- $crate::actions::Argument::new_in($name.to_string(), $var.clone())
+ std::sync::Arc::new(
+ $crate::actions::Argument::new_in(
+ $name.to_string(),
+ std::sync::Arc::clone(&$var)
+ )
+ )
};
+ // Helper interne pour créer un argument de sortie
(@arg out $name:literal, $var:expr) => {
- $crate::actions::Argument::new_out($name.to_string(), $var.clone())
+ std::sync::Arc::new(
+ $crate::actions::Argument::new_out(
+ $name.to_string(),
+ std::sync::Arc::clone(&$var)
+ )
+ )
};
}
-// ============= Exemples d'utilisation =============
-
-#[cfg(test)]
-mod examples {
- use super::*;
-
- // Utilisation originale (pour comparaison)
- mod original {
- use crate::mediarenderer::avtransport::variables::{
- A_ARG_TYPE_INSTANCE_ID,
- TRANSPORTPLAYSPEED
- };
- use crate::actions::{Action, Argument};
- use once_cell::sync::Lazy;
-
- pub static PLAY: Lazy = Lazy::new(|| -> Action {
- let mut ac = Action::new("Play".to_string());
-
- ac.add_argument(
- Argument::new_in("InstanceID".to_string(),
- A_ARG_TYPE_INSTANCE_ID.clone())
- );
-
- ac.add_argument(
- Argument::new_in("Speed".to_string(),
- TRANSPORTPLAYSPEED.clone())
- );
-
- ac
- });
- }
-
- // Avec la macro - Version simple
- mod with_macro {
- use crate::mediarenderer::avtransport::variables::*;
-
- define_action! {
- pub static PLAY = "Play" {
- in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
- in "Speed" => TRANSPORTPLAYSPEED,
- }
- }
-
- define_action! {
- pub static SETAVTRANSPORTURI = "SetAVTransportURI" {
- in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
- in "CurrentURI" => AVTRANSPORTURI,
- in "CurrentURIMetaData" => AVTRANSPORTURIMETADATA,
- }
- }
-
- define_action! {
- pub static STOP = "Stop" {
- in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
- }
- }
-
- // Action sans arguments
- define_action! {
- pub static PAUSE = "Pause"
- }
-
- }
-}
-
-// ============= Macro alternative encore plus concise =============
-
-/// Macro pour définir plusieurs actions en une fois
+/// Macro pour définir plusieurs actions UPnP en une seule déclaration.
+///
+/// Cette macro permet de regrouper la définition de plusieurs actions pour
+/// améliorer la lisibilité et réduire la répétition de code.
///
/// # Syntaxe
///
/// ```ignore
/// define_actions! {
+/// ACTION1 = "Action1" {
+/// in "Param1" => VAR1,
+/// out "Result1" => VAR2,
+/// }
+///
+/// ACTION2 = "Action2" {
+/// in "Param1" => VAR1,
+/// }
+///
+/// ACTION3 = "Action3"
+/// }
+/// ```
+///
+/// # Arguments
+///
+/// Chaque action suit la même syntaxe que [`define_action!`], mais sans
+/// le mot-clé `pub static`.
+///
+/// # Type de retour
+///
+/// Génère une `Lazy>` pour chaque action définie.
+///
+/// # Examples
+///
+/// ```ignore
+/// use once_cell::sync::Lazy;
+/// use std::sync::Arc;
+///
+/// // Variables d'état
+/// pub static INSTANCE_ID: Lazy> = Lazy::new(|| {
+/// Arc::new(StateVariable::new(StateVarType::UI4, "InstanceID".to_string()))
+/// });
+///
+/// pub static TRANSPORT_URI: Lazy> = Lazy::new(|| {
+/// Arc::new(StateVariable::new(StateVarType::String, "TransportURI".to_string()))
+/// });
+///
+/// pub static URI_METADATA: Lazy> = Lazy::new(|| {
+/// Arc::new(StateVariable::new(StateVarType::String, "URIMetaData".to_string()))
+/// });
+///
+/// // Définir plusieurs actions ensemble
+/// define_actions! {
/// PLAY = "Play" {
-/// in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
-/// in "Speed" => TRANSPORTPLAYSPEED,
+/// in "InstanceID" => INSTANCE_ID,
/// }
///
/// STOP = "Stop" {
-/// in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
+/// in "InstanceID" => INSTANCE_ID,
+/// }
+///
+/// PAUSE = "Pause" {
+/// in "InstanceID" => INSTANCE_ID,
+/// }
+///
+/// SET_AV_TRANSPORT_URI = "SetAVTransportURI" {
+/// in "InstanceID" => INSTANCE_ID,
+/// in "CurrentURI" => TRANSPORT_URI,
+/// in "CurrentURIMetaData" => URI_METADATA,
+/// }
+/// }
+///
+/// // Utilisation
+/// fn setup_transport_service() {
+/// let actions = vec![&*PLAY, &*STOP, &*PAUSE, &*SET_AV_TRANSPORT_URI];
+/// for action in actions {
+/// println!("Action: {}", action.get_name());
/// }
/// }
/// ```
+///
+/// # Avantages
+///
+/// - Regroupement logique des actions d'un service
+/// - Réduction de la répétition de `pub static` et `define_action!`
+/// - Meilleure lisibilité pour les services avec nombreuses actions
+///
+/// # Notes
+///
+/// - Toutes les actions définies sont publiques (`pub`)
+/// - Chaque action est indépendante et peut être utilisée séparément
+/// - La macro se développe en plusieurs appels à [`define_action!`]
#[macro_export]
macro_rules! define_actions {
+ // Variante avec arguments pour chaque action
(
$(
$name:ident = $action_name:literal {
@@ -154,44 +250,31 @@ macro_rules! define_actions {
)*
};
- // Support pour actions sans arguments
+ // Variante mixte : actions avec et sans arguments
(
$(
- $name:ident = $action_name:literal
+ $name:ident = $action_name:literal $({
+ $(
+ $direction:ident $arg_name:literal => $var_ref:expr
+ ),* $(,)?
+ })?
)*
) => {
$(
- define_action! {
- pub static $name = $action_name
- }
+ $(
+ define_action! {
+ pub static $name = $action_name {
+ $($direction $arg_name => $var_ref),*
+ }
+ }
+ )?
+ $(
+ // Cas sans accolades (action sans arguments)
+ #[allow(unused)]
+ define_action! {
+ pub static $name = $action_name
+ }
+ )?
)*
};
-}
-
-// ============= Exemple d'utilisation groupée =============
-
-#[cfg(test)]
-mod grouped_example {
- use crate::mediarenderer::avtransport::variables::*;
-
- define_actions! {
- PLAY = "Play" {
- in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
- in "Speed" => TRANSPORTPLAYSPEED,
- }
-
- STOP = "Stop" {
- in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
- }
-
- PAUSE = "Pause" {
- in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
- }
-
- SETAVTRANSPORTURI = "SetAVTransportURI" {
- in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
- in "CurrentURI" => AVTRANSPORTURI,
- in "CurrentURIMetaData" => AVTRANSPORTURIMETADATA,
- }
- }
}
\ No newline at end of file
diff --git a/pmoupnp/src/actions/mod.rs b/pmoupnp/src/actions/mod.rs
index 8a230945..8d87391c 100644
--- a/pmoupnp/src/actions/mod.rs
+++ b/pmoupnp/src/actions/mod.rs
@@ -4,15 +4,18 @@ mod action_methods;
mod action_instance;
mod action_set_methods;
mod action_instance_set;
-mod argument;
+mod argument_methods;
mod arg_set_methods;
+mod arg_inst_set_methods;
+mod arg_instance_methods;
mod macros;
-use std::collections::HashMap;
-use crate::{state_variables::StateVariable, UpnpObjectType};
+use std::sync::Arc;
+use crate::{state_variables::{StateVarInstance, StateVariable}, UpnpObjectSet, UpnpObjectType};
+pub use errors::ActionError;
#[derive(Debug, Clone)]
pub struct Action {
@@ -20,10 +23,7 @@ pub struct Action {
arguments: ArgumentSet,
}
-#[derive(Debug, Default, Clone)]
-pub struct ActionSet {
- actions: HashMap,
-}
+pub type ActionSet = UpnpObjectSet;
#[derive(Debug, Clone)]
pub struct ActionInstance {
@@ -31,20 +31,24 @@ pub struct ActionInstance {
model: Action,
}
-#[derive(Debug, Default, Clone)]
-pub struct ActionInstanceSet {
- instances: HashMap,
-}
+pub type ActionInstanceSet = UpnpObjectSet;
#[derive(Debug, Clone)]
pub struct Argument {
object: UpnpObjectType,
- state_variable: StateVariable,
+ state_variable: Arc,
is_in: bool,
is_out: bool,
}
-#[derive(Debug, Default, Clone)]
-pub struct ArgumentSet {
- arguments: HashMap,
+pub type ArgumentSet = UpnpObjectSet;
+
+
+#[derive(Debug, Clone)]
+pub struct ArgumentInstance {
+ object: UpnpObjectType,
+ model: Argument,
+ variable_instance: Option>,
}
+
+pub type ArgInstanceSet = UpnpObjectSet;
diff --git a/pmoupnp/src/lib.rs b/pmoupnp/src/lib.rs
index 38d1db34..7d6dfa9d 100644
--- a/pmoupnp/src/lib.rs
+++ b/pmoupnp/src/lib.rs
@@ -1,17 +1,33 @@
mod object_trait;
+mod object_set;
+pub mod actions;
+pub mod mediarenderer;
pub mod server;
+// pub mod services;
pub mod state_variables;
pub mod value_ranges;
pub mod variable_types;
-pub mod actions;
-pub mod mediarenderer;
-pub use crate::object_trait::{UpnpXml,UpnpObject};
+use std::{collections::HashMap, sync::Arc};
-#[derive(Debug,Clone)]
+use tokio::sync::RwLock;
+
+pub use crate::object_trait::*;
+
+#[derive(Debug, Clone)]
pub struct UpnpObjectType {
name: String,
object_type: String,
}
+
+#[derive(Debug)]
+pub struct UpnpObjectSet {
+ objects: RwLock>>,
+}
+
+pub enum UpnpObjectSetError {
+ AlreadyExists(String),
+}
+
diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/play.rs b/pmoupnp/src/mediarenderer/avtransport/actions/play.rs
index 7f0b0e4d..5ddc3a1b 100644
--- a/pmoupnp/src/mediarenderer/avtransport/actions/play.rs
+++ b/pmoupnp/src/mediarenderer/avtransport/actions/play.rs
@@ -1,19 +1,9 @@
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTPLAYSPEED};
-use crate::actions::{Action, Argument};
-use once_cell::sync::Lazy;
-
-pub static PLAY: Lazy = Lazy::new(|| -> Action {
- let mut ac = Action::new("Play".to_string());
-
- ac.add_argument(
- Argument::new_in("InstanceID".to_string(),
- A_ARG_TYPE_INSTANCE_ID.clone())
- );
-
- ac.add_argument(
- Argument::new_in("Speed".to_string(),
- TRANSPORTPLAYSPEED.clone())
- );
-
- ac
-});
\ No newline at end of file
+use crate::define_action;
+
+define_action! {
+ pub static PLAY = "Play" {
+ in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
+ in "Speed" => TRANSPORTPLAYSPEED,
+ }
+}
diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs
index 3c9990cb..1bdc2b51 100644
--- a/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs
+++ b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs
@@ -1,7 +1,9 @@
+use std::sync::Arc;
+
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
-pub static A_ARG_TYPE_INSTANCE_ID: Lazy = Lazy::new(|| -> StateVariable {
- StateVariable::new(StateVarType::UI4, "A_ARG_TYPE_InstanceID".to_string())
+pub static A_ARG_TYPE_INSTANCE_ID: Lazy> = Lazy::new(|| -> Arc {
+ Arc::new(StateVariable::new(StateVarType::UI4, "A_ARG_TYPE_InstanceID".to_string()))
});
diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs
index 4e370d65..9e263238 100644
--- a/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs
+++ b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs
@@ -1,7 +1,9 @@
+use std::sync::Arc;
+
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
-pub static A_ARG_TYPE_PLAY_SPEED: Lazy = Lazy::new(|| -> StateVariable {
- StateVariable::new(StateVarType::String, "A_ARG_TYPE_PlaySpeed".to_string())
+pub static A_ARG_TYPE_PLAY_SPEED: Lazy> = Lazy::new(|| -> Arc {
+ Arc::new(StateVariable::new(StateVarType::String, "A_ARG_TYPE_PlaySpeed".to_string()))
});
diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs
index de6626b9..ba58f7c0 100644
--- a/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs
+++ b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs
@@ -1,7 +1,9 @@
+use std::sync::Arc;
+
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
-pub static AVTRANSPORTURI: Lazy = Lazy::new(|| -> StateVariable {
- StateVariable::new(StateVarType::String, "AVTransportURI".to_string())
+pub static AVTRANSPORTURI: Lazy> = Lazy::new(|| -> Arc {
+ Arc::new(StateVariable::new(StateVarType::String, "AVTransportURI".to_string()))
});
diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs
index cfba4823..8fb31ed1 100644
--- a/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs
+++ b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs
@@ -25,9 +25,9 @@ fn avtransporturimetadataparser(value: &str) -> Result, StateVa
Ok(Box::new(didl) as Box)
}
-pub static AVTRANSPORTURIMETADATA: Lazy = Lazy::new(|| -> StateVariable {
+pub static AVTRANSPORTURIMETADATA: Lazy> = Lazy::new(|| -> Arc {
let mut sv = StateVariable::new(StateVarType::String, "AVTransportURIMetaData".to_string());
sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser");
- sv
+ Arc::new(sv)
});
diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackduration.rs b/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackduration.rs
index 9bc15741..00305005 100644
--- a/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackduration.rs
+++ b/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackduration.rs
@@ -1,8 +1,10 @@
+use std::sync::Arc;
+
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
-pub static CURRENTTRACKDURATION: Lazy = Lazy::new(|| -> StateVariable {
- StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string())
+pub static CURRENTTRACKDURATION: Lazy> = Lazy::new(|| -> Arc {
+ Arc::new(StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string()))
});
diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs b/pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs
index aef97d40..79f225d0 100644
--- a/pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs
+++ b/pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs
@@ -1,8 +1,10 @@
+use std::sync::Arc;
+
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
-pub static SEEKMODE: Lazy = Lazy::new(|| -> StateVariable {
- StateVariable::new(StateVarType::String, "SeekMode".to_string())
+pub static SEEKMODE: Lazy> = Lazy::new(|| -> Arc {
+ Arc::new(StateVariable::new(StateVarType::String, "SeekMode".to_string()))
});
diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs b/pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs
index 2b3e6da1..d992609b 100644
--- a/pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs
+++ b/pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs
@@ -1,13 +1,15 @@
+use std::sync::Arc;
+
use crate::state_variables::StateVariable;
use crate::variable_types::{StateValue, StateVarType};
use once_cell::sync::Lazy;
-pub static TRANSPORTPLAYSPEED: Lazy = Lazy::new(|| -> StateVariable {
+pub static TRANSPORTPLAYSPEED: Lazy> = Lazy::new(|| -> Arc {
let mut sv = StateVariable::new(StateVarType::String, "TransportPlaySpeed".to_string());
sv.push_allowed_value(&StateValue::String("1".to_string())).expect("Cannot add allowed value");
sv.set_default(&StateValue::String("1".to_string())).expect("Cannt set default value");
- sv
+ Arc::new(sv)
});
diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs b/pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs
index 12724c0f..9bccaf16 100644
--- a/pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs
+++ b/pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs
@@ -1,8 +1,10 @@
+use std::sync::Arc;
+
use crate::state_variables::StateVariable;
use crate::variable_types::{StateValue, StateVarType};
use once_cell::sync::Lazy;
-pub static TRANSPORTSTATE: Lazy = Lazy::new(|| -> StateVariable {
+pub static TRANSPORTSTATE: Lazy> = Lazy::new(|| -> Arc {
let mut sv = StateVariable::new(StateVarType::String, "TransportState".to_string());
sv.push_allowed_value(&StateValue::String("NO_MEDIA_PRESENT".to_string())).expect("Cannot add allowed value");
@@ -16,6 +18,6 @@ pub static TRANSPORTSTATE: Lazy = Lazy::new(|| -> StateVariable {
StateValue::String("NO_MEDIA_PRESENT".to_string()),
]).expect("Cannt set default value");
- sv
+ Arc::new(sv)
});
diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs b/pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs
index 98d79338..22c70115 100644
--- a/pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs
+++ b/pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs
@@ -1,8 +1,10 @@
+use std::sync::Arc;
+
use crate::state_variables::StateVariable;
use crate::variable_types::{StateValue, StateVarType};
use once_cell::sync::Lazy;
-pub static TRANSPORTSTATUS: Lazy = Lazy::new(|| -> StateVariable {
+pub static TRANSPORTSTATUS: Lazy> = Lazy::new(|| -> Arc {
let mut sv = StateVariable::new(StateVarType::String, "TransportStatus".to_string());
sv.push_allowed_value(&StateValue::String("OK".to_string()))
@@ -13,5 +15,5 @@ pub static TRANSPORTSTATUS: Lazy = Lazy::new(|| -> StateVariable
])
.expect("Cannt set default value");
- sv
+ Arc::new(sv)
});
diff --git a/pmoupnp/src/object_set.rs b/pmoupnp/src/object_set.rs
new file mode 100644
index 00000000..dd8ad820
--- /dev/null
+++ b/pmoupnp/src/object_set.rs
@@ -0,0 +1,218 @@
+use std::{collections::HashMap, sync::Arc};
+
+use tokio::sync::RwLock;
+
+use crate::{UpnpDeepClone, UpnpObjectSet, UpnpObjectSetError, UpnpTypedObject};
+
+/// Implémentation du clonage profond pour `UpnpObjectSet`.
+///
+/// Cette implémentation crée une copie complète et indépendante du set,
+/// en clonant chaque objet `T` et en créant de nouveaux `Arc` autour de ces clones.
+/// Les modifications sur l'un des sets n'affectent pas l'autre.
+impl UpnpDeepClone for UpnpObjectSet {
+ fn deep_clone(&self) -> Self {
+ let guard = self.objects.blocking_read();
+
+ let cloned_map: HashMap> = guard
+ .iter()
+ .map(|(key, arc)| (key.clone(), Arc::new((**arc).clone())))
+ .collect();
+
+ Self {
+ objects: RwLock::new(cloned_map),
+ }
+ }
+}
+
+/// Implémentation du clonage superficiel pour `UpnpObjectSet`.
+///
+/// Cette implémentation crée une copie du set qui **partage** les objets `T`
+/// via les `Arc`. C'est beaucoup plus rapide et économe en mémoire qu'un clonage
+/// profond, car seuls les pointeurs `Arc` sont clonés (incrémentation du compteur
+/// de références).
+///
+/// # Note
+///
+/// Les deux sets partagent les mêmes instances d'objets `T`. Si `T` contient
+/// de la mutabilité interne (via `Mutex`, `RwLock`, etc.), les modifications
+/// seront visibles depuis les deux sets.
+impl Clone for UpnpObjectSet {
+ fn clone(&self) -> Self {
+ let guard = self.objects.blocking_read();
+
+ Self {
+ objects: RwLock::new(guard.clone()),
+ }
+ }
+}
+
+impl UpnpObjectSet {
+
+ pub fn new() -> Self {
+ Self {
+ objects: RwLock::new(HashMap::new()),
+ }
+ }
+
+
+ /// Insère un objet dans le set.
+ ///
+ /// # Arguments
+ ///
+ /// * `object` - L'objet à insérer, encapsulé dans un `Arc`
+ ///
+ /// # Returns
+ ///
+ /// * `Ok(())` - Si l'insertion a réussi
+ /// * `Err(UpnpObjectSetError::AlreadyExists)` - Si un objet avec le même nom existe déjà
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let mut set = UpnpObjectSet::new();
+ /// let obj = Arc::new(MyObject::new("test"));
+ /// set.insert(obj).await?;
+ /// ```
+ pub async fn insert(&mut self, object: Arc) -> Result<(), UpnpObjectSetError> {
+ let mut guard = self.objects.write().await;
+ let key = object.get_name().to_string();
+
+ if guard.contains_key(&key) {
+ return Err(UpnpObjectSetError::AlreadyExists(key));
+ }
+
+ guard.insert(key, object);
+ Ok(())
+ }
+
+ /// Insère un objet dans le set, ou remplace l'objet existant s'il y en a un avec le même nom.
+ ///
+ /// Cette méthode ne retourne jamais d'erreur et écrase silencieusement tout objet existant.
+ ///
+ /// # Arguments
+ ///
+ /// * `object` - L'objet à insérer ou remplacer, encapsulé dans un `Arc`
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let mut set = UpnpObjectSet::new();
+ /// let obj1 = Arc::new(MyObject::new("test"));
+ /// let obj2 = Arc::new(MyObject::new("test")); // Même nom
+ ///
+ /// set.insert_or_replace(obj1).await;
+ /// set.insert_or_replace(obj2).await; // Remplace obj1
+ /// ```
+ pub async fn insert_or_replace(&mut self, object: Arc) {
+ let mut guard = self.objects.write().await;
+ let key: String = object.get_name().to_string();
+
+ guard.insert(key, object);
+ }
+
+ /// Vérifie si le set contient un objet donné.
+ ///
+ /// La vérification se base sur le nom de l'objet retourné par `get_name()`.
+ ///
+ /// # Arguments
+ ///
+ /// * `object` - L'objet à rechercher
+ ///
+ /// # Returns
+ ///
+ /// `true` si un objet avec le même nom existe dans le set, `false` sinon.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let set = UpnpObjectSet::new();
+ /// let obj = Arc::new(MyObject::new("test"));
+ ///
+ /// if set.contains(obj.clone()).await {
+ /// println!("L'objet existe déjà");
+ /// }
+ /// ```
+ pub async fn contains(&self, object: Arc) -> bool {
+ let guard = self.objects.read().await;
+ let key: String = object.get_name().to_string();
+
+ guard.contains_key(&key)
+ }
+
+ /// Récupère un objet par son nom (version asynchrone).
+ ///
+ /// # Arguments
+ ///
+ /// * `name` - Le nom de l'objet à rechercher
+ ///
+ /// # Returns
+ ///
+ /// * `Some(Arc)` - Si un objet avec ce nom existe
+ /// * `None` - Si aucun objet n'est trouvé
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let set = UpnpObjectSet::new();
+ ///
+ /// if let Some(obj) = set.get_by_name("test").await {
+ /// println!("Objet trouvé: {}", obj.get_name());
+ /// }
+ /// ```
+ pub async fn get_by_name(&self, name: &str) -> Option> {
+ let guard = self.objects.read().await;
+ guard.get(name).cloned()
+ }
+
+ /// Retourne tous les objets du set (version asynchrone).
+ ///
+ /// # Returns
+ ///
+ /// Un vecteur contenant des clones des `Arc` pointant vers tous les objets du set.
+ /// L'ordre des éléments n'est pas garanti.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let set = UpnpObjectSet::new();
+ ///
+ /// for obj in set.all().await {
+ /// println!("Objet: {}", obj.get_name());
+ /// }
+ /// ```
+ pub async fn all(&self) -> Vec> {
+ let guard = self.objects.read().await;
+ guard.values().cloned().collect()
+ }
+
+ /// Retourne tous les objets du set (version synchrone bloquante).
+ ///
+ /// Cette méthode bloque le thread appelant jusqu'à ce que le verrou de lecture
+ /// soit obtenu. Utilisez cette version uniquement dans du code synchrone.
+ /// Pour du code asynchrone, préférez [`all()`](Self::all).
+ ///
+ /// # Returns
+ ///
+ /// Un vecteur contenant des clones des `Arc` pointant vers tous les objets du set.
+ /// L'ordre des éléments n'est pas garanti.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let set = UpnpObjectSet::new();
+ ///
+ /// // Dans un contexte synchrone
+ /// for obj in set.get_all() {
+ /// println!("Objet: {}", obj.get_name());
+ /// }
+ /// ```
+ ///
+ /// # Avertissement
+ ///
+ /// N'appelez pas cette méthode depuis un contexte asynchrone car elle peut
+ /// bloquer l'executor Tokio. Utilisez [`all()`](Self::all) à la place.
+ pub fn get_all(&self) -> Vec> {
+ let guard = self.objects.blocking_read();
+ guard.values().cloned().collect()
+ }
+}
\ No newline at end of file
diff --git a/pmoupnp/src/object_trait.rs b/pmoupnp/src/object_trait.rs
index 4a8cd29a..91a44856 100644
--- a/pmoupnp/src/object_trait.rs
+++ b/pmoupnp/src/object_trait.rs
@@ -1,31 +1,151 @@
+//! ## Hiérarchie des traits
+//!
+//! ```text
+//! Clone + Debug
+//! └─> UpnpObject (trait de base)
+//! ├─> UpnpModel (modèles créant des instances)
+//! ├─> UpnpInstance (instances concrètes)
+//! ├─> UpnpTyped (objets avec nom et type)
+//! │ └─> UpnpTypedObject = UpnpObject + UpnpTyped
+//! │ └─> UpnpTypedInstance = UpnpTypedObject + UpnpInstance
+//! └─> UpnpSet (collections) + UpnpDeepClone
+//! ├─> UpnpModelSet = UpnpSet + UpnpModel
+//! └─> UpnInstanceSet = UpnpSet + UpnpInstance
+//!
+//! UpnpDeepClone (indépendant)
+//! ```
+//!
+//! ## Description des traits
+//!
+//! - **Traits de base** :
+//! - [`UpnpObject`] : Trait principal avec sérialisation XML/Markdown
+//! - [`UpnpDeepClone`] : Clonage profond (indépendant de la hiérarchie)
+//!
+//! - **Traits de spécialisation niveau 1** :
+//! - [`UpnpModel`] : Modèle pouvant créer des instances
+//! - [`UpnpInstance`] : Instance concrète créée depuis un modèle
+//! - [`UpnpTyped`] : Ajoute les informations de type et nom
+//! - [`UpnpSet`] : Marque un objet comme collection
+//!
+//! - **Traits combinés niveau 2** :
+//! - [`UpnpTypedObject`] : Objet typé (marker trait)
+//!
+//! - **Traits combinés niveau 3** :
+//! - [`UpnpTypedInstance`] : Instance typée (marker trait)
+//! - [`UpnpModelSet`] : Collection de modèles (marker trait)
+//! - [`UpnInstanceSet`] : Collection d'instances (marker trait)
+
+use std::{fmt::Debug, sync::Arc};
+
use xmltree::{Element, EmitterConfig};
use crate::UpnpObjectType;
-pub trait UpnpXml {
- fn to_xml_element(&self) -> Element;
- fn to_xml(&self) -> String {
- let elem = self.to_xml_element();
+/// Trait pour le clonage profond d'objets UPnP.
+///
+/// Contrairement au trait standard [`Clone`] qui peut effectuer un clonage superficiel
+/// (partage via `Arc`), ce trait garantit un clonage complet et indépendant de l'objet.
+///
+/// # Note
+///
+/// Ce trait est indépendant de la hiérarchie [`UpnpObject`] et peut être implémenté
+/// séparément.
+pub trait UpnpDeepClone {
+ /// Crée un clone profond de l'objet.
+ ///
+ /// Tous les éléments internes sont clonés, créant un objet complètement indépendant.
+ fn deep_clone(&self) -> Self;
+}
+
+/// Trait de base pour tous les objets UPnP.
+///
+/// Ce trait fournit les fonctionnalités communes à tous les objets UPnP :
+/// - Sérialisation XML
+/// - Conversion en Markdown
+/// - Identification du type d'objet (instance ou set)
+///
+/// # Traits requis
+///
+/// - [`Clone`] : Pour pouvoir dupliquer les objets
+/// - [`Debug`] : Pour le débogage
+///
+/// # Hiérarchie
+///
+/// Ce trait est à la base de toute la hiérarchie UPnP. Voir la documentation du module
+/// pour le graphe complet.
+pub trait UpnpObject: Clone + Debug {
+ /// Convertit l'objet en élément XML.
+ ///
+ /// # Returns
+ ///
+ /// Un [`Element`] xmltree représentant l'objet.
+ async fn to_xml_element(&self) -> Element;
+
+ /// Convertit l'objet en chaîne XML formatée.
+ ///
+ /// Génère une représentation XML complète avec en-tête et indentation.
+ ///
+ /// # Returns
+ ///
+ /// Une chaîne XML formatée avec :
+ /// - En-tête ``
+ /// - Indentation de 2 espaces
+ ///
+ /// # Examples
+ ///
+ /// ```ignore
+ /// let xml = my_object.to_xml();
+ /// println!("{}", xml);
+ /// //
+ /// //
+ /// // value
+ /// //
+ /// ```
+ async fn to_xml(&self) -> String {
+ let elem = self.to_xml_element().await;
- // Configurer l'indentation
let config = EmitterConfig::new()
.perform_indent(true)
- .indent_string(" "); // 2 espaces
+ .indent_string(" ");
- // Sérialiser dans un buffer
let mut buf = Vec::new();
- // écrire l'élément
elem.write_with_config(&mut buf, config)
.expect("Failed to write XML");
- // Préfixer avec l'en-tête XML
let mut xml_string = "\n".to_string();
xml_string.push_str(&String::from_utf8(buf).expect("Invalid UTF-8"));
xml_string
}
- fn to_markdown(&self) -> String {
- let elem = self.to_xml_element();
+
+ /// Convertit l'objet en représentation Markdown.
+ ///
+ /// Génère une vue hiérarchique de la structure XML en format Markdown,
+ /// avec détection automatique des URLs et images.
+ ///
+ /// # Fonctionnalités
+ ///
+ /// - Les URLs sont converties en liens cliquables
+ /// - Les URLs d'images sont affichées comme images
+ /// - Les attributs sont formatés comme `key=value`
+ /// - Structure hiérarchique avec indentation
+ ///
+ /// # Returns
+ ///
+ /// Une chaîne Markdown formatée.
+ ///
+ /// # Examples
+ ///
+ /// ```ignore
+ /// let md = my_object.to_markdown();
+ /// println!("{}", md);
+ /// // # UPnP XML (Markdown view)
+ /// //
+ /// // - **element**
+ /// // - **child**: `value`
+ /// ```
+ async fn to_markdown(&self) -> String {
+ let elem = self.to_xml_element().await;
let mut md = String::new();
fn is_url(s: &str) -> bool {
@@ -43,7 +163,7 @@ pub trait UpnpXml {
}
fn format_value(v: &str) -> String {
- let v = v.trim().to_string(); // <-- clone du texte nettoyé
+ let v = v.trim().to_string();
if is_url(&v) {
if is_image_url(&v) {
format!("[{}]({})
", v, v, v)
@@ -59,7 +179,6 @@ pub trait UpnpXml {
let indent = " ".repeat(depth);
md.push_str(&format!("{}- **{}**", indent, elem.name));
- // Attributs
if !elem.attributes.is_empty() {
let attrs: Vec = elem
.attributes
@@ -69,14 +188,16 @@ pub trait UpnpXml {
md.push_str(&format!(" ({})", attrs.join(", ")));
}
- // Texte (utilise get_text() maintenant)
- if let Some(text) = elem.get_text().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) {
+ if let Some(text) = elem
+ .get_text()
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+ {
md.push_str(&format!(": {}", format_value(&text)));
}
md.push('\n');
- // Enfants
for child in &elem.children {
if let xmltree::XMLNode::Element(child_elem) = child {
recurse(child_elem, md, depth + 1);
@@ -88,15 +209,558 @@ pub trait UpnpXml {
recurse(&elem, &mut md, 0);
md
}
-}
-pub trait UpnpObject: UpnpXml {
- fn as_upnp_object_type(&self) -> &UpnpObjectType;
- fn get_name(&self) -> &String {
- return &self.as_upnp_object_type().name;
+ /// Indique si l'objet est une instance.
+ ///
+ /// # Returns
+ ///
+ /// `false` par défaut. Surchargé par [`UpnpInstance`] pour retourner `true`.
+ fn is_instance(&self) -> bool {
+ false
}
+ /// Indique si l'objet est une collection (set).
+ ///
+ /// # Returns
+ ///
+ /// `false` par défaut. Surchargé par [`UpnpSet`] pour retourner `true`.
+ fn is_set(&self) -> bool {
+ false
+ }
+}
+
+/// Trait pour les modèles UPnP qui peuvent créer des instances.
+///
+/// Un modèle représente la définition ou template d'un objet UPnP, tandis qu'une
+/// instance est une occurrence concrète de cet objet.
+///
+/// # Type associé
+///
+/// - [`Instance`](Self::Instance) : Le type d'instance créée par ce modèle
+///
+/// # Méthodes
+///
+/// - [`create_instance`](Self::create_instance) : Crée une nouvelle instance
+///
+/// # Hiérarchie
+///
+/// ```text
+/// UpnpObject
+/// └─> UpnpModel
+/// ```
+///
+/// # Relation avec UpnpInstance
+///
+/// `UpnpModel` et [`UpnpInstance`] sont liés via leurs types associés :
+/// - Le modèle spécifie quel type d'instance il crée
+/// - L'instance spécifie de quel type de modèle elle provient
+///
+/// # Examples
+///
+/// ```ignore
+/// struct DeviceModel { /* ... */ }
+/// struct DeviceInstance { /* ... */ }
+///
+/// impl UpnpModel for DeviceModel {
+/// type Instance = DeviceInstance;
+/// }
+///
+/// impl UpnpInstance for DeviceInstance {
+/// type Model = DeviceModel;
+///
+/// fn new(model: &DeviceModel) -> Self {
+/// // Création de l'instance depuis le modèle
+/// }
+/// }
+///
+/// // Utilisation
+/// let model = DeviceModel::new();
+/// let instance = model.create_instance(); // Arc
+/// ```
+pub trait UpnpModel: UpnpObject {
+ /// Le type d'instance créée par ce modèle.
+ type Instance: UpnpInstance;
+
+ /// Crée une nouvelle instance à partir de ce modèle.
+ ///
+ /// # Returns
+ ///
+ /// Un `Arc` contenant la nouvelle instance créée.
+ ///
+ /// # Implémentation par défaut
+ ///
+ /// Par défaut, appelle [`UpnpInstance::new`] avec une référence vers ce modèle
+ /// et encapsule le résultat dans un `Arc`.
+ fn create_instance(&self) -> Arc {
+ Arc::new(Self::Instance::new(self))
+ }
+}
+
+/// Trait pour les instances UPnP concrètes.
+///
+/// Une instance représente une occurrence concrète d'un objet UPnP, créée à partir
+/// d'un modèle ([`UpnpModel`]).
+///
+/// # Type associé
+///
+/// - [`Model`](Self::Model) : Le type du modèle dont cette instance dérive
+///
+/// # Méthodes requises
+///
+/// - [`new`](Self::new) : Constructeur créant l'instance depuis un modèle
+///
+/// # Hiérarchie
+///
+/// ```text
+/// UpnpObject
+/// └─> UpnpInstance
+/// ```
+///
+/// # Relation avec UpnpModel
+///
+/// Voir la documentation de [`UpnpModel`] pour comprendre la relation entre
+/// modèles et instances.
+pub trait UpnpInstance: UpnpObject {
+ /// Le type du modèle dont cette instance est dérivée.
+ type Model: UpnpModel;
+
+ /// Crée une nouvelle instance à partir d'un modèle.
+ ///
+ /// # Arguments
+ ///
+ /// * `model` - Référence vers le modèle à partir duquel créer l'instance
+ ///
+ /// # Returns
+ ///
+ /// Une nouvelle instance initialisée depuis le modèle.
+ fn new(model: &Self::Model) -> Self;
+
+ /// Indique que cet objet est une instance.
+ ///
+ /// # Returns
+ ///
+ /// Toujours `true` pour les instances.
+ fn is_instance(&self) -> bool {
+ true
+ }
+}
+
+/// Trait pour les objets UPnP typés.
+///
+/// Ajoute les informations de type et de nom aux objets UPnP.
+///
+/// # Méthodes requises
+///
+/// - [`as_upnp_object_type`](Self::as_upnp_object_type) : Accès au type de l'objet
+///
+/// # Méthodes fournies
+///
+/// - [`get_name`](Self::get_name) : Récupère le nom de l'objet
+/// - [`get_object_type`](Self::get_object_type) : Récupère le type de l'objet
+///
+/// # Hiérarchie
+///
+/// ```text
+/// UpnpObject
+/// └─> UpnpTyped
+/// ```
+pub trait UpnpTyped: UpnpObject {
+ /// Retourne une référence vers le type de l'objet.
+ fn as_upnp_object_type(&self) -> &UpnpObjectType;
+
+ /// Retourne le nom de l'objet.
+ ///
+ /// # Returns
+ ///
+ /// Une référence vers le nom de l'objet.
+ fn get_name(&self) -> &String {
+ &self.as_upnp_object_type().name
+ }
+
+ /// Retourne le type de l'objet sous forme de chaîne.
+ ///
+ /// # Returns
+ ///
+ /// Une référence vers le type de l'objet (ex: "Device", "Service", etc.).
fn get_object_type(&self) -> &String {
&self.as_upnp_object_type().object_type
}
}
+
+/// Trait marqueur pour les objets UPnP typés.
+///
+/// Combine [`UpnpObject`] et [`UpnpTyped`] pour créer un objet avec toutes
+/// les fonctionnalités de base plus les informations de type.
+///
+/// # Hiérarchie
+///
+/// ```text
+/// UpnpObject + UpnpTyped
+/// └─> UpnpTypedObject
+/// ```
+///
+/// # Note
+///
+/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires.
+pub trait UpnpTypedObject: UpnpObject + UpnpTyped {}
+
+/// Trait marqueur pour les instances typées UPnP.
+///
+/// Combine [`UpnpTypedObject`] et [`UpnpInstance`] pour représenter une instance
+/// concrète d'un objet typé avec toutes les fonctionnalités :
+/// - Sérialisation XML/Markdown (de [`UpnpObject`])
+/// - Informations de type et nom (de [`UpnpTyped`])
+/// - Relation avec un modèle (de [`UpnpInstance`])
+///
+/// # Hiérarchie
+///
+/// ```text
+/// UpnpTypedObject + UpnpInstance
+/// └─> UpnpTypedInstance
+/// ```
+///
+/// # Note
+///
+/// Ce trait ajoute la méthode [`get_model`](Self::get_model) pour accéder
+/// au modèle de l'instance. Les collections d'instances ([`UpnInstanceSet`])
+/// n'ont pas cette méthode car elles contiennent plusieurs instances.
+pub trait UpnpTypedInstance: UpnpTypedObject + UpnpInstance
+where
+ Self::Model: UpnpModel
+{
+ /// Retourne une référence vers le modèle dont cette instance est dérivée.
+ ///
+ /// Permet d'accéder aux métadonnées et contraintes définies dans le modèle,
+ /// telles que les plages de valeurs autorisées, les types, les descriptions, etc.
+ ///
+ /// # Returns
+ ///
+ /// Une référence immuable vers le modèle.
+ ///
+ /// # Examples
+ ///
+ /// ```ignore
+ /// let instance = model.create_instance();
+ ///
+ /// // Accéder aux propriétés du modèle depuis l'instance
+ /// let model_ref = instance.get_model();
+ /// println!("Instance du modèle: {}", model_ref.get_name());
+ ///
+ /// // Vérifier les contraintes définies dans le modèle
+ /// if let Some(range) = model_ref.get_range() {
+ /// println!("Plage autorisée: {:?}", range);
+ /// }
+ /// ```
+ ///
+ /// # Use cases
+ ///
+ /// Cette méthode est particulièrement utile pour :
+ /// - Valider des valeurs contre les contraintes du modèle
+ /// - Accéder aux métadonnées sans dupliquer les informations
+ /// - Afficher des informations de type ou de description
+ /// - Implémenter des logiques conditionnelles basées sur le modèle
+ ///
+ /// # Différence avec les traits spécifiques
+ ///
+ /// Pour les variables d'état, le trait [`UpnpVariable`](crate::state_variables::UpnpVariable)
+ /// fournit également `get_definition()` qui est sémantiquement équivalent
+ /// mais spécifique au domaine des variables.
+ fn get_model(&self) -> &Self::Model;
+}
+
+
+/// Trait marqueur pour les collections UPnP.
+///
+/// Représente un ensemble (set) d'objets UPnP.
+///
+/// # Super-traits requis
+///
+/// - [`UpnpObject`] : Fonctionnalités de base (XML, etc.)
+/// - [`UpnpDeepClone`] : Permet le clonage profond des collections
+///
+/// # Implémentation
+///
+/// Ce trait surcharge [`UpnpObject::is_set`] pour retourner `true`.
+///
+/// # Hiérarchie
+///
+/// ```text
+/// UpnpObject + UpnpDeepClone
+/// └─> UpnpSet
+/// ```
+///
+/// # Note sur le clonage
+///
+/// Les collections UPnP contiennent généralement des `Arc` vers leurs éléments.
+/// Le trait [`Clone`] (via `UpnpObject`) effectue un clonage shallow des `Arc`,
+/// tandis que [`UpnpDeepClone`] clone profondément les éléments contenus.
+///
+/// # Examples
+///
+/// ```ignore
+/// struct ServiceSet {
+/// services: HashMap>,
+/// }
+///
+/// impl Clone for ServiceSet {
+/// fn clone(&self) -> Self {
+/// // Clone shallow : partage les Services via Arc
+/// Self {
+/// services: self.services.clone()
+/// }
+/// }
+/// }
+///
+/// impl UpnpDeepClone for ServiceSet {
+/// fn deep_clone(&self) -> Self {
+/// // Clone profond : crée de nouveaux Services
+/// let deep_services = self.services
+/// .iter()
+/// .map(|(k, v)| (k.clone(), Arc::new((**v).clone())))
+/// .collect();
+///
+/// Self {
+/// services: deep_services
+/// }
+/// }
+/// }
+/// ```
+pub trait UpnpSet: UpnpObject + UpnpDeepClone {
+ /// Indique que cet objet est une collection.
+ ///
+ /// # Returns
+ ///
+ /// Toujours `true` pour les collections.
+ fn is_set(&self) -> bool {
+ true
+ }
+}
+
+/// Trait marqueur pour les collections de modèles UPnP.
+///
+/// Combine [`UpnpSet`] et [`UpnpModel`] pour représenter une collection
+/// de modèles qui peut elle-même créer une collection d'instances.
+///
+/// # Cas d'usage
+///
+/// Ce trait est utilisé quand une collection de modèles doit pouvoir instancier
+/// une collection d'instances correspondante. Par exemple :
+/// - Un ensemble de modèles de services d'un device qui crée un ensemble d'instances de services
+/// - Une liste de modèles d'actions qui instancie une liste d'actions actives
+/// - Une collection de modèles de variables d'état qui génère une collection d'instances
+///
+/// # Hiérarchie
+///
+/// ```text
+/// UpnpSet + UpnpModel
+/// └─> UpnpModelSet
+/// ```
+///
+/// # Relation avec d'autres traits
+///
+/// - [`UpnpSet`] : Fournit les fonctionnalités de collection
+/// - [`UpnpModel`] : Fournit la capacité de créer des instances
+/// - [`UpnInstanceSet`] : Représente les collections d'instances (contrepartie)
+///
+/// # Note
+///
+/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires.
+/// Il est automatiquement implémenté pour tous les types éligibles via une
+/// blanket implementation.
+///
+/// # Examples
+///
+/// ```ignore
+/// /// Collection de modèles de services
+/// struct ServiceSetModel {
+/// services: Vec>,
+/// }
+///
+/// /// Collection d'instances de services
+/// struct ServiceSetInstance {
+/// model: Arc,
+/// service_instances: Vec>,
+/// }
+///
+/// impl UpnpObject for ServiceSetModel { /* ... */ }
+/// impl UpnpSet for ServiceSetModel {}
+///
+/// impl UpnpModel for ServiceSetModel {
+/// type Instance = ServiceSetInstance;
+///
+/// fn create_instance(&self) -> Arc {
+/// // Créer des instances pour chaque service
+/// let instances = self.services
+/// .iter()
+/// .map(|model| model.create_instance())
+/// .collect();
+///
+/// Arc::new(ServiceSetInstance {
+/// model: Arc::new(self.clone()),
+/// service_instances: instances,
+/// })
+/// }
+/// }
+///
+/// // UpnpModelSet est automatiquement implémenté !
+///
+/// // Utilisation
+/// let model_set = ServiceSetModel::new();
+/// let instance_set = model_set.create_instance(); // Crée toutes les instances
+/// ```
+pub trait UpnpModelSet: UpnpSet + UpnpModel {}
+
+
+/// Trait marqueur pour les collections d'instances UPnP.
+///
+/// Combine [`UpnpSet`] et [`UpnpInstance`] pour représenter une collection
+/// d'instances UPnP. Cela permet d'avoir des collections qui sont elles-mêmes
+/// des instances créées depuis un modèle.
+///
+/// # Hiérarchie
+///
+/// ```text
+/// UpnpSet + UpnpInstance
+/// └─> UpnInstanceSet
+/// ```
+///
+/// # Note
+///
+/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires.
+pub trait UpnInstanceSet: UpnpSet + UpnpInstance {}
+
+
+/// Implémentation automatique de [`UpnInstanceSet`] pour tous les types éligibles.
+///
+/// Cette *blanket implementation* fournit automatiquement le trait [`UpnInstanceSet`]
+/// à tout type `T` qui implémente à la fois [`UpnpSet`] et [`UpnpInstance`].
+///
+/// # Contraintes
+///
+/// - `T` doit implémenter [`UpnpSet`] (collection d'objets UPnP)
+/// - `T` doit implémenter [`UpnpInstance`] (instance créée depuis un modèle)
+///
+/// # Pourquoi cette implémentation existe
+///
+/// Certaines collections UPnP sont elles-mêmes des instances (par exemple, une
+/// collection de services pour un device spécifique). Ce trait marker permet
+/// d'identifier ces collections qui combinent les deux aspects. La blanket
+/// implementation évite d'avoir à l'implémenter manuellement pour chaque type.
+///
+/// # Utilisation
+///
+/// ```ignore
+/// struct ServiceSetInstance {
+/// model: Arc,
+/// services: Vec>,
+/// }
+///
+/// impl UpnpObject for ServiceSetInstance { /* ... */ }
+/// impl UpnpSet for ServiceSetInstance {}
+/// impl UpnpInstance for ServiceSetInstance {
+/// type Model = ServiceSetModel;
+/// fn new(model: &ServiceSetModel) -> Self { /* ... */ }
+/// }
+///
+/// // UpnInstanceSet est automatiquement implémenté !
+///
+/// fn process_instance_set(set: &T) {
+/// if set.is_set() && set.is_instance() {
+/// println!("C'est une collection ET une instance");
+/// }
+/// }
+/// ```
+impl UpnInstanceSet for T
+where
+ T: UpnpSet + UpnpInstance
+{}
+
+/// Implémentation automatique de [`UpnpTypedObject`] pour tous les types éligibles.
+///
+/// Cette *blanket implementation* fournit automatiquement le trait [`UpnpTypedObject`]
+/// à tout type `T` qui implémente à la fois [`UpnpObject`] et [`UpnpTyped`].
+///
+/// # Contraintes
+///
+/// - `T` doit implémenter [`UpnpObject`] (fonctionnalités de base UPnP)
+/// - `T` doit implémenter [`UpnpTyped`] (informations de type et nom)
+///
+/// # Utilisation
+///
+/// ```ignore
+/// struct Device {
+/// object_type: UpnpObjectType,
+/// }
+///
+/// impl UpnpObject for Device { /* ... */ }
+/// impl UpnpTyped for Device { /* ... */ }
+///
+/// // UpnpTypedObject est automatiquement implémenté !
+/// fn process(obj: &T) {
+/// println!("{}", obj.get_name());
+/// }
+/// ```
+impl UpnpTypedObject for T
+where
+ T: UpnpObject + UpnpTyped
+{}
+
+
+/// Implémentation automatique de [`UpnpModelSet`] pour tous les types éligibles.
+///
+/// Cette *blanket implementation* fournit automatiquement le trait [`UpnpModelSet`]
+/// à tout type `T` qui implémente à la fois [`UpnpSet`] et [`UpnpModel`].
+///
+/// # Contraintes
+///
+/// - `T` doit implémenter [`UpnpSet`] (collection d'objets UPnP)
+/// - `T` doit implémenter [`UpnpModel`] (peut créer des instances)
+///
+/// # Pourquoi cette implémentation existe
+///
+/// [`UpnpModelSet`] est un *marker trait* qui identifie les collections pouvant
+/// créer des collections d'instances. Plutôt que de demander aux développeurs
+/// d'écrire manuellement `impl UpnpModelSet for MyType {}`, cette blanket
+/// implementation le fait automatiquement dès que les traits requis sont implémentés.
+///
+/// # Fonctionnement
+///
+/// Lorsque vous définissez une collection de modèles :
+///
+/// ```ignore
+/// struct ActionSetModel {
+/// actions: Vec>,
+/// }
+///
+/// impl UpnpObject for ActionSetModel { /* ... */ }
+/// impl UpnpSet for ActionSetModel {}
+///
+/// impl UpnpModel for ActionSetModel {
+/// type Instance = ActionSetInstance;
+/// fn create_instance(&self) -> Arc { /* ... */ }
+/// }
+/// ```
+///
+/// Le compilateur Rust vérifie automatiquement que `ActionSetModel` satisfait
+/// toutes les contraintes (implémente `UpnpSet` ET `UpnpModel`) et applique
+/// donc `UpnpModelSet` sans code supplémentaire.
+///
+/// # Utilisation dans des signatures génériques
+///
+/// ```ignore
+/// fn process_model_set(set: &T) {
+/// println!("Processing model set that can create instances");
+/// let instance = set.create_instance();
+/// // ...
+/// }
+/// ```
+///
+/// # Différence avec UpnInstanceSet
+///
+/// - [`UpnpModelSet`] : Collection de **modèles** (peut créer des instances)
+/// - [`UpnInstanceSet`] : Collection d'**instances** (créée depuis un modèle)
+impl UpnpModelSet for T
+where
+ T: UpnpSet + UpnpModel
+{}
+
diff --git a/pmoupnp/src/services/errors.rs b/pmoupnp/src/services/errors.rs
new file mode 100644
index 00000000..9925393d
--- /dev/null
+++ b/pmoupnp/src/services/errors.rs
@@ -0,0 +1,19 @@
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ServiceError {
+ #[error("Action error: {0}")]
+ GeneralError(String),
+
+ #[error("Argument error: {0}")]
+ ArgumentError(String),
+
+ #[error("Set operation error: {0}")]
+ SetError(String),
+}
+
+impl From for ServiceError {
+ fn from(err: std::io::Error) -> Self {
+ ServiceError::GeneralError(format!("IO error: {}", err))
+ }
+}
\ No newline at end of file
diff --git a/pmoupnp/src/services/mod.rs b/pmoupnp/src/services/mod.rs
new file mode 100644
index 00000000..3567a698
--- /dev/null
+++ b/pmoupnp/src/services/mod.rs
@@ -0,0 +1,607 @@
+mod errors;
+
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+use std::time::Duration;
+use axum::{
+ extract::{Request, State},
+ response::{Html, IntoResponse, Response},
+ http::{StatusCode, HeaderMap},
+ body::Body,
+};
+use tokio::sync::RwLock;
+use tokio::time;
+use tracing::{info, warn, debug, error};
+
+use crate::actions::{Action, ActionSet, ActionInstance, ActionInstanceSet};
+use crate::state_variables::{StateVariable, StateVariableSet, StateVarInstance, StateVarInstanceSet};
+
+pub use errors::ServiceError;
+
+#[derive(Debug, Clone)]
+pub struct UpnpObjectType {
+ name: String,
+ object_type: String,
+}
+
+impl UpnpObjectType {
+ pub fn new(name: String, object_type: String) -> Self {
+ Self { name, object_type }
+ }
+
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+
+ pub fn object_type(&self) -> &str {
+ &self.object_type
+ }
+
+ pub fn set_name(&mut self, name: String) {
+ self.name = name;
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Service {
+ object: UpnpObjectType,
+ identifier: String,
+ version: u32,
+ actions: ActionSet,
+ state_table: StateVariableSet,
+}
+
+impl Service {
+ pub fn new(name: String) -> Self {
+ Self {
+ object: UpnpObjectType::new(name.clone(), "Service".to_string()),
+ identifier: name,
+ version: 1,
+ state_table: StateVariableSet::new(),
+ actions: ActionSet::new(),
+ }
+ }
+
+ pub fn name(&self) -> &str {
+ self.object.name()
+ }
+
+ pub fn type_id(&self) -> &str {
+ self.object.object_type()
+ }
+
+ pub fn identifier(&self) -> &str {
+ &self.identifier
+ }
+
+ pub fn set_identifier(&mut self, id: String) {
+ self.identifier = id;
+ }
+
+ pub fn version(&self) -> u32 {
+ self.version
+ }
+
+ pub fn set_version(&mut self, version: u32) -> Result<(), String> {
+ if version < 1 {
+ return Err("version must be greater than or equal to 1".to_string());
+ }
+ self.version = version;
+ Ok(())
+ }
+
+ pub fn add_variable(&mut self, sv: Arc) {
+ self.state_table.insert(sv);
+ }
+
+ pub fn contains_variable(&self, sv: Arc) -> bool {
+ self.state_table.contains(sv).await
+ }
+
+ pub fn variables(&self) -> impl Iterator- {
+ self.state_table.iter()
+ }
+
+ pub fn add_action(&mut self, action: Action) -> Result<(), ServiceError> {
+ self.actions.insert(action)
+ }
+
+ pub fn new_instance(&self) -> ServiceInstance {
+ // 1️⃣ D'abord créer les StateVarInstance
+ let mut statevariables = StateVarInstanceSet::new();
+ for v in self.state_table.all() {
+ statevariables.insert(v.new_instance());
+ }
+
+ // 2️⃣ Ensuite créer les ActionInstance en vérifiant les variables
+ let mut actions = ActionInstanceSet::new();
+ for a in self.actions.all() {
+ // Vérifier que toutes les variables d'état référencées existent
+ let mut missing_vars = Vec::new();
+
+ for arg in a.arguments().iter() {
+ let related_var_name = arg.state_variable().get_name();
+ if !statevariables.contains(related_var_name) {
+ missing_vars.push(related_var_name.to_string());
+ }
+ }
+
+ if !missing_vars.is_empty() {
+ error!(
+ "❌ Action '{}' references missing state variables: {:?}",
+ a.get_name(),
+ missing_vars
+ );
+ continue; // Skip cette action
+ }
+
+ if let Err(e) = actions.insert(a.new_instance()) {
+ error!("❌ Failed to insert action '{}': {:?}", a.get_name(), e);
+ }
+ }
+
+ ServiceInstance {
+ name: self.name().to_string(),
+ identifier: self.identifier.clone(),
+ version: self.version,
+ device: None,
+ statevariables,
+ actions,
+ subscribers: Arc::new(RwLock::new(HashMap::new())),
+ changed_buffer: Arc::new(Mutex::new(HashMap::new())),
+ seqid: Arc::new(Mutex::new(HashMap::new())),
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct ServiceInstance {
+ name: String,
+ identifier: String,
+ version: u32,
+ device: Option>,
+ statevariables: StateVarInstanceSet,
+ actions: ActionInstanceSet,
+ subscribers: Arc>>, // SID → Callback URL
+ changed_buffer: Arc>>, // Simplifié pour l'exemple
+ seqid: Arc>>,
+}
+
+pub const METHOD_SUBSCRIBE: &str = "SUBSCRIBE";
+pub const METHOD_UNSUBSCRIBE: &str = "UNSUBSCRIBE";
+
+impl ServiceInstance {
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+
+ pub fn type_id(&self) -> &str {
+ "ServiceInstance"
+ }
+
+ pub fn identifier(&self) -> &str {
+ &self.identifier
+ }
+
+ pub fn service_type(&self) -> String {
+ format!("urn:schemas-upnp-org:service:{}:{}", self.name, self.version)
+ }
+
+ pub fn service_id(&self) -> String {
+ format!("urn:upnp-org:serviceId:{}", self.identifier)
+ }
+
+ pub fn base_route(&self) -> String {
+ match &self.device {
+ Some(device) => format!("{}/service/{}", device.base_route(), self.name),
+ None => format!("/service/{}", self.name),
+ }
+ }
+
+ pub fn control_url(&self) -> String {
+ format!("{}/control", self.base_route())
+ }
+
+ pub fn event_sub_url(&self) -> String {
+ format!("{}/event", self.base_route())
+ }
+
+ pub fn scpd_url(&self) -> String {
+ format!("{}/desc.xml", self.base_route())
+ }
+
+ pub fn usn(&self) -> String {
+ match &self.device {
+ Some(device) => format!("uuid:{}::urn:{}", device.udn(), self.service_type()),
+ None => format!("uuid::urn:{}", self.service_type()),
+ }
+ }
+
+ pub fn statevariables(&self) -> &StateVarInstanceSet {
+ &self.statevariables
+ }
+
+ pub fn actions(&self) -> &ActionInstanceSet {
+ &self.actions
+ }
+
+ /// Enregistre les routes UPnP dans le serveur Axum
+ pub async fn register_urls(&self, server: &mut crate::server::Server) -> Result<(), String> {
+ info!(
+ "✅ Service description for {}:{} available at : {}{}",
+ self.device.as_ref().map(|d| d.name()).unwrap_or("unknown"),
+ self.name(),
+ self.device.as_ref().map(|d| d.server_base_url()).unwrap_or(""),
+ self.scpd_url(),
+ );
+
+ // Handler pour la description SCPD
+ let instance_scpd = self.clone();
+ server.add_handler(&self.scpd_url(), move || {
+ let instance = instance_scpd.clone();
+ async move { instance.scpd_handler().await }
+ }).await;
+
+ // Handler pour le contrôle
+ let instance_control = self.clone();
+ server.add_post_handler_with_state(
+ &self.control_url(),
+ control_handler,
+ instance_control,
+ ).await;
+
+ // Handler pour les événements
+ let instance_event = self.clone();
+ server.add_handler_with_state(
+ &self.event_sub_url(),
+ event_sub_handler,
+ instance_event,
+ ).await;
+
+ Ok(())
+ }
+
+ /// Génère l'élément XML SCPD
+ pub fn scpd_element(&self) -> xmltree::Element {
+ let mut elem = xmltree::Element::new("scpd");
+ elem.attributes.insert(
+ "xmlns".to_string(),
+ "urn:schemas-upnp-org:service-1-0".to_string(),
+ );
+
+ // Version spec
+ let mut spec = xmltree::Element::new("specVersion");
+ let mut major = xmltree::Element::new("major");
+ major.children.push(xmltree::XMLNode::Text("1".to_string()));
+ spec.children.push(xmltree::XMLNode::Element(major));
+
+ let mut minor = xmltree::Element::new("minor");
+ minor.children.push(xmltree::XMLNode::Text("0".to_string()));
+ spec.children.push(xmltree::XMLNode::Element(minor));
+
+ elem.children.push(xmltree::XMLNode::Element(spec));
+
+ // Actions
+ if !self.actions.is_empty() {
+ elem.children.push(xmltree::XMLNode::Element(
+ self.actions.to_xml_element()
+ ));
+ }
+
+ // State variables
+ if !self.statevariables.is_empty() {
+ elem.children.push(xmltree::XMLNode::Element(
+ self.statevariables.to_xml_element()
+ ));
+ }
+
+ elem
+ }
+
+ /// Handler pour le SCPD
+ async fn scpd_handler(&self) -> Response {
+ let elem = self.scpd_element();
+
+ let mut xml_output = Vec::new();
+ if let Err(e) = elem.write(&mut xml_output) {
+ error!("Failed to serialize SCPD XML: {}", e);
+ return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+ }
+
+ let xml = String::from_utf8_lossy(&xml_output).to_string();
+
+ (
+ StatusCode::OK,
+ [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
+ xml,
+ ).into_response()
+ }
+
+ /// Génère l'élément XML du service
+ pub fn to_xml_element(&self) -> xmltree::Element {
+ let mut elem = xmltree::Element::new("service");
+
+ let mut service_type = xmltree::Element::new("serviceType");
+ service_type.children.push(xmltree::XMLNode::Text(self.service_type()));
+ elem.children.push(xmltree::XMLNode::Element(service_type));
+
+ let mut service_id = xmltree::Element::new("serviceId");
+ service_id.children.push(xmltree::XMLNode::Text(self.service_id()));
+ elem.children.push(xmltree::XMLNode::Element(service_id));
+
+ let mut scpd_url = xmltree::Element::new("SCPDURL");
+ scpd_url.children.push(xmltree::XMLNode::Text(self.scpd_url()));
+ elem.children.push(xmltree::XMLNode::Element(scpd_url));
+
+ let mut control_url = xmltree::Element::new("controlURL");
+ control_url.children.push(xmltree::XMLNode::Text(self.control_url()));
+ elem.children.push(xmltree::XMLNode::Element(control_url));
+
+ let mut event_sub_url = xmltree::Element::new("eventSubURL");
+ event_sub_url.children.push(xmltree::XMLNode::Text(self.event_sub_url()));
+ elem.children.push(xmltree::XMLNode::Element(event_sub_url));
+
+ elem
+ }
+
+ pub async fn add_subscriber(&self, sid: String, callback: String) {
+ let mut subscribers = self.subscribers.write().await;
+ subscribers.insert(sid, callback);
+ }
+
+ pub async fn renew_subscriber(&self, sid: &str, timeout: &str) {
+ info!("♻️ Renewed SID {} for timeout {}", sid, timeout);
+ }
+
+ pub async fn remove_subscriber(&self, sid: &str) {
+ let mut subscribers = self.subscribers.write().await;
+ subscribers.remove(sid);
+ }
+
+ pub async fn send_initial_event(&self, sid: String) {
+ let callback = {
+ let subscribers = self.subscribers.read().await;
+ subscribers.get(&sid).cloned()
+ };
+
+ if let Some(callback) = callback {
+ let mut changed = HashMap::new();
+ for sv in self.statevariables.iter() {
+ if sv.is_sending_events() {
+ changed.insert(sv.get_name().to_string(), sv.value().to_string());
+ }
+ }
+
+ if changed.is_empty() {
+ return;
+ }
+
+ tokio::spawn(async move {
+ let callback = callback.trim().trim_matches(|c| c == '<' || c == '>');
+
+ let mut body = r#""#.to_string();
+ for (name, val) in changed {
+ body.push_str(&format!("<{0}>{1}{0}>", name, val));
+ }
+ body.push_str("");
+
+ let client = reqwest::Client::new();
+ match client
+ .request(reqwest::Method::from_bytes(b"NOTIFY").unwrap(), callback)
+ .header("Content-Type", r#"text/xml; charset="utf-8"#)
+ .header("NT", "upnp:event")
+ .header("NTS", "upnp:propchange")
+ .header("SID", &sid)
+ .header("SEQ", "0")
+ .body(body.clone())
+ .send()
+ .await
+ {
+ Ok(resp) => {
+ info!("✅ Initial event sent to {}, status={}", callback, resp.status());
+ }
+ Err(e) => {
+ error!("Failed to send initial event to {}: {}", callback, e);
+ }
+ }
+ });
+ }
+ }
+
+ pub fn event_to_be_sent(&self, name: String, value: String) {
+ let mut buffer = self.changed_buffer.lock().unwrap();
+ buffer.insert(name, value);
+ }
+
+ fn next_seq(&self, sid: &str) -> String {
+ let mut seqid = self.seqid.lock().unwrap();
+ let counter = seqid.entry(sid.to_string()).or_insert(0);
+ *counter += 1;
+ counter.to_string()
+ }
+
+ pub async fn notify_subscribers(&self) {
+ let subscribers_copy = {
+ let subscribers = self.subscribers.read().await;
+ if subscribers.is_empty() {
+ return;
+ }
+ subscribers.clone()
+ };
+
+ let changed = {
+ let mut buffer = self.changed_buffer.lock().unwrap();
+ if buffer.is_empty() {
+ return;
+ }
+ std::mem::take(&mut *buffer)
+ };
+
+ for (sid, callback) in subscribers_copy {
+ let changed_clone = changed.clone();
+ let seq = self.next_seq(&sid);
+
+ tokio::spawn(async move {
+ let callback = callback.trim().trim_matches(|c| c == '<' || c == '>');
+
+ let mut body = r#""#.to_string();
+ for (name, val) in changed_clone {
+ body.push_str(&format!("<{0}>{1}{0}>", name, val));
+ }
+ body.push_str("");
+
+ let client = reqwest::Client::new();
+ match client
+ .request(reqwest::Method::from_bytes(b"NOTIFY").unwrap(), callback)
+ .header("Content-Type", r#"text/xml; charset="utf-8"#)
+ .header("NT", "upnp:event")
+ .header("NTS", "upnp:propchange")
+ .header("SID", &sid)
+ .header("SEQ", seq)
+ .body(body)
+ .send()
+ .await
+ {
+ Ok(_) => {
+ info!("✅ Notified subscriber {} of changes", callback);
+ }
+ Err(e) => {
+ error!("Failed to notify subscriber {}: {}", callback, e);
+ }
+ }
+ });
+ }
+ }
+
+ pub fn start_notifier(&self, interval: Duration) -> tokio::task::JoinHandle<()> {
+ let instance = self.clone();
+
+ tokio::spawn(async move {
+ let mut ticker = time::interval(interval);
+ info!("✅ Starting notifier every {:?}", interval);
+
+ loop {
+ ticker.tick().await;
+ instance.notify_subscribers().await;
+ }
+ })
+ }
+}
+
+// Handler Axum pour les événements (SUBSCRIBE/UNSUBSCRIBE)
+async fn event_sub_handler(
+ State(instance): State,
+ headers: HeaderMap,
+ req: Request,
+) -> Response {
+ info!("📡 Event Subscription request for {}", instance.name());
+
+ let method = req.method().as_str();
+ let sid = headers.get("SID").and_then(|v| v.to_str().ok()).unwrap_or("");
+ let timeout = headers.get("Timeout").and_then(|v| v.to_str().ok()).unwrap_or("");
+ let callback = headers.get("Callback").and_then(|v| v.to_str().ok()).unwrap_or("");
+
+ match method {
+ METHOD_SUBSCRIBE => {
+ let (response_sid, response_timeout) = if sid.is_empty() {
+ // Nouvelle subscription
+ let new_sid = format!("uuid:{}", uuid::Uuid::new_v4());
+ if !callback.is_empty() {
+ instance.add_subscriber(new_sid.clone(), callback.to_string()).await;
+ }
+ let timeout_val = if timeout.is_empty() {
+ "Second-1800"
+ } else {
+ timeout
+ };
+ info!("🔔 New subscription: SID={}, Callback={}, Timeout={}", new_sid, callback, timeout_val);
+
+ let sid_clone = new_sid.clone();
+ let instance_clone = instance.clone();
+ tokio::spawn(async move {
+ instance_clone.send_initial_event(sid_clone).await;
+ });
+
+ (new_sid, timeout_val.to_string())
+ } else {
+ // Renouvellement
+ instance.renew_subscriber(sid, timeout).await;
+ info!("♻️ Renew subscription: SID={}, Timeout={}", sid, timeout);
+ (sid.to_string(), timeout.to_string())
+ };
+
+ (
+ StatusCode::OK,
+ [
+ (axum::http::header::HeaderName::from_static("sid"), response_sid.parse().unwrap()),
+ (axum::http::header::HeaderName::from_static("timeout"), response_timeout.parse().unwrap()),
+ ],
+ ).into_response()
+ }
+ METHOD_UNSUBSCRIBE => {
+ if !sid.is_empty() {
+ instance.remove_subscriber(sid).await;
+ info!("❌ Unsubscribe SID={}", sid);
+ }
+ StatusCode::OK.into_response()
+ }
+ _ => {
+ warn!("Unsupported EventSub method: {}", method);
+ StatusCode::METHOD_NOT_ALLOWED.into_response()
+ }
+ }
+}
+
+// Handler Axum pour le contrôle SOAP
+async fn control_handler(
+ State(instance): State,
+ body: String,
+) -> Response {
+ info!("📡 Control request for {}", instance.name());
+
+ // TODO: Parser le SOAP et appeler l'action correspondante
+ // Pour l'instant, réponse minimale
+
+ let response_xml = format!(
+ r#"
+
+
+
+
+
+ "#,
+ instance.service_type()
+ );
+
+ (
+ StatusCode::OK,
+ [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
+ response_xml,
+ ).into_response()
+}
+
+// Type placeholder pour DeviceInstance
+#[derive(Debug, Clone)]
+pub struct DeviceInstance {
+ name: String,
+ udn: String,
+}
+
+impl DeviceInstance {
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+
+ pub fn base_route(&self) -> String {
+ format!("/device/{}", self.name)
+ }
+
+ pub fn udn(&self) -> &str {
+ &self.udn
+ }
+
+ pub fn server_base_url(&self) -> String {
+ "http://localhost:8080".to_string()
+ }
+}
\ No newline at end of file
diff --git a/pmoupnp/src/state_variables/instance_methods.rs b/pmoupnp/src/state_variables/instance_methods.rs
index b5d76ba8..693f691c 100644
--- a/pmoupnp/src/state_variables/instance_methods.rs
+++ b/pmoupnp/src/state_variables/instance_methods.rs
@@ -1,59 +1,116 @@
+use std::fmt;
+
use chrono::{DateTime, Utc};
+use tokio::sync::RwLock;
use xmltree::Element;
use crate::{
- UpnpObject, UpnpObjectType,
- object_trait::UpnpXml,
- state_variables::{StateVarInstance, StateVariable, UpnpVariable},
- variable_types::StateValue,
+ object_trait::{UpnpInstance, UpnpObject}, state_variables::{StateVarInstance, StateVariable, UpnpVariable}, variable_types::{StateValue, StateValueError, UpnpVarType}, UpnpObjectType, UpnpTyped, UpnpTypedInstance, UpnpTypedObject
};
impl UpnpVariable for StateVarInstance {
fn get_definition(&self) -> &StateVariable {
- return &self.definition;
- }
-}
-
-impl UpnpXml for StateVarInstance {
- fn to_xml_element(&self) -> Element {
- self.get_definition().to_xml_element()
+ return &self.model;
}
}
impl UpnpObject for StateVarInstance {
- fn as_upnp_object_type(&self) -> &UpnpObjectType {
- return &self.object;
+ async fn to_xml_element(&self) -> Element {
+ self.get_definition().to_xml_element().await
}
}
-impl StateVarInstance {
- pub fn new(from: &StateVariable) -> Self {
+impl UpnpVarType for StateVarInstance {
+ fn as_state_var_type(&self) -> crate::variable_types::StateVarType {
+ self.get_definition().as_state_var_type()
+ }
+}
+
+impl UpnpInstance for StateVarInstance {
+ type Model = StateVariable;
+
+ fn new(from: &StateVariable) -> Self {
Self {
object: UpnpObjectType {
name: from.object.name.clone(),
object_type: "StateVarInstance".to_string(),
},
- definition: from.clone(),
- value: from.get_default(),
- old_value: from.get_default(),
- last_modified: Utc::now(),
- last_notification: Utc::now(),
+ model: from.clone(),
+ value: RwLock::new(from.get_default()),
+ old_value: RwLock::new(from.get_default()),
+ last_modified: RwLock::new(Utc::now()),
+ last_notification: RwLock::new(Utc::now()),
}
}
- pub fn set_value(&mut self, new_value: StateValue) {
- self.old_value = self.value.clone();
- self.value = new_value;
- self.last_modified = Utc::now(); // mise à jour automatique
- }
+}
+impl UpnpTyped for StateVarInstance {
+ fn as_upnp_object_type(&self) -> &UpnpObjectType {
+ return &self.object;
+ }
+}
+
+impl UpnpTypedInstance for StateVarInstance {
+
+ fn get_model(&self) -> &Self::Model {
+ &self.model
+ }
+}
+
+impl fmt::Debug for StateVarInstance {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("StateVarInstance")
+ .field("object", &self.object)
+ .field("model", &self.model)
+ .field("value", &self.value)
+ .field("old_value", &self.old_value)
+ .field("last_modified", &self.last_modified)
+ .field("last_notification", &self.last_notification)
+ .finish()
+ }
+}
+
+impl Clone for StateVarInstance {
+ fn clone(&self) -> Self {
+ Self {
+ object: self.object.clone(),
+ model: self.model.clone(),
+ value: RwLock::new(self.value.blocking_read().clone()),
+ old_value: RwLock::new(self.old_value.blocking_read().clone()),
+ last_modified: RwLock::new(self.last_modified.blocking_read().clone()),
+ last_notification: RwLock::new(self.last_notification.blocking_read().clone()),
+ }
+ }
+}
+
+impl StateVarInstance {
+ pub async fn set_value(&self, new_value: StateValue) -> Result<(), StateValueError> {
+ // Validation du type
+ if self.as_state_var_type() != new_value.as_state_var_type() {
+ return Err(StateValueError::TypeError(
+ "Value type mismatch".to_string()
+ ));
+ }
+
+ // Mise à jour avec les locks
+ let mut old_val = self.old_value.write().await;
+ let mut val = self.value.write().await;
+ let mut modified = self.last_modified.write().await;
+
+ *old_val = val.clone();
+ *val = new_value;
+ *modified = Utc::now();
+
+ Ok(())
+ }
/// Accès à la valeur
- pub fn value(&self) -> &StateValue {
- &self.value
+ pub fn value(&self) -> StateValue {
+ self.value.blocking_read().clone()
}
/// Accès au timestamp
pub fn last_modified(&self) -> DateTime {
- self.last_modified
+ self.last_modified.blocking_read().clone()
}
}
diff --git a/pmoupnp/src/state_variables/mod.rs b/pmoupnp/src/state_variables/mod.rs
index ff83479a..2f991bbe 100644
--- a/pmoupnp/src/state_variables/mod.rs
+++ b/pmoupnp/src/state_variables/mod.rs
@@ -2,22 +2,22 @@ mod errors;
mod instance_methods;
mod variable_methods;
mod var_set_methods;
+mod var_inst_set_methods;
mod variable_trait;
use std::{
collections::HashMap,
- sync::{Arc, RwLock},
+ sync::Arc,
};
pub use crate::state_variables::variable_trait::UpnpVariable;
use bevy_reflect::Reflect;
use chrono::{DateTime, Utc};
pub use errors::StateVariableError;
+use tokio::sync::RwLock;
use crate::{
- UpnpObjectType,
- value_ranges::ValueRange,
- variable_types::{StateValue, StateVarType},
+ value_ranges::ValueRange, variable_types::{StateValue, StateVarType}, UpnpObjectSet, UpnpObjectType, UpnpSet
};
/// Type pour les fonctions de condition d'événement
@@ -46,16 +46,16 @@ pub struct StateVariable {
marshal: Option,
}
-#[derive(Debug, Default, Clone)]
-pub struct StateVariableSet {
- instances: HashMap,
-}
+pub type StateVariableSet = UpnpObjectSet;
pub struct StateVarInstance {
object: UpnpObjectType,
- definition: StateVariable,
- value: StateValue,
- old_value: StateValue,
- last_modified: DateTime,
- last_notification: DateTime,
+ model: StateVariable,
+ value: RwLock,
+ old_value: RwLock,
+ last_modified: RwLock>,
+ last_notification: RwLock>,
}
+
+pub type StateVarInstanceSet = UpnpObjectSet;
+
diff --git a/pmoupnp/src/state_variables/var_inst_set_methods.rs b/pmoupnp/src/state_variables/var_inst_set_methods.rs
new file mode 100644
index 00000000..7c4b65e7
--- /dev/null
+++ b/pmoupnp/src/state_variables/var_inst_set_methods.rs
@@ -0,0 +1,33 @@
+use std::collections::HashMap;
+
+use tokio::sync::RwLock;
+use xmltree::{Element, XMLNode};
+
+use crate::{state_variables::{StateVarInstance, StateVarInstanceSet, StateVariableSet}, UpnInstanceSet, UpnpObject, UpnpTyped};
+
+use crate::UpnpInstance;
+
+impl UpnpObject for StateVarInstanceSet {
+ async fn to_xml_element(&self) -> Element {
+ let mut elem = Element::new("serviceStateTable");
+
+ for state_var in self.all().await {
+ let state_var_elem = state_var.to_xml_element().await; // retourne un complet
+ elem.children.push(XMLNode::Element(state_var_elem));
+ }
+
+ elem
+ }
+}
+
+impl UpnpInstance for StateVarInstanceSet {
+ type Model = StateVariableSet;
+
+ fn new(_: &StateVariableSet) -> Self {
+ Self { objects: RwLock::new(HashMap::new()) }
+ }
+
+
+}
+
+
diff --git a/pmoupnp/src/state_variables/var_set_methods.rs b/pmoupnp/src/state_variables/var_set_methods.rs
index 85113128..646b7d64 100644
--- a/pmoupnp/src/state_variables/var_set_methods.rs
+++ b/pmoupnp/src/state_variables/var_set_methods.rs
@@ -1,34 +1,25 @@
-use std::collections::HashMap;
+use xmltree::{Element, XMLNode};
-use crate::{state_variables::{StateVariable, StateVariableSet}, UpnpObject};
+use crate::{object_trait::UpnpModel, state_variables::{StateVarInstanceSet, StateVariableSet}, UpnInstanceSet, UpnpObject};
-impl StateVariableSet {
- pub fn new() -> Self {
- Self {
- instances: HashMap::new(),
+
+impl UpnpObject for StateVariableSet {
+
+ async fn to_xml_element(&self) -> Element {
+ let mut elem = Element::new("serviceStateTable");
+
+ for state_var in self.get_all() {
+ let state_var_elem = state_var.to_xml_element().await; // retourne un complet
+ elem.children.push(XMLNode::Element(state_var_elem));
}
- }
- pub fn insert(&mut self, instance: StateVariable) {
- self.instances.insert(instance.get_name().clone(), instance);
+ elem
}
- pub fn contains(&self, name: &str) -> bool {
- self.instances.contains_key(name)
- }
-
- pub fn get(&self, name: &str) -> Option<&StateVariable> {
- self.instances.get(name)
- }
-
- pub fn iter(&self) -> impl Iterator
- {
- self.instances.values()
- }
-
- pub fn all(&self) -> Vec<&StateVariable> {
- self.instances.values().collect()
- }
-
-
}
+impl UpnpModel for StateVariableSet {
+ type Instance = StateVarInstanceSet;
+}
+
+
diff --git a/pmoupnp/src/state_variables/variable_methods.rs b/pmoupnp/src/state_variables/variable_methods.rs
index 54838466..27206053 100644
--- a/pmoupnp/src/state_variables/variable_methods.rs
+++ b/pmoupnp/src/state_variables/variable_methods.rs
@@ -1,17 +1,37 @@
use std::{
- collections::HashMap, fmt, sync::{Arc, RwLock}
+ collections::HashMap,
+ fmt,
+ sync::Arc,
};
+use tokio::sync::RwLock;
use xmltree::{Element, XMLNode};
use crate::{
- object_trait::UpnpXml, state_variables::{
- variable_trait::UpnpVariable, StateConditionFunc, StateVariable, StringValueParser, ValueSerializer
- }, value_ranges::ValueRange, variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType}, UpnpObject, UpnpObjectType
+ UpnpObjectType, UpnpTyped,
+ object_trait::{UpnpModel, UpnpObject},
+ state_variables::{
+ StateConditionFunc, StateVarInstance, StateVariable, StringValueParser, ValueSerializer,
+ variable_trait::UpnpVariable,
+ },
+ value_ranges::ValueRange,
+ variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType},
};
-impl UpnpXml for StateVariable {
- fn to_xml_element(&self) -> Element {
+impl UpnpTyped for StateVariable {
+ fn as_upnp_object_type(&self) -> &UpnpObjectType {
+ &self.object
+ }
+}
+
+impl UpnpVarType for StateVariable {
+ fn as_state_var_type(&self) -> StateVarType {
+ self.value_type.as_state_var_type() // utilise ton From<&StateValue> existant
+ }
+}
+
+impl UpnpObject for StateVariable {
+ async fn to_xml_element(&self) -> Element {
// Création de l'élément racine
let mut root = Element::new("stateVariable");
root.attributes.insert(
@@ -39,16 +59,15 @@ impl UpnpXml for StateVariable {
}
// si défini
- if let Ok(av) = self.allowed_values.read() {
- if !av.is_empty() {
- let mut list_elem = Element::new("allowedValueList");
- for val in av.iter() {
- let mut val_elem = Element::new("allowedValue");
- val_elem.children.push(XMLNode::Text(val.to_string()));
- list_elem.children.push(XMLNode::Element(val_elem));
- }
- root.children.push(XMLNode::Element(list_elem));
+ let av = self.allowed_values.read().await;
+ if !av.is_empty() {
+ let mut list_elem = Element::new("allowedValueList");
+ for val in av.iter() {
+ let mut val_elem = Element::new("allowedValue");
+ val_elem.children.push(XMLNode::Text(val.to_string()));
+ list_elem.children.push(XMLNode::Element(val_elem));
}
+ root.children.push(XMLNode::Element(list_elem));
}
// si défini
@@ -82,13 +101,10 @@ impl UpnpXml for StateVariable {
root
}
-
}
-impl UpnpObject for StateVariable {
- fn as_upnp_object_type(&self) -> &UpnpObjectType {
- return &self.object;
- }
+impl UpnpModel for StateVariable {
+ type Instance = StateVarInstance;
}
impl Clone for StateVariable {
@@ -98,8 +114,7 @@ impl Clone for StateVariable {
// si le lock est "poisoned" on panic - tu peux adapter la gestion si tu veux
let guard = self
.event_conditions
- .read()
- .expect("RwLock poisoned during clone");
+ .blocking_read();
// nécessite que Key: Clone, Value: Clone
Arc::new(RwLock::new(guard.clone()))
};
@@ -107,8 +122,7 @@ impl Clone for StateVariable {
let allowed_values_clone = {
let guard = self
.allowed_values
- .read()
- .expect("RwLock poisoned during clone");
+ .blocking_read();
Arc::new(RwLock::new(guard.clone()))
};
@@ -142,7 +156,7 @@ impl fmt::Debug for StateVariable {
"event_conditions",
&format_args!(
"len={}",
- self.event_conditions.read().map(|m| m.len()).unwrap_or(0)
+ self.event_conditions.blocking_read().len()
),
)
.field("description", &self.description)
@@ -152,22 +166,30 @@ impl fmt::Debug for StateVariable {
"allowed_values",
&format_args!(
"len={}",
- self.allowed_values.read().map(|v| v.len()).unwrap_or(0)
+ self.allowed_values.blocking_read().len()
),
)
.field("send_events", &self.send_events)
- .field("parse", &self.parse.as_ref().map(|_| "Some(StringValueParser)").unwrap_or("None"))
- .field("marshal", &self.marshal.as_ref().map(|_| "Some(ValueSerializer)").unwrap_or("None"))
+ .field(
+ "parse",
+ &self
+ .parse
+ .as_ref()
+ .map(|_| "Some(StringValueParser)")
+ .unwrap_or("None"),
+ )
+ .field(
+ "marshal",
+ &self
+ .marshal
+ .as_ref()
+ .map(|_| "Some(ValueSerializer)")
+ .unwrap_or("None"),
+ )
.finish()
}
}
-impl UpnpVarType for StateVariable {
- fn as_state_var_type(&self) -> StateVarType {
- self.value_type // utilise ton From<&StateValue> existant
- }
-}
-
impl UpnpVariable for StateVariable {
fn get_definition(&self) -> &StateVariable {
return self;
@@ -276,18 +298,18 @@ impl StateVariable {
pub fn add_event_condition(&self, name: String, func: StateConditionFunc) {
// on lock en écriture
- let mut guard = self.event_conditions.write().unwrap();
+ let mut guard = self.event_conditions.blocking_write();
guard.insert(name, func);
// le lock est automatiquement relâché ici (RAII)
}
pub fn remove_event_condition(&self, name: &str) {
- let mut guard = self.event_conditions.write().unwrap();
+ let mut guard = self.event_conditions.blocking_write();
guard.remove(name);
}
pub fn clear_event_conditions(&mut self) {
- let mut guard = self.event_conditions.write().unwrap();
+ let mut guard = self.event_conditions.blocking_write();
guard.clear()
}
@@ -312,8 +334,7 @@ impl StateVariable {
pub fn extend_allowed_values(&mut self, values: &[StateValue]) -> Result<(), StateValueError> {
let mut av = self
.allowed_values
- .write()
- .map_err(|_| StateValueError::TypeError("Lock poisoned".to_string()))?;
+ .blocking_write();
for v in values {
if self.as_state_var_type() == v.as_state_var_type() {
@@ -331,8 +352,7 @@ impl StateVariable {
pub fn push_allowed_value(&mut self, value: &StateValue) -> Result<(), StateValueError> {
let mut av = self
.allowed_values
- .write()
- .map_err(|_| StateValueError::TypeError("Lock poisoned".to_string()))?;
+ .blocking_write();
if self.as_state_var_type() == value.as_state_var_type() {
av.push(value.clone());
diff --git a/pmoupnp/src/state_variables/variable_trait.rs b/pmoupnp/src/state_variables/variable_trait.rs
index a6c4e3a9..52a2f19a 100644
--- a/pmoupnp/src/state_variables/variable_trait.rs
+++ b/pmoupnp/src/state_variables/variable_trait.rs
@@ -3,46 +3,199 @@ use crate::{
variable_types::{StateValue, UpnpVarType},
};
+/// Trait pour accéder aux propriétés et contraintes d'une variable UPnP.
+///
+/// Ce trait fournit une interface uniforme pour interroger les métadonnées,
+/// contraintes et comportements d'une variable UPnP, qu'il s'agisse d'une
+/// définition ([`StateVariable`]) ou d'une instance ([`StateVarInstance`]).
+///
+/// # Architecture
+///
+/// Le trait utilise le pattern "trait avec implémentation par défaut" :
+/// - Une seule méthode requise : [`get_definition`](Self::get_definition)
+/// - Toutes les autres méthodes sont implémentées par défaut en déléguant à la définition
+///
+/// Cela permet une interface cohérente entre modèles et instances sans duplication de code.
+///
+/// # Hiérarchie
+///
+/// ```text
+/// UpnpVariable
+/// ├─> StateVariable (get_definition() retourne self)
+/// └─> StateVarInstance (get_definition() retourne self.definition)
+/// ```
+///
+/// # Examples
+///
+/// ```ignore
+/// fn display_variable_info(var: &V) {
+/// println!("Variable: {}", var.get_definition().get_name());
+///
+/// if var.has_default() {
+/// println!("Default: {:?}", var.get_default());
+/// }
+///
+/// if var.has_range() {
+/// println!("Has range constraints");
+/// }
+///
+/// if var.has_allowed_values() {
+/// println!("Has allowed values list");
+/// }
+/// }
+/// ```
pub trait UpnpVariable {
+ /// Retourne une référence vers la définition de la variable.
+ ///
+ /// Cette méthode est la base de toutes les autres méthodes du trait.
+ ///
+ /// # Implementation
+ ///
+ /// - Pour [`StateVariable`] : retourne `self`
+ /// - Pour [`StateVarInstance`] : retourne `self.definition`
fn get_definition(&self) -> &StateVariable;
+ /// Indique si la variable a un pas (step) défini.
+ ///
+ /// Le pas définit l'incrément minimal entre deux valeurs valides pour
+ /// les types numériques.
+ ///
+ /// # Returns
+ ///
+ /// `true` si un pas est défini, `false` sinon.
+ ///
+ /// # Examples
+ ///
+ /// ```ignore
+ /// if var.has_step() {
+ /// println!("Step: {:?}", var.get_step());
+ /// }
+ /// ```
fn has_step(&self) -> bool {
- return self.get_definition().step.is_some();
+ self.get_definition().step.is_some()
}
+ /// Retourne le pas (step) de la variable s'il est défini.
+ ///
+ /// # Returns
+ ///
+ /// - `Some(StateValue)` si un pas est défini
+ /// - `None` sinon
+ ///
+ /// # See also
+ ///
+ /// - [`has_step`](Self::has_step) pour tester l'existence
fn get_step(&self) -> Option {
- return self.get_definition().step.clone();
+ self.get_definition().step.clone()
}
+ /// Indique si la variable a une plage de valeurs (range) définie.
+ ///
+ /// La plage définit les valeurs minimale et maximale acceptables.
+ ///
+ /// # Returns
+ ///
+ /// `true` si une plage est définie, `false` sinon.
fn has_range(&self) -> bool {
- return self.get_definition().value_range.is_some();
+ self.get_definition().value_range.is_some()
}
+ /// Indique si la variable est modifiable.
+ ///
+ /// Une variable non modifiable est en lecture seule.
+ ///
+ /// # Returns
+ ///
+ /// `true` si la variable peut être modifiée, `false` sinon.
fn is_modifiable(&self) -> bool {
- return self.get_definition().modifiable;
+ self.get_definition().modifiable
}
+ /// Indique si la variable a des conditions d'événement définies.
+ ///
+ /// Les conditions d'événement déterminent quand des notifications
+ /// doivent être envoyées lors de changements de valeur.
+ ///
+ /// # Returns
+ ///
+ /// `true` si au moins une condition d'événement existe, `false` sinon.
+ ///
+ /// # Note
+ ///
+ /// Retourne `false` si le lock est empoisonné (poisoned).
fn has_event_conditions(&self) -> bool {
- return self.get_definition().event_conditions.read().unwrap().len() > 0;
+ let guard = self.get_definition().event_conditions.blocking_read();
+ !guard.is_empty()
}
+ /// Vérifie si une condition d'événement spécifique existe.
+ ///
+ /// # Arguments
+ ///
+ /// * `name` - Le nom de la condition à rechercher
+ ///
+ /// # Returns
+ ///
+ /// `true` si la condition existe, `false` sinon.
+ ///
+ /// # Note
+ ///
+ /// Retourne `false` si le lock est empoisonné (poisoned).
fn has_event_condition(&self, name: &String) -> bool {
- let guard = self.get_definition().event_conditions.read().unwrap();
- return guard.contains_key(name);
+ let guard = self.get_definition().event_conditions.blocking_read();
+ guard.contains_key(name)
}
+ /// Indique si la variable a une description non vide.
+ ///
+ /// # Returns
+ ///
+ /// `true` si une description existe et n'est pas vide, `false` sinon.
fn has_description(&self) -> bool {
- return !String::is_empty(&self.get_definition().description);
+ !self.get_definition().description.is_empty()
}
+ /// Retourne la description de la variable.
+ ///
+ /// # Returns
+ ///
+ /// La description sous forme de `String`. Peut être vide.
+ ///
+ /// # See also
+ ///
+ /// - [`has_description`](Self::has_description) pour tester si non vide
fn get_description(&self) -> String {
- return self.get_definition().description.clone();
+ self.get_definition().description.clone()
}
+ /// Indique si la variable a une valeur par défaut définie explicitement.
+ ///
+ /// # Returns
+ ///
+ /// `true` si une valeur par défaut est explicitement définie, `false` sinon.
+ ///
+ /// # Note
+ ///
+ /// Même si cette méthode retourne `false`, [`get_default`](Self::get_default)
+ /// retournera toujours une valeur (la valeur par défaut du type).
fn has_default(&self) -> bool {
- return self.get_definition().default_value.is_some();
+ self.get_definition().default_value.is_some()
}
+ /// Retourne la valeur par défaut de la variable.
+ ///
+ /// # Returns
+ ///
+ /// La valeur par défaut. Si aucune valeur par défaut n'est explicitement
+ /// définie, retourne la valeur par défaut du type de la variable
+ /// (ex: 0 pour les entiers, chaîne vide pour String, etc.).
+ ///
+ /// # Examples
+ ///
+ /// ```ignore
+ /// let default = var.get_default();
+ /// println!("Default value: {:?}", default);
+ /// ```
fn get_default(&self) -> StateValue {
self.get_definition()
.default_value
@@ -50,23 +203,99 @@ pub trait UpnpVariable {
.unwrap_or_else(|| self.get_definition().as_state_var_type().default_value())
}
+ /// Indique si la variable a une liste de valeurs autorisées.
+ ///
+ /// Lorsqu'une liste de valeurs autorisées est définie, seules ces valeurs
+ /// sont acceptables pour la variable.
+ ///
+ /// # Returns
+ ///
+ /// `true` si une liste non vide de valeurs autorisées existe, `false` sinon.
+ ///
+ /// # Note
+ ///
+ /// Retourne `false` si le lock est empoisonné (poisoned).
fn has_allowed_values(&self) -> bool {
- return self.get_definition().allowed_values.read().unwrap().len() > 0;
+ let guard = self.get_definition()
+ .allowed_values
+ .blocking_read();
+
+ !guard.is_empty()
}
- fn is_an_allowed_values(&self, value: &StateValue) -> bool {
- let guard = self.get_definition().allowed_values.read().unwrap();
- return guard.contains(value);
+ /// Vérifie si une valeur fait partie des valeurs autorisées.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - La valeur à vérifier
+ ///
+ /// # Returns
+ ///
+ /// `true` si la valeur est dans la liste des valeurs autorisées, `false` sinon.
+ /// Retourne également `false` si aucune liste de valeurs autorisées n'est définie
+ /// ou si le lock est empoisonné.
+ ///
+ /// # Examples
+ ///
+ /// ```ignore
+ /// let value = StateValue::String("ON".to_string());
+ /// if var.is_an_allowed_value(&value) {
+ /// println!("Value is allowed");
+ /// }
+ /// ```
+ ///
+ /// # Note
+ ///
+ /// Si aucune liste de valeurs autorisées n'est définie, cette méthode
+ /// retourne `false`. Utilisez [`has_allowed_values`](Self::has_allowed_values)
+ /// pour distinguer "pas de liste" de "valeur non autorisée".
+ fn is_an_allowed_value(&self, value: &StateValue) -> bool {
+ let guard = self.get_definition()
+ .allowed_values
+ .blocking_read();
+
+ guard.contains(value)
}
+ /// Indique si la variable envoie des notifications d'événement.
+ ///
+ /// Les notifications d'événement sont envoyées aux abonnés lorsque
+ /// la valeur de la variable change.
+ ///
+ /// # Returns
+ ///
+ /// `true` si les notifications sont activées, `false` sinon.
+ ///
+ /// # See also
+ ///
+ /// - [`has_event_conditions`](Self::has_event_conditions) pour vérifier
+ /// les conditions d'envoi d'événements
fn is_sending_notification(&self) -> bool {
self.get_definition().send_events
}
+ /// Indique si la variable a un parser de valeur personnalisé.
+ ///
+ /// Un parser personnalisé est utilisé pour convertir des chaînes de
+ /// caractères en valeurs typées. Disponible uniquement pour les variables
+ /// de type String.
+ ///
+ /// # Returns
+ ///
+ /// `true` si un parser est défini, `false` sinon.
fn has_value_parser(&self) -> bool {
self.get_definition().parse.is_some()
}
+ /// Indique si la variable a un marshaler de valeur personnalisé.
+ ///
+ /// Un marshaler personnalisé est utilisé pour sérialiser des valeurs
+ /// en chaînes de caractères. Disponible uniquement pour les variables
+ /// de type String.
+ ///
+ /// # Returns
+ ///
+ /// `true` si un marshaler est défini, `false` sinon.
fn has_value_marshaler(&self) -> bool {
self.get_definition().marshal.is_some()
}
diff --git a/pmoupnp/src/variable_types/mod.rs b/pmoupnp/src/variable_types/mod.rs
index f4144a76..ee60afa4 100644
--- a/pmoupnp/src/variable_types/mod.rs
+++ b/pmoupnp/src/variable_types/mod.rs
@@ -41,6 +41,8 @@ use uuid::Uuid;
pub use errors::StateValueError;
pub use type_trait::UpnpVarType;
+pub use crate::variable_types::value_trait::UpnpValue;
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StateVarType {
UI1, // Unsigned 8-bit integer
@@ -68,7 +70,7 @@ pub enum StateVarType {
URI, // Uniform Resource Identifier
}
-#[derive(Debug, Clone)]
+#[derive(Clone, Debug)]
pub enum StateValue {
UI1(u8),
UI2(u16),
diff --git a/pmoupnp/src/variable_types/value_trait.rs b/pmoupnp/src/variable_types/value_trait.rs
index 87b9edac..7dccd192 100644
--- a/pmoupnp/src/variable_types/value_trait.rs
+++ b/pmoupnp/src/variable_types/value_trait.rs
@@ -1 +1 @@
-pub trait UpnpValue {}
+pub trait UpnpValue: Clone {}
diff --git a/pmoupnp/src/variable_types/values_from_str.rs b/pmoupnp/src/variable_types/values_from_str.rs
index db8e6d3b..1054f650 100644
--- a/pmoupnp/src/variable_types/values_from_str.rs
+++ b/pmoupnp/src/variable_types/values_from_str.rs
@@ -1,5 +1,4 @@
use std::convert::TryFrom;
-use url::Url;
use crate::variable_types::{StateValue, StateValueError};
diff --git a/pmoutils/src/ip_utils.rs b/pmoutils/src/ip_utils.rs
index f16c9bae..590b27bc 100644
--- a/pmoutils/src/ip_utils.rs
+++ b/pmoutils/src/ip_utils.rs
@@ -1,8 +1,37 @@
use get_if_addrs::get_if_addrs;
use std::net::UdpSocket;
+/// Devine l'adresse IP locale de la machine.
+///
+/// Cette fonction tente de déterminer l'adresse IP locale en créant une connexion UDP
+/// vers un serveur DNS public (8.8.8.8). Cette technique permet d'identifier l'interface
+/// réseau qui serait utilisée pour communiquer avec Internet.
+///
+/// # Fonctionnement
+///
+/// 1. Crée un socket UDP lié à `0.0.0.0:0` (n'importe quelle interface, port aléatoire)
+/// 2. Tente une connexion (non effective pour UDP) vers `8.8.8.8:80`
+/// 3. Récupère l'adresse IP locale du socket
+/// 4. En cas d'échec à n'importe quelle étape, retourne `127.0.0.1`
+///
+/// # Returns
+///
+/// Retourne l'adresse IP locale sous forme de `String`, ou `"127.0.0.1"` en cas d'erreur.
+///
+/// # Examples
+///
+/// ```
+/// let ip = guess_local_ip();
+/// println!("IP locale détectée: {}", ip);
+/// // Affiche par exemple: "IP locale détectée: 192.168.1.42"
+/// ```
+///
+/// # Note
+///
+/// Cette méthode ne crée pas de véritable connexion réseau (UDP est sans connexion),
+/// elle demande simplement au système d'exploitation quelle interface serait utilisée
+/// pour joindre l'adresse cible.
pub fn guess_local_ip() -> String {
- // On tente de deviner l'IP locale
match UdpSocket::bind("0.0.0.0:0") {
Ok(socket) => {
if socket.connect("8.8.8.8:80").is_ok() {
@@ -10,13 +39,43 @@ pub fn guess_local_ip() -> String {
return local_addr.ip().to_string();
}
}
- // Si erreur sur connect ou récupération de l'adresse
"127.0.0.1".to_string()
}
- Err(_) => "127.0.0.1".to_string(), // Si bind échoue
+ Err(_) => "127.0.0.1".to_string(),
}
}
+/// Liste toutes les adresses IP non-loopback des interfaces réseau.
+///
+/// Parcourt toutes les interfaces réseau de la machine et collecte leurs adresses IPv4,
+/// en excluant les adresses de loopback (127.0.0.1).
+///
+/// # Returns
+///
+/// Retourne une `HashMap` où :
+/// - **Clé** : nom de l'interface réseau (ex: `"eth0"`, `"wlan0"`, `"en0"`)
+/// - **Valeur** : vecteur des adresses IP (format String) associées à cette interface
+///
+/// En cas d'erreur lors de la récupération des interfaces, retourne une HashMap
+/// contenant une entrée `"error"` avec un message d'erreur.
+///
+/// # Examples
+///
+/// ```
+/// let ips = list_all_ips();
+/// for (interface, addresses) in ips {
+/// println!("Interface {}: {:?}", interface, addresses);
+/// }
+/// // Affiche par exemple:
+/// // Interface eth0: ["192.168.1.42"]
+/// // Interface wlan0: ["10.0.0.15"]
+/// ```
+///
+/// # Note
+///
+/// - Seules les adresses IPv4 sont retournées
+/// - Les adresses de loopback (127.x.x.x) sont filtrées
+/// - Les adresses IPv6 sont ignorées
fn list_all_ips() -> std::collections::HashMap> {
let mut result = std::collections::HashMap::new();
@@ -42,3 +101,163 @@ fn list_all_ips() -> std::collections::HashMap> {
result
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::net::IpAddr;
+
+ #[test]
+ fn test_guess_local_ip_returns_valid_ip() {
+ let ip = guess_local_ip();
+
+ // Vérifie que le résultat est parsable comme une IP
+ assert!(ip.parse::().is_ok(), "Should return a valid IP address");
+ }
+
+ #[test]
+ fn test_guess_local_ip_not_empty() {
+ let ip = guess_local_ip();
+
+ assert!(!ip.is_empty(), "IP should not be empty");
+ }
+
+ #[test]
+ fn test_guess_local_ip_is_ipv4() {
+ let ip = guess_local_ip();
+
+ if let Ok(parsed_ip) = ip.parse::() {
+ assert!(parsed_ip.is_ipv4(), "Should return an IPv4 address");
+ }
+ }
+
+ #[test]
+ fn test_guess_local_ip_fallback_is_localhost() {
+ // Ce test vérifie que si aucune IP n'est trouvée, on retourne 127.0.0.1
+ // (difficile à tester sans mocker, mais on vérifie la cohérence)
+ let ip = guess_local_ip();
+ let parsed = ip.parse::().unwrap();
+
+ // L'IP doit être soit locale (127.0.0.1) soit une IP privée valide
+ assert!(
+ parsed.is_loopback() || is_private_ip(&ip),
+ "IP should be either loopback or private"
+ );
+ }
+
+ #[test]
+ fn test_list_all_ips_no_loopback() {
+ let ips = list_all_ips();
+
+ // Vérifie qu'aucune adresse de loopback n'est présente
+ for (_, addresses) in ips.iter() {
+ for addr in addresses {
+ if let Ok(parsed_ip) = addr.parse::() {
+ assert!(
+ !parsed_ip.is_loopback(),
+ "Loopback addresses should be filtered out"
+ );
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn test_list_all_ips_only_ipv4() {
+ let ips = list_all_ips();
+
+ // Vérifie que seules des adresses IPv4 sont retournées
+ for (iface_name, addresses) in ips.iter() {
+ if iface_name == "error" {
+ continue; // Skip error entries
+ }
+
+ for addr in addresses {
+ if let Ok(parsed_ip) = addr.parse::() {
+ assert!(
+ parsed_ip.is_ipv4(),
+ "Only IPv4 addresses should be returned"
+ );
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn test_list_all_ips_valid_format() {
+ let ips = list_all_ips();
+
+ // Vérifie que toutes les IPs sont dans un format valide
+ for (iface_name, addresses) in ips.iter() {
+ if iface_name == "error" {
+ continue;
+ }
+
+ for addr in addresses {
+ assert!(
+ addr.parse::().is_ok(),
+ "Each IP should be in valid format: {}",
+ addr
+ );
+ }
+ }
+ }
+
+ #[test]
+ fn test_list_all_ips_interface_names_not_empty() {
+ let ips = list_all_ips();
+
+ // Vérifie que les noms d'interface ne sont pas vides
+ for (iface_name, _) in ips.iter() {
+ assert!(!iface_name.is_empty(), "Interface names should not be empty");
+ }
+ }
+
+ #[test]
+ fn test_list_all_ips_no_duplicate_ips_per_interface() {
+ let ips = list_all_ips();
+
+ // Vérifie qu'il n'y a pas de doublons par interface
+ for (iface_name, addresses) in ips.iter() {
+ if iface_name == "error" {
+ continue;
+ }
+
+ let unique_addresses: std::collections::HashSet<_> = addresses.iter().collect();
+ assert_eq!(
+ addresses.len(),
+ unique_addresses.len(),
+ "No duplicate IPs should exist for interface {}",
+ iface_name
+ );
+ }
+ }
+
+ // Fonction helper pour les tests
+ fn is_private_ip(ip_str: &str) -> bool {
+ if let Ok(ip) = ip_str.parse::() {
+ match ip {
+ IpAddr::V4(ipv4) => {
+ let octets = ipv4.octets();
+ // Plages privées: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
+ octets[0] == 10
+ || (octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31)
+ || (octets[0] == 192 && octets[1] == 168)
+ }
+ IpAddr::V6(_) => false,
+ }
+ } else {
+ false
+ }
+ }
+
+ #[test]
+ fn test_helper_is_private_ip() {
+ // Tests pour la fonction helper
+ assert!(is_private_ip("10.0.0.1"));
+ assert!(is_private_ip("172.16.0.1"));
+ assert!(is_private_ip("192.168.1.1"));
+ assert!(!is_private_ip("8.8.8.8"));
+ assert!(!is_private_ip("127.0.0.1")); // loopback n'est pas "privé" au sens réseau local
+ }
+}
\ No newline at end of file
diff --git a/pmoutils/src/lib.rs b/pmoutils/src/lib.rs
index d5108408..e578b603 100644
--- a/pmoutils/src/lib.rs
+++ b/pmoutils/src/lib.rs
@@ -1,3 +1,20 @@
+/// Utilitaires pour la gestion des adresses IP réseau.
+///
+/// Ce module fournit des fonctions pour détecter et lister les adresses IP
+/// des interfaces réseau locales de la machine.
+///
+/// # Fonctions principales
+///
+/// - [`guess_local_ip`] : Devine l'adresse IP locale utilisée pour les connexions sortantes
+///
+/// # Examples
+///
+/// ```
+/// use votre_crate::guess_local_ip;
+///
+/// let ip = guess_local_ip();
+/// println!("Adresse IP locale: {}", ip);
+/// ```
mod ip_utils;
-pub use ip_utils::guess_local_ip;
+pub use ip_utils::guess_local_ip;
\ No newline at end of file
diff --git a/structure.svg b/structure.svg
new file mode 100644
index 00000000..e69de29b
diff --git a/tools/build_prompt b/tools/build_prompt
index 8b09b058..7e231ce6 100755
--- a/tools/build_prompt
+++ b/tools/build_prompt
@@ -12,13 +12,12 @@ EOF
echo ============== Debut des sources des packages ===============
for package in $*; do
- pushd $package 2>&1 >/dev/null
- for f in *.go; do
- echo "------- $package/$f ------"
- cat $f
+ find $package -type f \( -name '*.rs' -o -name '*.toml' \) -print0 |
+ while IFS= read -r -d '' file; do
+ echo "------- $file ------"
+ cat $file
echo "-----------------"
done
- popd 2>&1 >/dev/null
done
echo ============== Fin des sources des packages ===============