Gros refactoring
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use pmoupnp::{mediarenderer::avtransport::actions::{SETAVTRANSPORTURI}, server::{
|
use pmoupnp::{mediarenderer::avtransport::actions::{SETAVTRANSPORTURI}, server::{
|
||||||
logs::{log_dump, log_sse, LogState, SseLayer}, ServerBuilder, Webapp
|
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::Registry;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -47,7 +47,7 @@ async fn main() {
|
|||||||
|
|
||||||
server.add_redirect("/", "/app").await;
|
server.add_redirect("/", "/app").await;
|
||||||
|
|
||||||
info!("{}",SETAVTRANSPORTURI.to_markdown());
|
info!("{}",SETAVTRANSPORTURI.to_markdown().await);
|
||||||
|
|
||||||
server.start().await;
|
server.start().await;
|
||||||
server.wait().await;
|
server.wait().await;
|
||||||
|
|||||||
3168
deps.svg
Normal file
3168
deps.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 182 KiB |
46
doc/traits_object.md
Normal file
46
doc/traits_object.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
Clone[Clone<br/><i>std trait</i>]:::stdTrait
|
||||||
|
Debug[Debug<br/><i>std trait</i>]:::stdTrait
|
||||||
|
|
||||||
|
UpnpDeepClone[UpnpDeepClone<br/>deep_clone]:::baseTrait
|
||||||
|
|
||||||
|
UpnpObject[UpnpObject<br/>to_xml_element<br/>to_xml<br/>to_markdown]:::baseTrait
|
||||||
|
|
||||||
|
UpnpModel[UpnpModel<br/>create_instance]:::derived1
|
||||||
|
UpnpInstance[UpnpInstance<br/>new]:::derived1
|
||||||
|
UpnpTyped[UpnpTyped<br/>get_name<br/>get_object_type]:::derived1
|
||||||
|
UpnpSet[UpnpSet<br/>is_set]:::derived1
|
||||||
|
|
||||||
|
UpnpTypedObject[UpnpTypedObject<br/><i>marker</i>]:::derived2
|
||||||
|
|
||||||
|
UpnpTypedInstance[UpnpTypedInstance<br/><i>marker</i>]:::derived3
|
||||||
|
UpnpModelSet[UpnpModelSet<br/><i>marker</i>]:::derived3
|
||||||
|
UpnInstanceSet[UpnInstanceSet<br/><i>marker</i>]:::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
|
||||||
|
```
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use xmltree::{Element, XMLNode};
|
use xmltree::{Element, XMLNode};
|
||||||
|
|
||||||
use crate::actions::Action;
|
use crate::actions::Action;
|
||||||
use crate::actions::Argument;
|
use crate::actions::Argument;
|
||||||
use crate::actions::ArgumentSet;
|
use crate::actions::ArgumentSet;
|
||||||
use crate::actions::ActionInstance;
|
use crate::actions::ActionInstance;
|
||||||
use crate::UpnpXml;
|
use crate::UpnpInstance;
|
||||||
use crate::{UpnpObject, UpnpObjectType};
|
use crate::UpnpObject;
|
||||||
|
use crate::UpnpTyped;
|
||||||
|
use crate::UpnpTypedInstance;
|
||||||
|
use crate::{UpnpTypedObject, UpnpObjectType};
|
||||||
|
|
||||||
impl UpnpXml for ActionInstance {
|
impl UpnpObject for ActionInstance {
|
||||||
fn to_xml_element(&self) -> Element {
|
async fn to_xml_element(&self) -> Element {
|
||||||
let mut elem = Element::new("action");
|
let mut elem = Element::new("action");
|
||||||
|
|
||||||
// <name>
|
// <name>
|
||||||
@@ -17,21 +22,24 @@ fn to_xml_element(&self) -> Element {
|
|||||||
elem.children.push(XMLNode::Element(name_elem));
|
elem.children.push(XMLNode::Element(name_elem));
|
||||||
|
|
||||||
// déplacer tous les enfants de args_elem dans un nouvel Element
|
// 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.children.push(XMLNode::Element(args_container));
|
||||||
|
|
||||||
elem
|
elem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl UpnpObject for ActionInstance {
|
|
||||||
|
impl UpnpTyped for ActionInstance {
|
||||||
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
||||||
return &self.object;
|
return &self.object;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionInstance {
|
impl UpnpInstance for ActionInstance {
|
||||||
|
|
||||||
pub fn new(action: &Action) -> Self {
|
type Model = Action;
|
||||||
|
|
||||||
|
fn new(action: &Action) -> Self {
|
||||||
Self {
|
Self {
|
||||||
object: UpnpObjectType {
|
object: UpnpObjectType {
|
||||||
name: action.get_name().clone(),
|
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<Arc<Argument>> {
|
||||||
|
self.model.arguments.get_by_name(name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn arguments_set(&self) -> &ArgumentSet {
|
pub fn arguments_set(&self) -> &ArgumentSet {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
UpnpObject, UpnpXml,
|
UpnpTypedObject, UpnpObject,
|
||||||
actions::{ActionInstance, ActionInstanceSet},
|
actions::{ActionInstanceSet},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
|
||||||
use xmltree::{Element,XMLNode};
|
use xmltree::{Element,XMLNode};
|
||||||
|
|
||||||
impl UpnpXml for ActionInstanceSet {
|
impl UpnpObject for ActionInstanceSet {
|
||||||
// Méthode pour convertir en XML (à implémenter avec une librairie XML)
|
// 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");
|
let mut elem = Element::new("actionList");
|
||||||
|
|
||||||
for action in self.iter() {
|
for action in self.all().await {
|
||||||
let action_elem = action.to_xml_element(); // retourne un <action> complet
|
let action_elem = action.to_xml_element().await; // retourne un <action> complet
|
||||||
elem.children.push(XMLNode::Element(action_elem));
|
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<Item = &ActionInstance> {
|
|
||||||
self.instances.values()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn all(&self) -> Vec<&ActionInstance> {
|
|
||||||
self.instances.values().collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use xmltree::{Element,XMLNode};
|
use xmltree::{Element,XMLNode};
|
||||||
|
|
||||||
use crate::UpnpXml;
|
use crate::actions::ActionInstance;
|
||||||
|
use crate::UpnpModel;
|
||||||
|
use crate::UpnpObject;
|
||||||
use crate::actions::Action;
|
use crate::actions::Action;
|
||||||
use crate::actions::Argument;
|
use crate::actions::Argument;
|
||||||
use crate::actions::ArgumentSet;
|
use crate::actions::ArgumentSet;
|
||||||
use crate::{UpnpObject, UpnpObjectType};
|
use crate::UpnpTyped;
|
||||||
|
use crate::{UpnpTypedObject, UpnpObjectType};
|
||||||
|
|
||||||
impl UpnpXml for Action {
|
impl UpnpObject for Action {
|
||||||
fn to_xml_element(&self) -> Element {
|
async fn to_xml_element(&self) -> Element {
|
||||||
let mut action_elem = Element::new("action");
|
let mut action_elem = Element::new("action");
|
||||||
|
|
||||||
// <name>
|
// <name>
|
||||||
@@ -18,13 +23,20 @@ impl UpnpXml for Action {
|
|||||||
action_elem.children.push(XMLNode::Element(name_elem));
|
action_elem.children.push(XMLNode::Element(name_elem));
|
||||||
|
|
||||||
// <argumentList>
|
// <argumentList>
|
||||||
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.children.push(XMLNode::Element(args_elem));
|
||||||
|
|
||||||
action_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 {
|
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
||||||
return &self.object;
|
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<Argument>) {
|
||||||
self.arguments.insert(arg);
|
self.arguments.insert(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ use xmltree::{Element, XMLNode};
|
|||||||
use crate::actions::Action;
|
use crate::actions::Action;
|
||||||
use crate::actions::ActionSet;
|
use crate::actions::ActionSet;
|
||||||
use crate::actions::errors::ActionError;
|
use crate::actions::errors::ActionError;
|
||||||
|
use crate::UpnpTypedObject;
|
||||||
use crate::UpnpObject;
|
use crate::UpnpObject;
|
||||||
use crate::UpnpXml;
|
|
||||||
|
|
||||||
impl UpnpXml for ActionSet {
|
impl UpnpObject for ActionSet {
|
||||||
fn to_xml_element(&self) -> Element {
|
async fn to_xml_element(&self) -> Element {
|
||||||
let mut elem = Element::new("actionList");
|
let mut elem = Element::new("actionList");
|
||||||
|
|
||||||
for action in self.iter() {
|
for action in self.all().await {
|
||||||
let action_elem = action.to_xml_element(); // retourne un <action> complet
|
let action_elem = action.to_xml_element().await; // retourne un <action> complet
|
||||||
elem.children.push(XMLNode::Element(action_elem));
|
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<Item = &Action> {
|
|
||||||
self.actions.values()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn all(&self) -> Vec<&Action> {
|
|
||||||
self.actions.values().collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
31
pmoupnp/src/actions/arg_inst_set_methods.rs
Normal file
31
pmoupnp/src/actions/arg_inst_set_methods.rs
Normal file
@@ -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 <stateVariable> 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()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
42
pmoupnp/src/actions/arg_instance_methods.rs
Normal file
42
pmoupnp/src/actions/arg_instance_methods.rs
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
use crate::UpnpObject;
|
use crate::actions::ArgInstanceSet;
|
||||||
|
use crate::{UpnpModel, UpnpTypedObject};
|
||||||
use crate::{
|
use crate::{
|
||||||
UpnpXml,
|
UpnpObject,
|
||||||
actions::{Argument, ArgumentSet},
|
actions::{ArgumentSet},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::RwLock;
|
||||||
use xmltree::Element;
|
use xmltree::Element;
|
||||||
|
|
||||||
impl UpnpXml for ArgumentSet {
|
impl UpnpObject for ArgumentSet {
|
||||||
// Méthode pour convertir en XML (à implémenter avec une librairie XML)
|
// Méthode pour convertir en XML (à implémenter avec une librairie XML)
|
||||||
fn to_xml_element(&self) -> Element {
|
fn to_xml_element(&self) -> Element {
|
||||||
let mut elem = Element::new("argumentList");
|
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 <argumentList> contenant 1 ou 2 <argument>
|
let arg_elem = arg.to_xml_element(); // toujours un <argumentList> contenant 1 ou 2 <argument>
|
||||||
|
|
||||||
// Pour InOut, on ajoute tous les enfants du <argumentList> généré
|
// Pour InOut, on ajoute tous les enfants du <argumentList> généré
|
||||||
@@ -23,30 +25,7 @@ impl UpnpXml for ArgumentSet {
|
|||||||
elem
|
elem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl ArgumentSet {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
arguments: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert(&mut self, arg: Argument) {
|
impl UpnpModel for ArgumentSet {
|
||||||
self.arguments.insert(arg.get_name().clone(), arg);
|
type Instance = ArgInstanceSet;
|
||||||
}
|
|
||||||
|
|
||||||
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<Item = &Argument> {
|
|
||||||
self.arguments.values()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn all(&self) -> Vec<&Argument> {
|
|
||||||
self.arguments.values().collect()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use xmltree::{Element, XMLNode};
|
use xmltree::{Element, XMLNode};
|
||||||
|
|
||||||
use crate::{
|
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 {
|
impl UpnpTyped for Argument {
|
||||||
fn to_xml_element(&self) -> Element {
|
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");
|
let mut parent = Element::new("argumentList");
|
||||||
|
|
||||||
if self.is_in() && self.is_out() {
|
if self.is_in() && self.is_out() {
|
||||||
@@ -34,14 +42,14 @@ impl UpnpXml for Argument {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpnpObject for Argument {
|
impl UpnpModel for Argument {
|
||||||
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
type Instance = ArgumentInstance;
|
||||||
return &self.object;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
impl Argument {
|
impl Argument {
|
||||||
fn new(name: String, state_variable: StateVariable) -> Self {
|
fn new(name: String, state_variable: Arc<StateVariable>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
object: UpnpObjectType {
|
object: UpnpObjectType {
|
||||||
name,
|
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<StateVariable>) -> Self {
|
||||||
let mut arg = Self::new(name, state_variable);
|
let mut arg = Self::new(name, state_variable);
|
||||||
arg.is_in = true;
|
arg.is_in = true;
|
||||||
arg
|
arg
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_out(name: String, state_variable: StateVariable) -> Self {
|
pub fn new_out(name: String, state_variable: Arc<StateVariable>) -> Self {
|
||||||
let mut arg = Self::new(name, state_variable);
|
let mut arg = Self::new(name, state_variable);
|
||||||
arg.is_out = true;
|
arg.is_out = true;
|
||||||
arg
|
arg
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_in_out(name: String, state_variable: StateVariable) -> Self {
|
pub fn new_in_out(name: String, state_variable: Arc<StateVariable>) -> Self {
|
||||||
let mut arg = Self::new(name, state_variable);
|
let mut arg = Self::new(name, state_variable);
|
||||||
arg.is_in = true;
|
arg.is_in = true;
|
||||||
arg.is_out = true;
|
arg.is_out = true;
|
||||||
@@ -17,3 +17,21 @@ impl From<std::io::Error> for ActionError {
|
|||||||
ActionError::GeneralError(format!("IO error: {}", err))
|
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<std::io::Error> for ArgumentError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
ArgumentError::GeneralError(format!("IO error: {}", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
/// # Syntaxe
|
||||||
///
|
///
|
||||||
|
/// ## Action avec arguments
|
||||||
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// define_action! {
|
/// define_action! {
|
||||||
/// pub static ACTION_NAME = "ActionName" {
|
/// pub static ACTION_NAME = "ActionName" {
|
||||||
/// in "ParamName" => VARIABLE_REF,
|
/// in "ParamName" => VARIABLE_REF,
|
||||||
/// in "OtherParam" => OTHER_VAR,
|
|
||||||
/// out "ResultParam" => RESULT_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<Arc<StateVariable>>`
|
||||||
|
///
|
||||||
|
/// # Type de retour
|
||||||
|
///
|
||||||
|
/// La macro génère une `Lazy<Arc<Action>>` 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<Arc<StateVariable>> = 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<Arc<StateVariable>> = Lazy::new(|| {
|
||||||
|
/// Arc::new(StateVariable::new(StateVarType::UI4, "InstanceID".to_string()))
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// pub static TRANSPORT_URI: Lazy<Arc<StateVariable>> = 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<Arc<Action>>
|
||||||
|
/// println!("Action: {}", play_action.get_name());
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Notes d'implémentation
|
||||||
|
///
|
||||||
|
/// - Les `Arc<StateVariable>` 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_export]
|
||||||
macro_rules! define_action {
|
macro_rules! define_action {
|
||||||
// Variante sans arguments
|
// Variante sans arguments
|
||||||
(pub static $name:ident = $action_name:literal) => {
|
(pub static $name:ident = $action_name:literal) => {
|
||||||
pub static $name: once_cell::sync::Lazy<$crate::actions::Action> =
|
pub static $name: once_cell::sync::Lazy<std::sync::Arc<$crate::actions::Action>> =
|
||||||
once_cell::sync::Lazy::new(|| {
|
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
|
$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<std::sync::Arc<$crate::actions::Action>> =
|
||||||
once_cell::sync::Lazy::new(|| {
|
once_cell::sync::Lazy::new(|| {
|
||||||
let mut ac = $crate::actions::Action::new($action_name.to_string());
|
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) => {
|
(@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) => {
|
(@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 =============
|
/// Macro pour définir plusieurs actions UPnP en une seule déclaration.
|
||||||
|
///
|
||||||
#[cfg(test)]
|
/// Cette macro permet de regrouper la définition de plusieurs actions pour
|
||||||
mod examples {
|
/// améliorer la lisibilité et réduire la répétition de code.
|
||||||
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<Action> = 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
|
|
||||||
///
|
///
|
||||||
/// # Syntaxe
|
/// # Syntaxe
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// define_actions! {
|
/// 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<Arc<Action>>` pour chaque action définie.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// use once_cell::sync::Lazy;
|
||||||
|
/// use std::sync::Arc;
|
||||||
|
///
|
||||||
|
/// // Variables d'état
|
||||||
|
/// pub static INSTANCE_ID: Lazy<Arc<StateVariable>> = Lazy::new(|| {
|
||||||
|
/// Arc::new(StateVariable::new(StateVarType::UI4, "InstanceID".to_string()))
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// pub static TRANSPORT_URI: Lazy<Arc<StateVariable>> = Lazy::new(|| {
|
||||||
|
/// Arc::new(StateVariable::new(StateVarType::String, "TransportURI".to_string()))
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// pub static URI_METADATA: Lazy<Arc<StateVariable>> = Lazy::new(|| {
|
||||||
|
/// Arc::new(StateVariable::new(StateVarType::String, "URIMetaData".to_string()))
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// // Définir plusieurs actions ensemble
|
||||||
|
/// define_actions! {
|
||||||
/// PLAY = "Play" {
|
/// PLAY = "Play" {
|
||||||
/// in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
|
/// in "InstanceID" => INSTANCE_ID,
|
||||||
/// in "Speed" => TRANSPORTPLAYSPEED,
|
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// STOP = "Stop" {
|
/// 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_export]
|
||||||
macro_rules! define_actions {
|
macro_rules! define_actions {
|
||||||
|
// Variante avec arguments pour chaque action
|
||||||
(
|
(
|
||||||
$(
|
$(
|
||||||
$name:ident = $action_name:literal {
|
$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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,15 +4,18 @@ mod action_methods;
|
|||||||
mod action_instance;
|
mod action_instance;
|
||||||
mod action_set_methods;
|
mod action_set_methods;
|
||||||
mod action_instance_set;
|
mod action_instance_set;
|
||||||
mod argument;
|
mod argument_methods;
|
||||||
mod arg_set_methods;
|
mod arg_set_methods;
|
||||||
|
mod arg_inst_set_methods;
|
||||||
|
mod arg_instance_methods;
|
||||||
|
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::sync::Arc;
|
||||||
use crate::{state_variables::StateVariable, UpnpObjectType};
|
use crate::{state_variables::{StateVarInstance, StateVariable}, UpnpObjectSet, UpnpObjectType};
|
||||||
|
|
||||||
|
pub use errors::ActionError;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Action {
|
pub struct Action {
|
||||||
@@ -20,10 +23,7 @@ pub struct Action {
|
|||||||
arguments: ArgumentSet,
|
arguments: ArgumentSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
pub type ActionSet = UpnpObjectSet<Action>;
|
||||||
pub struct ActionSet {
|
|
||||||
actions: HashMap<String, Action>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ActionInstance {
|
pub struct ActionInstance {
|
||||||
@@ -31,20 +31,24 @@ pub struct ActionInstance {
|
|||||||
model: Action,
|
model: Action,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
pub type ActionInstanceSet = UpnpObjectSet<ActionInstance>;
|
||||||
pub struct ActionInstanceSet {
|
|
||||||
instances: HashMap<String, ActionInstance>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Argument {
|
pub struct Argument {
|
||||||
object: UpnpObjectType,
|
object: UpnpObjectType,
|
||||||
state_variable: StateVariable,
|
state_variable: Arc<StateVariable>,
|
||||||
is_in: bool,
|
is_in: bool,
|
||||||
is_out: bool,
|
is_out: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
pub type ArgumentSet = UpnpObjectSet<Argument>;
|
||||||
pub struct ArgumentSet {
|
|
||||||
arguments: HashMap<String, Argument>,
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ArgumentInstance {
|
||||||
|
object: UpnpObjectType,
|
||||||
|
model: Argument,
|
||||||
|
variable_instance: Option<Arc<StateVarInstance>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type ArgInstanceSet = UpnpObjectSet<ArgumentInstance>;
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
mod object_trait;
|
mod object_trait;
|
||||||
|
mod object_set;
|
||||||
|
|
||||||
|
pub mod actions;
|
||||||
|
pub mod mediarenderer;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
// pub mod services;
|
||||||
pub mod state_variables;
|
pub mod state_variables;
|
||||||
pub mod value_ranges;
|
pub mod value_ranges;
|
||||||
pub mod variable_types;
|
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 {
|
pub struct UpnpObjectType {
|
||||||
name: String,
|
name: String,
|
||||||
object_type: String,
|
object_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UpnpObjectSet<T: UpnpTypedObject> {
|
||||||
|
objects: RwLock<HashMap<String, Arc<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum UpnpObjectSetError {
|
||||||
|
AlreadyExists(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTPLAYSPEED};
|
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTPLAYSPEED};
|
||||||
use crate::actions::{Action, Argument};
|
use crate::define_action;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
pub static PLAY: Lazy<Action> = Lazy::new(|| -> Action {
|
define_action! {
|
||||||
let mut ac = Action::new("Play".to_string());
|
pub static PLAY = "Play" {
|
||||||
|
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
|
||||||
ac.add_argument(
|
in "Speed" => TRANSPORTPLAYSPEED,
|
||||||
Argument::new_in("InstanceID".to_string(),
|
}
|
||||||
A_ARG_TYPE_INSTANCE_ID.clone())
|
}
|
||||||
);
|
|
||||||
|
|
||||||
ac.add_argument(
|
|
||||||
Argument::new_in("Speed".to_string(),
|
|
||||||
TRANSPORTPLAYSPEED.clone())
|
|
||||||
);
|
|
||||||
|
|
||||||
ac
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::state_variables::StateVariable;
|
use crate::state_variables::StateVariable;
|
||||||
use crate::variable_types::StateVarType;
|
use crate::variable_types::StateVarType;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
pub static A_ARG_TYPE_INSTANCE_ID: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
|
pub static A_ARG_TYPE_INSTANCE_ID: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||||||
StateVariable::new(StateVarType::UI4, "A_ARG_TYPE_InstanceID".to_string())
|
Arc::new(StateVariable::new(StateVarType::UI4, "A_ARG_TYPE_InstanceID".to_string()))
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::state_variables::StateVariable;
|
use crate::state_variables::StateVariable;
|
||||||
use crate::variable_types::StateVarType;
|
use crate::variable_types::StateVarType;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
pub static A_ARG_TYPE_PLAY_SPEED: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
|
pub static A_ARG_TYPE_PLAY_SPEED: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||||||
StateVariable::new(StateVarType::String, "A_ARG_TYPE_PlaySpeed".to_string())
|
Arc::new(StateVariable::new(StateVarType::String, "A_ARG_TYPE_PlaySpeed".to_string()))
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::state_variables::StateVariable;
|
use crate::state_variables::StateVariable;
|
||||||
use crate::variable_types::StateVarType;
|
use crate::variable_types::StateVarType;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
pub static AVTRANSPORTURI: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
|
pub static AVTRANSPORTURI: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||||||
StateVariable::new(StateVarType::String, "AVTransportURI".to_string())
|
Arc::new(StateVariable::new(StateVarType::String, "AVTransportURI".to_string()))
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ fn avtransporturimetadataparser(value: &str) -> Result<Box<dyn Reflect>, StateVa
|
|||||||
Ok(Box::new(didl) as Box<dyn Reflect>)
|
Ok(Box::new(didl) as Box<dyn Reflect>)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static AVTRANSPORTURIMETADATA: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
|
pub static AVTRANSPORTURIMETADATA: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||||||
let mut sv = StateVariable::new(StateVarType::String, "AVTransportURIMetaData".to_string());
|
let mut sv = StateVariable::new(StateVarType::String, "AVTransportURIMetaData".to_string());
|
||||||
|
|
||||||
sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser");
|
sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser");
|
||||||
sv
|
Arc::new(sv)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::state_variables::StateVariable;
|
use crate::state_variables::StateVariable;
|
||||||
use crate::variable_types::StateVarType;
|
use crate::variable_types::StateVarType;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
pub static CURRENTTRACKDURATION: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
|
pub static CURRENTTRACKDURATION: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||||||
StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string())
|
Arc::new(StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string()))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::state_variables::StateVariable;
|
use crate::state_variables::StateVariable;
|
||||||
use crate::variable_types::StateVarType;
|
use crate::variable_types::StateVarType;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
pub static SEEKMODE: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
|
pub static SEEKMODE: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||||||
StateVariable::new(StateVarType::String, "SeekMode".to_string())
|
Arc::new(StateVariable::new(StateVarType::String, "SeekMode".to_string()))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::state_variables::StateVariable;
|
use crate::state_variables::StateVariable;
|
||||||
use crate::variable_types::{StateValue, StateVarType};
|
use crate::variable_types::{StateValue, StateVarType};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
pub static TRANSPORTPLAYSPEED: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
|
pub static TRANSPORTPLAYSPEED: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||||||
let mut sv = StateVariable::new(StateVarType::String, "TransportPlaySpeed".to_string());
|
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.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.set_default(&StateValue::String("1".to_string())).expect("Cannt set default value");
|
||||||
|
|
||||||
sv
|
Arc::new(sv)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::state_variables::StateVariable;
|
use crate::state_variables::StateVariable;
|
||||||
use crate::variable_types::{StateValue, StateVarType};
|
use crate::variable_types::{StateValue, StateVarType};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
pub static TRANSPORTSTATE: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
|
pub static TRANSPORTSTATE: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||||||
let mut sv = StateVariable::new(StateVarType::String, "TransportState".to_string());
|
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");
|
sv.push_allowed_value(&StateValue::String("NO_MEDIA_PRESENT".to_string())).expect("Cannot add allowed value");
|
||||||
@@ -16,6 +18,6 @@ pub static TRANSPORTSTATE: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
|
|||||||
StateValue::String("NO_MEDIA_PRESENT".to_string()),
|
StateValue::String("NO_MEDIA_PRESENT".to_string()),
|
||||||
]).expect("Cannt set default value");
|
]).expect("Cannt set default value");
|
||||||
|
|
||||||
sv
|
Arc::new(sv)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::state_variables::StateVariable;
|
use crate::state_variables::StateVariable;
|
||||||
use crate::variable_types::{StateValue, StateVarType};
|
use crate::variable_types::{StateValue, StateVarType};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
pub static TRANSPORTSTATUS: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
|
pub static TRANSPORTSTATUS: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||||||
let mut sv = StateVariable::new(StateVarType::String, "TransportStatus".to_string());
|
let mut sv = StateVariable::new(StateVarType::String, "TransportStatus".to_string());
|
||||||
|
|
||||||
sv.push_allowed_value(&StateValue::String("OK".to_string()))
|
sv.push_allowed_value(&StateValue::String("OK".to_string()))
|
||||||
@@ -13,5 +15,5 @@ pub static TRANSPORTSTATUS: Lazy<StateVariable> = Lazy::new(|| -> StateVariable
|
|||||||
])
|
])
|
||||||
.expect("Cannt set default value");
|
.expect("Cannt set default value");
|
||||||
|
|
||||||
sv
|
Arc::new(sv)
|
||||||
});
|
});
|
||||||
|
|||||||
218
pmoupnp/src/object_set.rs
Normal file
218
pmoupnp/src/object_set.rs
Normal file
@@ -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<T: UpnpTypedObject> UpnpDeepClone for UpnpObjectSet<T> {
|
||||||
|
fn deep_clone(&self) -> Self {
|
||||||
|
let guard = self.objects.blocking_read();
|
||||||
|
|
||||||
|
let cloned_map: HashMap<String, Arc<T>> = 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<T: UpnpTypedObject> Clone for UpnpObjectSet<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
let guard = self.objects.blocking_read();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
objects: RwLock::new(guard.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: UpnpTypedObject> UpnpObjectSet<T> {
|
||||||
|
|
||||||
|
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<T>) -> 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<T>) {
|
||||||
|
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<T>) -> 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<T>)` - 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<Arc<T>> {
|
||||||
|
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<Arc<T>> {
|
||||||
|
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<Arc<T>> {
|
||||||
|
let guard = self.objects.blocking_read();
|
||||||
|
guard.values().cloned().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 xmltree::{Element, EmitterConfig};
|
||||||
|
|
||||||
use crate::UpnpObjectType;
|
use crate::UpnpObjectType;
|
||||||
|
|
||||||
pub trait UpnpXml {
|
/// Trait pour le clonage profond d'objets UPnP.
|
||||||
fn to_xml_element(&self) -> Element;
|
///
|
||||||
fn to_xml(&self) -> String {
|
/// Contrairement au trait standard [`Clone`] qui peut effectuer un clonage superficiel
|
||||||
let elem = self.to_xml_element();
|
/// (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 `<?xml version="1.0" encoding="UTF-8"?>`
|
||||||
|
/// - Indentation de 2 espaces
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let xml = my_object.to_xml();
|
||||||
|
/// println!("{}", xml);
|
||||||
|
/// // <?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
/// // <element>
|
||||||
|
/// // <child>value</child>
|
||||||
|
/// // </element>
|
||||||
|
/// ```
|
||||||
|
async fn to_xml(&self) -> String {
|
||||||
|
let elem = self.to_xml_element().await;
|
||||||
|
|
||||||
// Configurer l'indentation
|
|
||||||
let config = EmitterConfig::new()
|
let config = EmitterConfig::new()
|
||||||
.perform_indent(true)
|
.perform_indent(true)
|
||||||
.indent_string(" "); // 2 espaces
|
.indent_string(" ");
|
||||||
|
|
||||||
// Sérialiser dans un buffer
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
// écrire l'élément
|
|
||||||
elem.write_with_config(&mut buf, config)
|
elem.write_with_config(&mut buf, config)
|
||||||
.expect("Failed to write XML");
|
.expect("Failed to write XML");
|
||||||
|
|
||||||
// Préfixer avec l'en-tête XML
|
|
||||||
let mut xml_string = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".to_string();
|
let mut xml_string = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".to_string();
|
||||||
xml_string.push_str(&String::from_utf8(buf).expect("Invalid UTF-8"));
|
xml_string.push_str(&String::from_utf8(buf).expect("Invalid UTF-8"));
|
||||||
|
|
||||||
xml_string
|
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();
|
let mut md = String::new();
|
||||||
|
|
||||||
fn is_url(s: &str) -> bool {
|
fn is_url(s: &str) -> bool {
|
||||||
@@ -43,7 +163,7 @@ pub trait UpnpXml {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn format_value(v: &str) -> String {
|
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_url(&v) {
|
||||||
if is_image_url(&v) {
|
if is_image_url(&v) {
|
||||||
format!("[{}]({})<br>", v, v, v)
|
format!("[{}]({})<br>", v, v, v)
|
||||||
@@ -59,7 +179,6 @@ pub trait UpnpXml {
|
|||||||
let indent = " ".repeat(depth);
|
let indent = " ".repeat(depth);
|
||||||
md.push_str(&format!("{}- **{}**", indent, elem.name));
|
md.push_str(&format!("{}- **{}**", indent, elem.name));
|
||||||
|
|
||||||
// Attributs
|
|
||||||
if !elem.attributes.is_empty() {
|
if !elem.attributes.is_empty() {
|
||||||
let attrs: Vec<String> = elem
|
let attrs: Vec<String> = elem
|
||||||
.attributes
|
.attributes
|
||||||
@@ -69,14 +188,16 @@ pub trait UpnpXml {
|
|||||||
md.push_str(&format!(" ({})", attrs.join(", ")));
|
md.push_str(&format!(" ({})", attrs.join(", ")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Texte (utilise get_text() maintenant)
|
if let Some(text) = elem
|
||||||
if let Some(text) = elem.get_text().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) {
|
.get_text()
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
{
|
||||||
md.push_str(&format!(": {}", format_value(&text)));
|
md.push_str(&format!(": {}", format_value(&text)));
|
||||||
}
|
}
|
||||||
|
|
||||||
md.push('\n');
|
md.push('\n');
|
||||||
|
|
||||||
// Enfants
|
|
||||||
for child in &elem.children {
|
for child in &elem.children {
|
||||||
if let xmltree::XMLNode::Element(child_elem) = child {
|
if let xmltree::XMLNode::Element(child_elem) = child {
|
||||||
recurse(child_elem, md, depth + 1);
|
recurse(child_elem, md, depth + 1);
|
||||||
@@ -88,15 +209,558 @@ pub trait UpnpXml {
|
|||||||
recurse(&elem, &mut md, 0);
|
recurse(&elem, &mut md, 0);
|
||||||
md
|
md
|
||||||
}
|
}
|
||||||
}
|
|
||||||
pub trait UpnpObject: UpnpXml {
|
|
||||||
fn as_upnp_object_type(&self) -> &UpnpObjectType;
|
|
||||||
|
|
||||||
fn get_name(&self) -> &String {
|
/// Indique si l'objet est une instance.
|
||||||
return &self.as_upnp_object_type().name;
|
///
|
||||||
|
/// # 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<DeviceInstance>
|
||||||
|
/// ```
|
||||||
|
pub trait UpnpModel: UpnpObject {
|
||||||
|
/// Le type d'instance créée par ce modèle.
|
||||||
|
type Instance: UpnpInstance<Model = Self>;
|
||||||
|
|
||||||
|
/// 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<Self::Instance> {
|
||||||
|
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<Instance = Self>;
|
||||||
|
|
||||||
|
/// 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 {
|
fn get_object_type(&self) -> &String {
|
||||||
&self.as_upnp_object_type().object_type
|
&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<Instance = Self>
|
||||||
|
{
|
||||||
|
/// 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<T>` 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<String, Arc<Service>>,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// 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<Arc<ServiceModel>>,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// /// Collection d'instances de services
|
||||||
|
/// struct ServiceSetInstance {
|
||||||
|
/// model: Arc<ServiceSetModel>,
|
||||||
|
/// service_instances: Vec<Arc<ServiceInstance>>,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl UpnpObject for ServiceSetModel { /* ... */ }
|
||||||
|
/// impl UpnpSet for ServiceSetModel {}
|
||||||
|
///
|
||||||
|
/// impl UpnpModel for ServiceSetModel {
|
||||||
|
/// type Instance = ServiceSetInstance;
|
||||||
|
///
|
||||||
|
/// fn create_instance(&self) -> Arc<ServiceSetInstance> {
|
||||||
|
/// // 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<ServiceSetModel>,
|
||||||
|
/// services: Vec<Arc<ServiceInstance>>,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// 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<T: UpnInstanceSet>(set: &T) {
|
||||||
|
/// if set.is_set() && set.is_instance() {
|
||||||
|
/// println!("C'est une collection ET une instance");
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
impl<T> 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<T: UpnpTypedObject>(obj: &T) {
|
||||||
|
/// println!("{}", obj.get_name());
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
impl<T> 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<Arc<ActionModel>>,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl UpnpObject for ActionSetModel { /* ... */ }
|
||||||
|
/// impl UpnpSet for ActionSetModel {}
|
||||||
|
///
|
||||||
|
/// impl UpnpModel for ActionSetModel {
|
||||||
|
/// type Instance = ActionSetInstance;
|
||||||
|
/// fn create_instance(&self) -> Arc<ActionSetInstance> { /* ... */ }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// 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<T: UpnpModelSet>(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<T> UpnpModelSet for T
|
||||||
|
where
|
||||||
|
T: UpnpSet + UpnpModel
|
||||||
|
{}
|
||||||
|
|
||||||
|
|||||||
19
pmoupnp/src/services/errors.rs
Normal file
19
pmoupnp/src/services/errors.rs
Normal file
@@ -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<std::io::Error> for ServiceError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
ServiceError::GeneralError(format!("IO error: {}", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
607
pmoupnp/src/services/mod.rs
Normal file
607
pmoupnp/src/services/mod.rs
Normal file
@@ -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<StateVariable>) {
|
||||||
|
self.state_table.insert(sv);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains_variable(&self, sv: Arc<StateVariable>) -> bool {
|
||||||
|
self.state_table.contains(sv).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn variables(&self) -> impl Iterator<Item = &StateVariable> {
|
||||||
|
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<Arc<DeviceInstance>>,
|
||||||
|
statevariables: StateVarInstanceSet,
|
||||||
|
actions: ActionInstanceSet,
|
||||||
|
subscribers: Arc<RwLock<HashMap<String, String>>>, // SID → Callback URL
|
||||||
|
changed_buffer: Arc<Mutex<HashMap<String, String>>>, // Simplifié pour l'exemple
|
||||||
|
seqid: Arc<Mutex<HashMap<String, u32>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"#.to_string();
|
||||||
|
for (name, val) in changed {
|
||||||
|
body.push_str(&format!("<e:property><{0}>{1}</{0}></e:property>", name, val));
|
||||||
|
}
|
||||||
|
body.push_str("</e:propertyset>");
|
||||||
|
|
||||||
|
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#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"#.to_string();
|
||||||
|
for (name, val) in changed_clone {
|
||||||
|
body.push_str(&format!("<e:property><{0}>{1}</{0}></e:property>", name, val));
|
||||||
|
}
|
||||||
|
body.push_str("</e:propertyset>");
|
||||||
|
|
||||||
|
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<ServiceInstance>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> 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<ServiceInstance>,
|
||||||
|
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#"<?xml version="1.0"?>
|
||||||
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
|
||||||
|
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||||
|
<s:Body>
|
||||||
|
<u:Response xmlns:u="{}">
|
||||||
|
</u:Response>
|
||||||
|
</s:Body>
|
||||||
|
</s:Envelope>"#,
|
||||||
|
instance.service_type()
|
||||||
|
);
|
||||||
|
|
||||||
|
(
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,59 +1,116 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use xmltree::Element;
|
use xmltree::Element;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
UpnpObject, UpnpObjectType,
|
object_trait::{UpnpInstance, UpnpObject}, state_variables::{StateVarInstance, StateVariable, UpnpVariable}, variable_types::{StateValue, StateValueError, UpnpVarType}, UpnpObjectType, UpnpTyped, UpnpTypedInstance, UpnpTypedObject
|
||||||
object_trait::UpnpXml,
|
|
||||||
state_variables::{StateVarInstance, StateVariable, UpnpVariable},
|
|
||||||
variable_types::StateValue,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl UpnpVariable for StateVarInstance {
|
impl UpnpVariable for StateVarInstance {
|
||||||
fn get_definition(&self) -> &StateVariable {
|
fn get_definition(&self) -> &StateVariable {
|
||||||
return &self.definition;
|
return &self.model;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UpnpXml for StateVarInstance {
|
|
||||||
fn to_xml_element(&self) -> Element {
|
|
||||||
self.get_definition().to_xml_element()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpnpObject for StateVarInstance {
|
impl UpnpObject for StateVarInstance {
|
||||||
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
async fn to_xml_element(&self) -> Element {
|
||||||
return &self.object;
|
self.get_definition().to_xml_element().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StateVarInstance {
|
impl UpnpVarType for StateVarInstance {
|
||||||
pub fn new(from: &StateVariable) -> Self {
|
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 {
|
Self {
|
||||||
object: UpnpObjectType {
|
object: UpnpObjectType {
|
||||||
name: from.object.name.clone(),
|
name: from.object.name.clone(),
|
||||||
object_type: "StateVarInstance".to_string(),
|
object_type: "StateVarInstance".to_string(),
|
||||||
},
|
},
|
||||||
definition: from.clone(),
|
model: from.clone(),
|
||||||
value: from.get_default(),
|
value: RwLock::new(from.get_default()),
|
||||||
old_value: from.get_default(),
|
old_value: RwLock::new(from.get_default()),
|
||||||
last_modified: Utc::now(),
|
last_modified: RwLock::new(Utc::now()),
|
||||||
last_notification: 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
|
/// Accès à la valeur
|
||||||
pub fn value(&self) -> &StateValue {
|
pub fn value(&self) -> StateValue {
|
||||||
&self.value
|
self.value.blocking_read().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Accès au timestamp
|
/// Accès au timestamp
|
||||||
pub fn last_modified(&self) -> DateTime<Utc> {
|
pub fn last_modified(&self) -> DateTime<Utc> {
|
||||||
self.last_modified
|
self.last_modified.blocking_read().clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,22 @@ mod errors;
|
|||||||
mod instance_methods;
|
mod instance_methods;
|
||||||
mod variable_methods;
|
mod variable_methods;
|
||||||
mod var_set_methods;
|
mod var_set_methods;
|
||||||
|
mod var_inst_set_methods;
|
||||||
mod variable_trait;
|
mod variable_trait;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::{Arc, RwLock},
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::state_variables::variable_trait::UpnpVariable;
|
pub use crate::state_variables::variable_trait::UpnpVariable;
|
||||||
use bevy_reflect::Reflect;
|
use bevy_reflect::Reflect;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
pub use errors::StateVariableError;
|
pub use errors::StateVariableError;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
UpnpObjectType,
|
value_ranges::ValueRange, variable_types::{StateValue, StateVarType}, UpnpObjectSet, UpnpObjectType, UpnpSet
|
||||||
value_ranges::ValueRange,
|
|
||||||
variable_types::{StateValue, StateVarType},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Type pour les fonctions de condition d'événement
|
/// Type pour les fonctions de condition d'événement
|
||||||
@@ -46,16 +46,16 @@ pub struct StateVariable {
|
|||||||
marshal: Option<ValueSerializer>,
|
marshal: Option<ValueSerializer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
pub type StateVariableSet = UpnpObjectSet<StateVariable>;
|
||||||
pub struct StateVariableSet {
|
|
||||||
instances: HashMap<String, StateVariable>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct StateVarInstance {
|
pub struct StateVarInstance {
|
||||||
object: UpnpObjectType,
|
object: UpnpObjectType,
|
||||||
definition: StateVariable,
|
model: StateVariable,
|
||||||
value: StateValue,
|
value: RwLock<StateValue>,
|
||||||
old_value: StateValue,
|
old_value: RwLock<StateValue>,
|
||||||
last_modified: DateTime<Utc>,
|
last_modified: RwLock<DateTime<Utc>>,
|
||||||
last_notification: DateTime<Utc>,
|
last_notification: RwLock<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type StateVarInstanceSet = UpnpObjectSet<StateVarInstance>;
|
||||||
|
|
||||||
|
|||||||
33
pmoupnp/src/state_variables/var_inst_set_methods.rs
Normal file
33
pmoupnp/src/state_variables/var_inst_set_methods.rs
Normal file
@@ -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 <stateVariable> 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()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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 {
|
impl UpnpObject for StateVariableSet {
|
||||||
Self {
|
|
||||||
instances: HashMap::new(),
|
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 <stateVariable> complet
|
||||||
|
elem.children.push(XMLNode::Element(state_var_elem));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert(&mut self, instance: StateVariable) {
|
elem
|
||||||
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<&StateVariable> {
|
|
||||||
self.instances.get(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &StateVariable> {
|
|
||||||
self.instances.values()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn all(&self) -> Vec<&StateVariable> {
|
|
||||||
self.instances.values().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl UpnpModel for StateVariableSet {
|
||||||
|
type Instance = StateVarInstanceSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,37 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap, fmt, sync::{Arc, RwLock}
|
collections::HashMap,
|
||||||
|
fmt,
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use xmltree::{Element, XMLNode};
|
use xmltree::{Element, XMLNode};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
object_trait::UpnpXml, state_variables::{
|
UpnpObjectType, UpnpTyped,
|
||||||
variable_trait::UpnpVariable, StateConditionFunc, StateVariable, StringValueParser, ValueSerializer
|
object_trait::{UpnpModel, UpnpObject},
|
||||||
}, value_ranges::ValueRange, variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType}, UpnpObject, UpnpObjectType
|
state_variables::{
|
||||||
|
StateConditionFunc, StateVarInstance, StateVariable, StringValueParser, ValueSerializer,
|
||||||
|
variable_trait::UpnpVariable,
|
||||||
|
},
|
||||||
|
value_ranges::ValueRange,
|
||||||
|
variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType},
|
||||||
};
|
};
|
||||||
|
|
||||||
impl UpnpXml for StateVariable {
|
impl UpnpTyped for StateVariable {
|
||||||
fn to_xml_element(&self) -> Element {
|
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 <stateVariable>
|
// Création de l'élément racine <stateVariable>
|
||||||
let mut root = Element::new("stateVariable");
|
let mut root = Element::new("stateVariable");
|
||||||
root.attributes.insert(
|
root.attributes.insert(
|
||||||
@@ -39,16 +59,15 @@ impl UpnpXml for StateVariable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// <allowedValueList> si défini
|
// <allowedValueList> si défini
|
||||||
if let Ok(av) = self.allowed_values.read() {
|
let av = self.allowed_values.read().await;
|
||||||
if !av.is_empty() {
|
if !av.is_empty() {
|
||||||
let mut list_elem = Element::new("allowedValueList");
|
let mut list_elem = Element::new("allowedValueList");
|
||||||
for val in av.iter() {
|
for val in av.iter() {
|
||||||
let mut val_elem = Element::new("allowedValue");
|
let mut val_elem = Element::new("allowedValue");
|
||||||
val_elem.children.push(XMLNode::Text(val.to_string()));
|
val_elem.children.push(XMLNode::Text(val.to_string()));
|
||||||
list_elem.children.push(XMLNode::Element(val_elem));
|
list_elem.children.push(XMLNode::Element(val_elem));
|
||||||
}
|
|
||||||
root.children.push(XMLNode::Element(list_elem));
|
|
||||||
}
|
}
|
||||||
|
root.children.push(XMLNode::Element(list_elem));
|
||||||
}
|
}
|
||||||
|
|
||||||
// <allowedValueRange> si défini
|
// <allowedValueRange> si défini
|
||||||
@@ -82,13 +101,10 @@ impl UpnpXml for StateVariable {
|
|||||||
|
|
||||||
root
|
root
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpnpObject for StateVariable {
|
impl UpnpModel for StateVariable {
|
||||||
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
type Instance = StateVarInstance;
|
||||||
return &self.object;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for StateVariable {
|
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
|
// si le lock est "poisoned" on panic - tu peux adapter la gestion si tu veux
|
||||||
let guard = self
|
let guard = self
|
||||||
.event_conditions
|
.event_conditions
|
||||||
.read()
|
.blocking_read();
|
||||||
.expect("RwLock poisoned during clone");
|
|
||||||
// nécessite que Key: Clone, Value: Clone
|
// nécessite que Key: Clone, Value: Clone
|
||||||
Arc::new(RwLock::new(guard.clone()))
|
Arc::new(RwLock::new(guard.clone()))
|
||||||
};
|
};
|
||||||
@@ -107,8 +122,7 @@ impl Clone for StateVariable {
|
|||||||
let allowed_values_clone = {
|
let allowed_values_clone = {
|
||||||
let guard = self
|
let guard = self
|
||||||
.allowed_values
|
.allowed_values
|
||||||
.read()
|
.blocking_read();
|
||||||
.expect("RwLock poisoned during clone");
|
|
||||||
Arc::new(RwLock::new(guard.clone()))
|
Arc::new(RwLock::new(guard.clone()))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,7 +156,7 @@ impl fmt::Debug for StateVariable {
|
|||||||
"event_conditions",
|
"event_conditions",
|
||||||
&format_args!(
|
&format_args!(
|
||||||
"len={}",
|
"len={}",
|
||||||
self.event_conditions.read().map(|m| m.len()).unwrap_or(0)
|
self.event_conditions.blocking_read().len()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.field("description", &self.description)
|
.field("description", &self.description)
|
||||||
@@ -152,22 +166,30 @@ impl fmt::Debug for StateVariable {
|
|||||||
"allowed_values",
|
"allowed_values",
|
||||||
&format_args!(
|
&format_args!(
|
||||||
"len={}",
|
"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("send_events", &self.send_events)
|
||||||
.field("parse", &self.parse.as_ref().map(|_| "Some(StringValueParser)").unwrap_or("None"))
|
.field(
|
||||||
.field("marshal", &self.marshal.as_ref().map(|_| "Some(ValueSerializer)").unwrap_or("None"))
|
"parse",
|
||||||
|
&self
|
||||||
|
.parse
|
||||||
|
.as_ref()
|
||||||
|
.map(|_| "Some(StringValueParser)")
|
||||||
|
.unwrap_or("None"),
|
||||||
|
)
|
||||||
|
.field(
|
||||||
|
"marshal",
|
||||||
|
&self
|
||||||
|
.marshal
|
||||||
|
.as_ref()
|
||||||
|
.map(|_| "Some(ValueSerializer)")
|
||||||
|
.unwrap_or("None"),
|
||||||
|
)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpnpVarType for StateVariable {
|
|
||||||
fn as_state_var_type(&self) -> StateVarType {
|
|
||||||
self.value_type // utilise ton From<&StateValue> existant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UpnpVariable for StateVariable {
|
impl UpnpVariable for StateVariable {
|
||||||
fn get_definition(&self) -> &StateVariable {
|
fn get_definition(&self) -> &StateVariable {
|
||||||
return self;
|
return self;
|
||||||
@@ -276,18 +298,18 @@ impl StateVariable {
|
|||||||
|
|
||||||
pub fn add_event_condition(&self, name: String, func: StateConditionFunc) {
|
pub fn add_event_condition(&self, name: String, func: StateConditionFunc) {
|
||||||
// on lock en écriture
|
// on lock en écriture
|
||||||
let mut guard = self.event_conditions.write().unwrap();
|
let mut guard = self.event_conditions.blocking_write();
|
||||||
guard.insert(name, func);
|
guard.insert(name, func);
|
||||||
// le lock est automatiquement relâché ici (RAII)
|
// le lock est automatiquement relâché ici (RAII)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_event_condition(&self, name: &str) {
|
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);
|
guard.remove(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_event_conditions(&mut self) {
|
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()
|
guard.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,8 +334,7 @@ impl StateVariable {
|
|||||||
pub fn extend_allowed_values(&mut self, values: &[StateValue]) -> Result<(), StateValueError> {
|
pub fn extend_allowed_values(&mut self, values: &[StateValue]) -> Result<(), StateValueError> {
|
||||||
let mut av = self
|
let mut av = self
|
||||||
.allowed_values
|
.allowed_values
|
||||||
.write()
|
.blocking_write();
|
||||||
.map_err(|_| StateValueError::TypeError("Lock poisoned".to_string()))?;
|
|
||||||
|
|
||||||
for v in values {
|
for v in values {
|
||||||
if self.as_state_var_type() == v.as_state_var_type() {
|
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> {
|
pub fn push_allowed_value(&mut self, value: &StateValue) -> Result<(), StateValueError> {
|
||||||
let mut av = self
|
let mut av = self
|
||||||
.allowed_values
|
.allowed_values
|
||||||
.write()
|
.blocking_write();
|
||||||
.map_err(|_| StateValueError::TypeError("Lock poisoned".to_string()))?;
|
|
||||||
|
|
||||||
if self.as_state_var_type() == value.as_state_var_type() {
|
if self.as_state_var_type() == value.as_state_var_type() {
|
||||||
av.push(value.clone());
|
av.push(value.clone());
|
||||||
|
|||||||
@@ -3,46 +3,199 @@ use crate::{
|
|||||||
variable_types::{StateValue, UpnpVarType},
|
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<V: UpnpVariable>(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 {
|
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;
|
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 {
|
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<StateValue> {
|
fn get_step(&self) -> Option<StateValue> {
|
||||||
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 {
|
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 {
|
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 {
|
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 {
|
fn has_event_condition(&self, name: &String) -> bool {
|
||||||
let guard = self.get_definition().event_conditions.read().unwrap();
|
let guard = self.get_definition().event_conditions.blocking_read();
|
||||||
return guard.contains_key(name);
|
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 {
|
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 {
|
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 {
|
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 {
|
fn get_default(&self) -> StateValue {
|
||||||
self.get_definition()
|
self.get_definition()
|
||||||
.default_value
|
.default_value
|
||||||
@@ -50,23 +203,99 @@ pub trait UpnpVariable {
|
|||||||
.unwrap_or_else(|| self.get_definition().as_state_var_type().default_value())
|
.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 {
|
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 {
|
/// Vérifie si une valeur fait partie des valeurs autorisées.
|
||||||
let guard = self.get_definition().allowed_values.read().unwrap();
|
///
|
||||||
return guard.contains(value);
|
/// # 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 {
|
fn is_sending_notification(&self) -> bool {
|
||||||
self.get_definition().send_events
|
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 {
|
fn has_value_parser(&self) -> bool {
|
||||||
self.get_definition().parse.is_some()
|
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 {
|
fn has_value_marshaler(&self) -> bool {
|
||||||
self.get_definition().marshal.is_some()
|
self.get_definition().marshal.is_some()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ use uuid::Uuid;
|
|||||||
pub use errors::StateValueError;
|
pub use errors::StateValueError;
|
||||||
pub use type_trait::UpnpVarType;
|
pub use type_trait::UpnpVarType;
|
||||||
|
|
||||||
|
pub use crate::variable_types::value_trait::UpnpValue;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum StateVarType {
|
pub enum StateVarType {
|
||||||
UI1, // Unsigned 8-bit integer
|
UI1, // Unsigned 8-bit integer
|
||||||
@@ -68,7 +70,7 @@ pub enum StateVarType {
|
|||||||
URI, // Uniform Resource Identifier
|
URI, // Uniform Resource Identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum StateValue {
|
pub enum StateValue {
|
||||||
UI1(u8),
|
UI1(u8),
|
||||||
UI2(u16),
|
UI2(u16),
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
pub trait UpnpValue {}
|
pub trait UpnpValue: Clone {}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::variable_types::{StateValue, StateValueError};
|
use crate::variable_types::{StateValue, StateValueError};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,37 @@
|
|||||||
use get_if_addrs::get_if_addrs;
|
use get_if_addrs::get_if_addrs;
|
||||||
use std::net::UdpSocket;
|
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 {
|
pub fn guess_local_ip() -> String {
|
||||||
// On tente de deviner l'IP locale
|
|
||||||
match UdpSocket::bind("0.0.0.0:0") {
|
match UdpSocket::bind("0.0.0.0:0") {
|
||||||
Ok(socket) => {
|
Ok(socket) => {
|
||||||
if socket.connect("8.8.8.8:80").is_ok() {
|
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();
|
return local_addr.ip().to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Si erreur sur connect ou récupération de l'adresse
|
|
||||||
"127.0.0.1".to_string()
|
"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<String, Vec<String>> {
|
fn list_all_ips() -> std::collections::HashMap<String, Vec<String>> {
|
||||||
let mut result = std::collections::HashMap::new();
|
let mut result = std::collections::HashMap::new();
|
||||||
|
|
||||||
@@ -42,3 +101,163 @@ fn list_all_ips() -> std::collections::HashMap<String, Vec<String>> {
|
|||||||
|
|
||||||
result
|
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::<IpAddr>().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::<IpAddr>() {
|
||||||
|
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::<IpAddr>().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::<IpAddr>() {
|
||||||
|
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::<IpAddr>() {
|
||||||
|
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::<IpAddr>().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::<IpAddr>() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
mod ip_utils;
|
||||||
|
|
||||||
pub use ip_utils::guess_local_ip;
|
pub use ip_utils::guess_local_ip;
|
||||||
0
structure.svg
Normal file
0
structure.svg
Normal file
@@ -12,13 +12,12 @@ EOF
|
|||||||
echo ============== Debut des sources des packages ===============
|
echo ============== Debut des sources des packages ===============
|
||||||
|
|
||||||
for package in $*; do
|
for package in $*; do
|
||||||
pushd $package 2>&1 >/dev/null
|
find $package -type f \( -name '*.rs' -o -name '*.toml' \) -print0 |
|
||||||
for f in *.go; do
|
while IFS= read -r -d '' file; do
|
||||||
echo "------- $package/$f ------"
|
echo "------- $file ------"
|
||||||
cat $f
|
cat $file
|
||||||
echo "-----------------"
|
echo "-----------------"
|
||||||
done
|
done
|
||||||
popd 2>&1 >/dev/null
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo ============== Fin des sources des packages ===============
|
echo ============== Fin des sources des packages ===============
|
||||||
|
|||||||
Reference in New Issue
Block a user