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