# Debut des sources des crates ## fichier: `Cargo.toml` ```toml [workspace] resolver = "3" members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils", "pmodidl"] ``` ## fichier: `pmoconfig/Cargo.toml` ```toml [package] name = "pmoconfig" version = "0.1.0" edition = "2021" [dependencies] pmoutils ={ path = "../pmoutils" } serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.9.33" lazy_static = "1.4.0" dirs = "6.0.0" log = "0.4.20" anyhow = "1.0.75" uuid = { version = "1.18.1", features = ["v4"] } tracing = "0.1.41"``` ## fichier: `pmoconfig/src/lib.rs` ```rust use anyhow::{anyhow, Result}; use dirs::home_dir; use lazy_static::lazy_static; use pmoutils::guess_local_ip; use serde_yaml::{Mapping, Value}; use std::{ env, fs, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; use tracing::{info, warn}; use uuid::Uuid; // Configuration par défaut intégrée const DEFAULT_CONFIG: &str = include_str!("pmomusic.yaml"); lazy_static! { static ref CONFIG: Arc = Arc::new(Config::load_config("").expect("Failed to load PMOMusic configuration")); } const ENV_CONFIG_FILE: &str = "PMOMUSIC_CONFIG"; const ENV_PREFIX: &str = "PMOMUSIC_CONFIG__"; #[derive(Debug)] pub struct Config { path: String, data: Mutex, } // Implémentation manuelle de Clone impl Clone for Config { fn clone(&self) -> Self { let data = self.data.lock().unwrap().clone(); Self { path: self.path.clone(), data: Mutex::new(data), } } } impl Config { pub fn load_config(filename: &str) -> Result { let mut path = filename.to_string(); let mut data: Option> = None; // Essayer de charger depuis différents emplacements if !filename.is_empty() { info!(config_file=%path, "Trying to load config"); data = fs::read(&path).ok(); if data.is_none() { warn!(config_file=%path, "Cannot read config file"); path.clear(); } } if path.is_empty() { if let Ok(env_path) = env::var(ENV_CONFIG_FILE) { info!(env_var=ENV_CONFIG_FILE, path=%env_path, "Trying to load config from env"); path = env_path.clone(); data = fs::read(&path).ok(); if data.is_none() { warn!(config_file=%path, "Cannot read config file from env var"); path.clear(); } } } if path.is_empty() { let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); path = current_dir .join(".pmomusic.yml") .to_string_lossy() .to_string(); info!(config_file=%path, "Trying to load config file from current directory"); data = fs::read(&path).ok(); if data.is_none() { warn!(config_file=%path, "Cannot read config file in current dir"); path.clear(); } } if path.is_empty() { path = Self::get_home_yml_path(); info!(config_file=%path, "Trying to load config file from home directory"); data = fs::read(&path).ok(); if data.is_none() { warn!(config_file=%path, "Cannot read config file in home directory"); path.clear(); } } let yaml_data = if let Some(d) = data { d } else { info!("Using default embedded config"); DEFAULT_CONFIG.as_bytes().to_vec() }; let mut config_value: Value = serde_yaml::from_slice(&yaml_data)?; config_value = Self::lower_keys_value(config_value); Self::apply_env_overrides(&mut config_value); if path.is_empty() || !Self::is_writable(&path) { let candidates = [ filename.to_string(), env::var(ENV_CONFIG_FILE).unwrap_or_default(), ".pmomusic.yml".to_string(), Self::get_home_yml_path(), ]; for candidate in candidates.iter().filter(|c| !c.is_empty()) { if Self::is_writable(candidate) { path = candidate.clone(); break; } } } if path.is_empty() { return Err(anyhow!("Cannot find a place to store config file")); } info!(config_file=%path, "Config file will be stored here"); let config = Config { path, data: Mutex::new(config_value), }; config.save()?; Ok(config) } pub fn save(&self) -> Result<()> { let data = self.data.lock().unwrap(); let yaml = serde_yaml::to_string(&*data)?; fs::write(&self.path, yaml)?; Ok(()) } pub fn set_value(&self, path: &[&str], value: Value) -> Result<()> { let mut data = self.data.lock().unwrap(); Self::set_value_internal(&mut data, path, value.clone())?; drop(data); self.save()?; Ok(()) } fn set_value_internal(data: &mut Value, path: &[&str], value: Value) -> Result<()> { if path.is_empty() { *data = value; return Ok(()); } if let Value::Mapping(map) = data { let key = path[0].to_lowercase(); let key_value = Value::String(key.clone()); if path.len() == 1 { map.insert(key_value, value); } else { let entry = map .entry(key_value) .or_insert(Value::Mapping(Mapping::new())); Self::set_value_internal(entry, &path[1..], value)?; } Ok(()) } else { Err(anyhow!("Current node is not a map")) } } pub fn get_value(&self, path: &[&str]) -> Result { let data = self.data.lock().unwrap(); Self::get_value_internal(&data, path) } fn get_value_internal(data: &Value, path: &[&str]) -> Result { let mut current = data; for (i, key) in path.iter().enumerate() { if let Value::Mapping(map) = current { let key = key.to_lowercase(); if let Some(next) = map.get(&Value::String(key)) { current = next; } else { return Err(anyhow!("Path {} does not exist", path[..=i].join("."))); } } else { return Err(anyhow!("Path {} is not a Config", path[..i].join("."))); } } Ok(current.clone()) } fn get_home_yml_path() -> String { home_dir() .map(|p| p.join(".pmomusic.yml")) .unwrap_or_else(|| PathBuf::from(".")) .to_string_lossy() .to_string() } fn apply_env_overrides(config: &mut Value) { for (key, value) in env::vars() { if key.starts_with(ENV_PREFIX) { let key_path = key .trim_start_matches(ENV_PREFIX) .split("__") .collect::>(); let yaml_value = Self::convert_env_value(&value); let _ = Self::set_value_internal(config, &key_path, yaml_value); } } } fn convert_env_value(value: &str) -> Value { if let Ok(parsed) = serde_yaml::from_str::(value) { return parsed; } Value::String(value.to_string()) } fn lower_keys_value(value: Value) -> Value { match value { Value::Mapping(map) => { let mut new_map = Mapping::new(); for (k, v) in map { if let Value::String(s) = k { let new_key = Value::String(s.to_lowercase()); let new_val = Self::lower_keys_value(v); new_map.insert(new_key, new_val); } else { new_map.insert(k, Self::lower_keys_value(v)); } } Value::Mapping(new_map) } Value::Sequence(seq) => { Value::Sequence(seq.into_iter().map(Self::lower_keys_value).collect()) } _ => value, } } fn is_writable(path: &str) -> bool { let path = Path::new(path); if let Some(parent) = path.parent() { fs::metadata(parent) .map(|m| !m.permissions().readonly()) .unwrap_or(false) } else { false } } pub fn get_base_url(&self) -> String { match self.get_value(&["host", "base_url"]) { Ok(Value::String(s)) if !s.is_empty() => s, Ok(_) => { tracing::warn!("Base URL is not a string or empty, using default localhost"); guess_local_ip() } Err(err) => { tracing::warn!("Failed to get base URL: {}, using default localhost", err); guess_local_ip() } } } pub fn get_http_port(&self) -> u16 { match self.get_value(&["host", "http_port"]) { Ok(Value::Number(n)) if n.is_i64() => n.as_i64().unwrap() as u16, Ok(Value::String(s)) => match s.parse::() { Ok(port) => port, Err(_) => { tracing::warn!("Invalid HTTP port '{}', using default 8080", s); 8080 } }, Ok(_) => { tracing::warn!("HTTP port not a number or string, using default 8080"); 8080 } Err(err) => { tracing::warn!("Failed to get HTTP port: {}, using default 8080", err); 8080 } } } pub fn get_device_udn(&self, devtype: &str, name: &str) -> Result { let path = &["devices", devtype, name, "udn"]; match self.get_value(path) { Ok(Value::String(udn)) => Ok(udn), _ => { let new_udn = Uuid::new_v4().to_string(); self.set_value(path, Value::String(new_udn.clone()))?; Ok(new_udn) } } } pub fn get_cover_cache_dir(&self) -> Result { match self.get_value(&["host", "cover_cache", "directory"])? { Value::String(s) => Ok(s), _ => Ok("./.pmomusic_covers".to_string()), } } pub fn get_cover_cache_size(&self) -> Result { match self.get_value(&["host", "cover_cache", "size"])? { Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as usize), Value::Number(n) if n.is_u64() => Ok(n.as_u64().unwrap() as usize), _ => Ok(2000), } } } /// Retourne l'instance globale pub fn get_config() -> Arc { CONFIG.clone() } ``` ## fichier: `pmodidl/Cargo.toml` ```toml [package] name = "pmodidl" version = "0.1.0" edition = "2024" [dependencies] serde = "1.0.228" utoipa = { version = "5.4.0", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } quick-xml = { version = "0.38.3", features = ["serialize"] } bevy_reflect = "0.17.1" bevy_reflect_derive = "0.17.1" ``` ## fichier: `pmodidl/src/lib.rs` ```rust //! # pmodidl - DIDL-Lite Parser //! //! Parser et utilitaires pour le format DIDL-Lite utilisé dans UPnP/DLNA. use serde::{Deserialize, Serialize}; use std::fmt::Write; use bevy_reflect::Reflect; // ============= Couche d'abstraction générique ============= /// Trait pour tout parser de métadonnées média pub trait MediaMetadataParser: Sized { type Error: std::error::Error + Send + Sync + 'static; /// Parse une chaîne de métadonnées fn parse(input: &str) -> Result; /// Retourne le format du parser fn format_name() -> &'static str; } /// Enveloppe générique pour tout type de métadonnées parsées #[derive(Debug, Clone, Serialize, Deserialize, Reflect)] pub struct ParsedMetadata { /// Format du document (ex: "DIDL-Lite", "RSS", etc.) pub format: String, /// Données parsées pub data: T, /// Timestamp du parsing (exclu de la réflexion car SystemTime n'implémente pas Reflect) #[reflect(ignore)] #[serde(skip_serializing_if = "Option::is_none")] pub parsed_at: Option, } impl ParsedMetadata { pub fn new(format: impl Into, data: T) -> Self { Self { format: format.into(), data, parsed_at: Some(std::time::SystemTime::now()), } } /// Transforme les données avec une fonction pub fn map(self, f: F) -> ParsedMetadata where F: FnOnce(T) -> U, { ParsedMetadata { format: self.format, data: f(self.data), parsed_at: self.parsed_at, } } } /// Fonction helper pour parser et envelopper automatiquement pub fn parse_metadata(input: &str) -> Result, P::Error> { let data = P::parse(input)?; Ok(ParsedMetadata::new(P::format_name(), data)) } // ============= Implémentation pour DIDLLite ============= impl MediaMetadataParser for DIDLLite { type Error = quick_xml::de::DeError; fn parse(input: &str) -> Result { quick_xml::de::from_str(input) } fn format_name() -> &'static str { "DIDL-Lite" } } /// Type alias pour faciliter l'utilisation pub type DidlMetadata = ParsedMetadata; // ============= Structures DIDL-Lite ============= /// Racine d'un document DIDL-Lite #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] #[serde(rename = "DIDL-Lite")] pub struct DIDLLite { #[serde(rename = "@xmlns")] pub xmlns: String, #[serde(rename = "@xmlns:upnp", skip_serializing_if = "Option::is_none")] pub xmlns_upnp: Option, #[serde(rename = "@xmlns:dc", skip_serializing_if = "Option::is_none")] pub xmlns_dc: Option, #[serde(rename = "@xmlns:dlna", skip_serializing_if = "Option::is_none")] pub xmlns_dlna: Option, #[serde(rename = "@xmlns:sec", skip_serializing_if = "Option::is_none")] pub xmlns_sec: Option, #[serde(rename = "@xmlns:pv", skip_serializing_if = "Option::is_none")] pub xmlns_pv: Option, #[serde(rename = "container", default)] pub containers: Vec, #[serde(rename = "item", default)] pub items: Vec, } /// Container pouvant contenir d'autres containers ou items #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] pub struct Container { #[serde(rename = "@id")] pub id: String, #[serde(rename = "@parentID")] pub parent_id: String, #[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")] pub restricted: Option, #[serde(rename = "@childCount", skip_serializing_if = "Option::is_none")] pub child_count: Option, #[serde(rename = "dc:title", alias = "title")] pub title: String, #[serde(rename = "upnp:class", alias = "class")] pub class: String, #[serde(rename = "container", default)] pub containers: Vec, #[serde(rename = "item", default)] pub items: Vec, } /// Item représentant un objet audio #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] pub struct Item { #[serde(rename = "@id")] pub id: String, #[serde(rename = "@parentID")] pub parent_id: String, #[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")] pub restricted: Option, #[serde(rename = "dc:title", alias = "title")] pub title: String, #[serde(rename = "dc:creator", alias = "creator", skip_serializing_if = "Option::is_none")] pub creator: Option, #[serde(rename = "upnp:class", alias = "class")] pub class: String, #[serde(rename = "upnp:artist", alias = "artist", skip_serializing_if = "Option::is_none")] pub artist: Option, #[serde(rename = "upnp:album", alias = "album", skip_serializing_if = "Option::is_none")] pub album: Option, #[serde(rename = "upnp:genre", alias = "genre", skip_serializing_if = "Option::is_none")] pub genre: Option, #[serde(rename = "upnp:albumArtURI", alias = "albumArtURI", skip_serializing_if = "Option::is_none")] pub album_art: Option, #[serde(skip)] pub album_art_pk: Option, #[serde(rename = "dc:date", alias = "date", skip_serializing_if = "Option::is_none")] pub date: Option, #[serde(rename = "upnp:originalTrackNumber", alias = "originalTrackNumber", skip_serializing_if = "Option::is_none")] pub original_track_number: Option, #[serde(rename = "res", default)] pub resources: Vec, #[serde(rename = "desc", default)] pub descriptions: Vec, } /// Ressource média (fichier audio) #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] pub struct Resource { #[serde(rename = "@protocolInfo")] pub protocol_info: String, #[serde(rename = "@bitsPerSample", skip_serializing_if = "Option::is_none")] pub bits_per_sample: Option, #[serde(rename = "@sampleFrequency", skip_serializing_if = "Option::is_none")] pub sample_frequency: Option, #[serde(rename = "@nrAudioChannels", skip_serializing_if = "Option::is_none")] pub nr_audio_channels: Option, #[serde(rename = "@duration", skip_serializing_if = "Option::is_none")] pub duration: Option, #[serde(rename = "$text")] pub url: String, } /// Description avec métadonnées additionnelles (replaygain, etc.) #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] pub struct Description { #[serde(rename = "@id", skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(rename = "@nameSpace", skip_serializing_if = "Option::is_none")] pub namespace: Option, #[serde(rename = "track_gain", skip_serializing_if = "Option::is_none")] pub track_gain: Option, #[serde(rename = "track_peak", skip_serializing_if = "Option::is_none")] pub track_peak: Option, } // ============= Implémentation des méthodes ============= impl DIDLLite { /// Itère sur tous les containers de manière récursive pub fn all_containers(&self) -> impl Iterator { AllContainersIter::new(&self.containers) } /// Itère sur tous les items de manière récursive pub fn all_items(&self) -> impl Iterator { AllItemsIter::new(&self.containers, &self.items) } /// Trouve un container par ID pub fn get_container_by_id(&self, id: &str) -> Option<&Container> { self.all_containers().find(|c| c.id == id) } /// Trouve un item par ID pub fn get_item_by_id(&self, id: &str) -> Option<&Item> { self.all_items().find(|i| i.id == id) } /// Filtre les containers pub fn filter_containers(&self, predicate: F) -> impl Iterator where F: Fn(&Container) -> bool, { self.all_containers().filter(move |c| predicate(c)) } /// Filtre les items pub fn filter_items(&self, predicate: F) -> impl Iterator where F: Fn(&Item) -> bool, { self.all_items().filter(move |i| predicate(i)) } /// Génère une représentation Markdown pub fn to_markdown(&self) -> String { let mut buf = String::new(); buf.push_str("### DIDL-Lite Document\n\n"); if !self.containers.is_empty() { buf.push_str("#### Containers\n\n"); for container in &self.containers { container.write_markdown(&mut buf, 0); } } if !self.items.is_empty() { buf.push_str("#### Items\n\n"); for item in &self.items { item.write_markdown(&mut buf, 0); } } buf } } impl Container { /// Itère sur tous les containers enfants récursivement pub fn all_containers(&self) -> impl Iterator { AllContainersIter::new(&self.containers) } /// Itère sur tous les items de ce container et ses enfants pub fn all_items(&self) -> impl Iterator { AllItemsIter::new(&self.containers, &self.items) } fn write_markdown(&self, buf: &mut String, depth: usize) { let indent = " ".repeat(depth); writeln!(buf, "{}- **Container**: {}", indent, self.title).unwrap(); writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap(); writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap(); writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap(); if let Some(ref restricted) = self.restricted { writeln!(buf, "{} - Restricted: `{}`", indent, restricted).unwrap(); } if let Some(ref count) = self.child_count { writeln!(buf, "{} - ChildCount: `{}`", indent, count).unwrap(); } if !self.containers.is_empty() { writeln!(buf, "{} - Subcontainers:", indent).unwrap(); for sub in &self.containers { sub.write_markdown(buf, depth + 2); } } if !self.items.is_empty() { writeln!(buf, "{} - Items:", indent).unwrap(); for item in &self.items { item.write_markdown(buf, depth + 2); } } buf.push('\n'); } } impl Item { /// Itère sur les ressources audio uniquement pub fn audio_resources(&self) -> impl Iterator { self.resources.iter() .filter(|r| r.protocol_info.contains("audio/")) } /// Retourne la ressource principale (première disponible) pub fn primary_resource(&self) -> Option<&Resource> { self.resources.first() } /// Itère sur les métadonnées sous forme de paires clé-valeur pub fn metadata(&self) -> impl Iterator { let mut pairs = Vec::new(); pairs.push(("title", self.title.as_str())); if let Some(ref artist) = self.artist { pairs.push(("artist", artist.as_str())); } if let Some(ref album) = self.album { pairs.push(("album", album.as_str())); } if let Some(ref genre) = self.genre { pairs.push(("genre", genre.as_str())); } if let Some(ref date) = self.date { pairs.push(("date", date.as_str())); } if let Some(ref track) = self.original_track_number { pairs.push(("trackNumber", track.as_str())); } for desc in &self.descriptions { if let Some(ref gain) = desc.track_gain { pairs.push(("replayGain", gain.as_str())); } if let Some(ref peak) = desc.track_peak { pairs.push(("replayPeak", peak.as_str())); } } pairs.into_iter() } fn write_markdown(&self, buf: &mut String, depth: usize) { let indent = " ".repeat(depth); writeln!(buf, "{}- **Item**: {}", indent, self.title).unwrap(); writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap(); writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap(); writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap(); if let Some(ref creator) = self.creator { writeln!(buf, "{} - Creator: {}", indent, creator).unwrap(); } if let Some(ref artist) = self.artist { writeln!(buf, "{} - Artist: {}", indent, artist).unwrap(); } if let Some(ref album) = self.album { writeln!(buf, "{} - Album: {}", indent, album).unwrap(); } if let Some(ref genre) = self.genre { writeln!(buf, "{} - Genre: {}", indent, genre).unwrap(); } if let Some(ref art) = self.album_art { writeln!(buf, "{} - Album Art: ![Cover]({})", indent, art).unwrap(); } if let Some(ref date) = self.date { writeln!(buf, "{} - Date: {}", indent, date).unwrap(); } if let Some(ref track) = self.original_track_number { writeln!(buf, "{} - Track: {}", indent, track).unwrap(); } if !self.resources.is_empty() { writeln!(buf, "{} - Resources:", indent).unwrap(); for res in &self.resources { writeln!(buf, "{} - URL: {}", indent, res.url).unwrap(); writeln!(buf, "{} - Protocol: `{}`", indent, res.protocol_info).unwrap(); if let Some(ref dur) = res.duration { writeln!(buf, "{} - Duration: `{}`", indent, dur).unwrap(); } if let Some(ref bits) = res.bits_per_sample { writeln!(buf, "{} - BitsPerSample: `{}`", indent, bits).unwrap(); } if let Some(ref freq) = res.sample_frequency { writeln!(buf, "{} - SampleFrequency: `{}`", indent, freq).unwrap(); } if let Some(ref channels) = res.nr_audio_channels { writeln!(buf, "{} - Channels: `{}`", indent, channels).unwrap(); } } } if !self.descriptions.is_empty() { writeln!(buf, "{} - Descriptions:", indent).unwrap(); for desc in &self.descriptions { if let Some(ref ns) = desc.namespace { writeln!(buf, "{} - Namespace: `{}`", indent, ns).unwrap(); } if let Some(ref gain) = desc.track_gain { writeln!(buf, "{} - Track Gain: `{}`", indent, gain).unwrap(); } if let Some(ref peak) = desc.track_peak { writeln!(buf, "{} - Track Peak: `{}`", indent, peak).unwrap(); } } } buf.push('\n'); } } // ============= Itérateurs personnalisés ============= struct AllContainersIter<'a> { stack: Vec<&'a Container>, } impl<'a> AllContainersIter<'a> { fn new(containers: &'a [Container]) -> Self { Self { stack: containers.iter().collect(), } } } impl<'a> Iterator for AllContainersIter<'a> { type Item = &'a Container; fn next(&mut self) -> Option { self.stack.pop().map(|container| { // Ajouter les enfants à la pile self.stack.extend(container.containers.iter()); container }) } } struct AllItemsIter<'a> { containers: Vec<&'a Container>, current_items: std::slice::Iter<'a, Item>, } impl<'a> AllItemsIter<'a> { fn new(containers: &'a [Container], items: &'a [Item]) -> Self { Self { containers: containers.iter().collect(), current_items: items.iter(), } } } impl<'a> Iterator for AllItemsIter<'a> { type Item = &'a Item; fn next(&mut self) -> Option { loop { if let Some(item) = self.current_items.next() { return Some(item); } let container = self.containers.pop()?; self.containers.extend(container.containers.iter()); self.current_items = container.items.iter(); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_simple_didl() { let xml = r#" Test Song object.item.audioItem.musicTrack http://example.com/song.mp3 "#; let didl = DIDLLite::parse(xml).unwrap(); assert_eq!(didl.items.len(), 1); assert_eq!(didl.items[0].title, "Test Song"); } #[test] fn test_parse_without_namespaces() { // Teste un XML sans namespaces explicites (devices UPnP laxistes) let xml = r#" Test Song object.item.audioItem.musicTrack http://example.com/song.mp3 "#; let didl = DIDLLite::parse(xml).unwrap(); assert_eq!(didl.items.len(), 1); assert_eq!(didl.items[0].title, "Test Song"); } #[test] fn test_generic_parser() { let xml = r#" "#; // Utiliser le parser générique let metadata: DidlMetadata = parse_metadata(xml).unwrap(); assert_eq!(metadata.format, "DIDL-Lite"); assert!(metadata.parsed_at.is_some()); } #[test] fn test_metadata_map() { let xml = r#" "#; let metadata: DidlMetadata = parse_metadata(xml).unwrap(); // Transformer les données let item_count = metadata.map(|didl| didl.items.len()); assert_eq!(item_count.format, "DIDL-Lite"); assert_eq!(item_count.data, 0); } }``` ## fichier: `PMOMusic/Cargo.toml` ```toml [package] name = "PMOMusic" version = "0.1.0" edition = "2024" [dependencies] pmoconfig = { path = "../pmoconfig" } pmoupnp = { path = "../pmoupnp"} tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time","signal"] } tracing = "0.1.41" tracing-subscriber = "0.3.20" axum = "0.8.4" serde_json = "1.0.145" ``` ## fichier: `PMOMusic/src/main.rs` ```rust use pmoupnp::{mediarenderer::avtransport::actions::{SETAVTRANSPORTURI}, server::{ logs::{log_dump, log_sse, LogState, SseLayer}, ServerBuilder, Webapp }, UpnpObject}; // ton module pmoupnp::server use tracing_subscriber::Registry; use tracing_subscriber::prelude::*; use tracing::info; #[tokio::main] async fn main() { // Charger la config let mut server = ServerBuilder::new_configured().build(); // Ajouter des routes server .add_route("/hello", || async { serde_json::json!({"message": "Hello World"}) }) .await; server .add_route("/info", || async { serde_json::json!({"version": "1.0.0"}) }) .await; server.add_spa::("/app").await; // Gère la sortie des logs et sur le serveur SSE pour l'interface web et sur la console let log_state = LogState::new(1000); let subscriber = Registry::default() .with( tracing_subscriber::fmt::layer() .with_target(true) .with_level(true) .with_ansi(true), // Couleurs dans le terminal ) .with(SseLayer::new(log_state.clone())); tracing::subscriber::set_global_default(subscriber).unwrap(); server .add_handler_with_state("/log-sse", log_sse, log_state.clone()) .await; server .add_handler_with_state("/log-dump", log_dump, log_state.clone()) .await; server.add_redirect("/", "/app").await; info!("{}",SETAVTRANSPORTURI.to_markdown()); server.start().await; server.wait().await; } ``` ## fichier: `pmoupnp/Cargo.toml` ```toml [package] name = "pmoupnp" version = "0.1.0" edition = "2024" [dependencies] pmoconfig = { path = "../pmoconfig" } pmodidl = { path = "../pmodidl"} url = "2.5.7" uuid = "1.18.1" hex = "0.4.3" base64 = "0.22.1" thiserror = "2.0.16" xmltree = "0.11.0" get_if_addrs = "0.5.3" axum = "0.8.4" tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time"] } tokio-stream = "0.1" futures-util = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" chrono = { version = "0.4.42", features = ["serde"] } log = "0.4.28" once_cell = "1.20" parking_lot = "0.12" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } futures = "0.3" async-stream = "0.3.6" axum-server = "0.7.2" axum-embed = "0.1.0" rust-embed = "8.7.2" anyhow = "1.0" utoipa = { version = "5.4.0", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } validator = { version = "0.20.0", features = ["derive"] } bevy_reflect = "0.17.1" bevy_reflect_derive = "0.17.1" reqwest = "0.12.23" ``` ## fichier: `pmoupnp/webapp/src/App.vue` ```vue ``` ## fichier: `pmoupnp/webapp/src/main.ts` ```typescript import { createApp } from "vue"; import App from "./App.vue"; import router from "./router"; import "./style.css"; createApp(App).use(router).mount("#app"); ``` ## fichier: `pmoupnp/webapp/src/components/LogView.vue` ```vue ``` ## fichier: `pmoupnp/webapp/src/components/HelloWorld.vue` ```vue ``` ## fichier: `pmoupnp/webapp/src/style.css` ```css :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; width: 100vw; } h1 { font-size: 3.2em; line-height: 1.1; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } .card { padding: 2em; } #app { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } ``` ## fichier: `pmoupnp/webapp/src/shims-vue.d.ts` ```typescript declare module "*.vue" { import { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; } ``` ## fichier: `pmoupnp/webapp/src/router/index.ts` ```typescript import { createRouter, createWebHistory } from "vue-router"; import HelloWorld from "../components/HelloWorld.vue"; import LogView from "../components/LogView.vue"; const routes = [ { path: "/", name: "home", component: HelloWorld }, { path: "/logs", name: "logs", component: LogView }, ]; const router = createRouter({ // history avec base /app history: createWebHistory("/app"), routes, }); export default router; ``` ## fichier: `pmoupnp/errors.rs` ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum StateVariableError { #[error("Conversion error: {0}")] ConversionError(String), #[error("Validation error: {0}")] ValidationError(String), #[error("Range error: {0}")] RangeError(String), #[error("Type error: {0}")] TypeError(String), #[error("Parse error: {0}")] ParseError(String), #[error("Event condition error: {0}")] EventConditionError(String), #[error("Arithmetic error: {0}")] ArithmeticError(String), #[error("Unknown error: {0}")] Unknown(String), } impl From for StateVariableError { fn from(err: std::num::TryFromIntError) -> Self { StateVariableError::ConversionError(format!("Integer conversion error: {}", err)) } } impl From for StateVariableError { fn from(err: std::str::ParseBoolError) -> Self { StateVariableError::ConversionError(format!("Boolean conversion error: {}", err)) } } impl From for StateVariableError { fn from(err: uuid::Error) -> Self { StateVariableError::ConversionError(format!("UUID conversion error: {}", err)) } } impl From for StateVariableError { fn from(err: chrono::ParseError) -> Self { StateVariableError::ConversionError(format!("Time conversion error: {}", err)) } } impl From for StateVariableError { fn from(err: url::ParseError) -> Self { StateVariableError::ConversionError(format!("URI conversion error: {}", err)) } } impl From for StateVariableError { fn from(err: base64::DecodeError) -> Self { StateVariableError::ConversionError(format!("Base64 conversion error: {}", err)) } } impl From for StateVariableError { fn from(err: hex::FromHexError) -> Self { StateVariableError::ConversionError(format!("Hex conversion error: {}", err)) } } ``` ## fichier: `pmoupnp/src/object_set.rs` ```rust use std::{collections::HashMap, sync::Arc}; use std::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.read().unwrap(); 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.read().unwrap(); Self { objects: RwLock::new(guard.clone()), } } } impl UpnpObjectSet { /// Crée un nouveau `UpnpObjectSet` vide. /// /// # Examples /// /// ``` /// let set: UpnpObjectSet = UpnpObjectSet::new(); /// ``` 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)?; /// ``` pub fn insert(&mut self, object: Arc) -> Result<(), UpnpObjectSetError> { let mut guard = self.objects.write().unwrap(); 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); /// set.insert_or_replace(obj2); // Remplace obj1 /// ``` pub fn insert_or_replace(&mut self, object: Arc) { let mut guard = self.objects.write().unwrap(); 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()) { /// println!("L'objet existe déjà"); /// } /// ``` pub fn contains(&self, object: Arc) -> bool { let guard = self.objects.read().unwrap(); let key: String = object.get_name().to_string(); guard.contains_key(&key) } /// Récupère un objet par son nom. /// /// # 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") { /// println!("Objet trouvé: {}", obj.get_name()); /// } /// ``` pub fn get_by_name(&self, name: &str) -> Option> { let guard = self.objects.read().unwrap(); guard.get(name).cloned() } /// Retourne tous les objets du set. /// /// # 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() { /// println!("Objet: {}", obj.get_name()); /// } /// ``` /// /// # Thread-safety /// /// Cette méthode acquiert un verrou de lecture. Plusieurs threads peuvent /// appeler cette méthode simultanément sans blocage. pub fn all(&self) -> Vec> { let guard = self.objects.read().unwrap(); guard.values().cloned().collect() } }``` ## fichier: `pmoupnp/src/state_variables/instance_methods.rs` ```rust use std::fmt; use chrono::{DateTime, Utc}; use std::sync::RwLock; use xmltree::Element; use crate::{ object_trait::{UpnpInstance, UpnpObject}, state_variables::{StateVarInstance, StateVariable, UpnpVariable}, variable_types::{StateValue, StateValueError, UpnpVarType}, UpnpObjectType, UpnpTyped, UpnpTypedInstance }; impl UpnpVariable for StateVarInstance { fn get_definition(&self) -> &StateVariable { return &self.model; } } impl UpnpObject for StateVarInstance { fn to_xml_element(&self) -> Element { self.get_definition().to_xml_element() } } 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(), }, 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()), } } } 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.read().unwrap().clone()), old_value: RwLock::new(self.old_value.read().unwrap().clone()), last_modified: RwLock::new(self.last_modified.read().unwrap().clone()), last_notification: RwLock::new(self.last_notification.read().unwrap().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().unwrap(); let mut val = self.value.write().unwrap(); let mut modified = self.last_modified.write().unwrap(); *old_val = val.clone(); *val = new_value; *modified = Utc::now(); Ok(()) } /// Accès à la valeur pub fn value(&self) -> StateValue { self.value.read().unwrap().clone() } /// Accès au timestamp pub fn last_modified(&self) -> DateTime { self.last_modified.read().unwrap().clone() } } ``` ## fichier: `pmoupnp/src/state_variables/var_set_methods.rs` ```rust use xmltree::{Element, XMLNode}; use crate::{object_trait::UpnpModel, state_variables::{StateVarInstanceSet, StateVariableSet}, UpnpObject}; impl UpnpObject for StateVariableSet { fn to_xml_element(&self) -> Element { let mut elem = Element::new("serviceStateTable"); for state_var in self.all() { let state_var_elem = state_var.to_xml_element(); // retourne un complet elem.children.push(XMLNode::Element(state_var_elem)); } elem } } impl UpnpModel for StateVariableSet { type Instance = StateVarInstanceSet; } ``` ## fichier: `pmoupnp/src/state_variables/variable_trait.rs` ```rust use crate::{ state_variables::StateVariable, 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 { 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 { 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 { 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 { 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 { let guard = self.get_definition().event_conditions.read().unwrap(); !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(); 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 { !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 { 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 { 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 .clone() .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 { let guard = self.get_definition() .allowed_values .read().unwrap(); !guard.is_empty() } /// 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 .read().unwrap(); 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() } } ``` ## fichier: `pmoupnp/src/state_variables/mod.rs` ```rust 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, }; pub use crate::state_variables::variable_trait::UpnpVariable; use bevy_reflect::Reflect; use chrono::{DateTime, Utc}; pub use errors::StateVariableError; use std::sync::RwLock; use crate::{ value_ranges::ValueRange, variable_types::{StateValue, StateVarType}, UpnpObjectSet, UpnpObjectType, }; /// Type pour les fonctions de condition d'événement pub type StateConditionFunc = Arc bool + Send + Sync>; /// Type pour les fonctions de parsing de valeurs depuis des chaînes pub type StringValueParser = Arc Result, StateVariableError> + Send + Sync>; /// Type pour les fonctions de sérialisation de valeurs vers des chaînes pub type ValueSerializer = Arc Result + Send + Sync>; pub struct StateVariable { object: UpnpObjectType, value_type: StateVarType, step: Option, modifiable: bool, event_conditions: Arc>>, description: String, default_value: Option, value_range: Option, allowed_values: Arc>>, send_events: bool, parse: Option, marshal: Option, } pub type StateVariableSet = UpnpObjectSet; pub struct StateVarInstance { object: UpnpObjectType, model: StateVariable, value: RwLock, old_value: RwLock, last_modified: RwLock>, last_notification: RwLock>, } pub type StateVarInstanceSet = UpnpObjectSet; ``` ## fichier: `pmoupnp/src/state_variables/var_inst_set_methods.rs` ```rust use std::collections::HashMap; use std::sync::RwLock; use xmltree::{Element, XMLNode}; use crate::{state_variables::{StateVarInstanceSet, StateVariableSet}, UpnpObject}; use crate::UpnpInstance; impl UpnpObject for StateVarInstanceSet { fn to_xml_element(&self) -> Element { let mut elem = Element::new("serviceStateTable"); for state_var in self.all() { let state_var_elem = state_var.to_xml_element(); // 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()) } } } ``` ## fichier: `pmoupnp/src/state_variables/errors.rs` ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum StateVariableError { #[error("Conversion error: {0}")] ConversionError(String), #[error("Validation error: {0}")] ValidationError(String), #[error("Range error: {0}")] RangeError(String), #[error("Type error: {0}")] TypeError(String), #[error("Parse error: {0}")] ParseError(String), #[error("Event condition error: {0}")] EventConditionError(String), #[error("Arithmetic error: {0}")] ArithmeticError(String), #[error("Unknown error: {0}")] Unknown(String), } ``` ## fichier: `pmoupnp/src/state_variables/variable_methods.rs` ```rust use std::{ collections::HashMap, fmt, sync::Arc, }; use std::sync::RwLock; use xmltree::{Element, XMLNode}; use crate::{ 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 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 { fn to_xml_element(&self) -> Element { // Création de l'élément racine let mut root = Element::new("stateVariable"); root.attributes.insert( "sendEvents".to_string(), if self.send_events { "yes" } else { "no" }.to_string(), ); // let mut name_elem = Element::new("name"); name_elem .children .push(XMLNode::Text(self.get_name().clone())); // let mut datatype_elem = Element::new("dataType"); datatype_elem .children .push(XMLNode::Text(self.value_type.to_string())); // StateVarType doit impl Display // si défini if let Some(default) = &self.default_value { let mut def_elem = Element::new("defaultValue"); def_elem.children.push(XMLNode::Text(default.to_string())); root.children.push(XMLNode::Element(def_elem)); } // si défini let av = self.allowed_values.read().unwrap(); 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 if let Some(range) = &self.value_range { let mut range_elem = Element::new("allowedValueRange"); let mut min_elem = Element::new("minimum"); min_elem .children .push(XMLNode::Text(range.get_minimum().to_string())); range_elem.children.push(XMLNode::Element(min_elem)); let mut max_elem = Element::new("maximum"); max_elem .children .push(XMLNode::Text(range.get_maximum().to_string())); range_elem.children.push(XMLNode::Element(max_elem)); if let Some(step) = &self.step { let mut step_elem = Element::new("step"); step_elem.children.push(XMLNode::Text(step.to_string())); range_elem.children.push(XMLNode::Element(step_elem)); } root.children.push(XMLNode::Element(range_elem)); } // Ajouter les enfants communs root.children.push(XMLNode::Element(name_elem)); root.children.push(XMLNode::Element(datatype_elem)); root } } impl UpnpModel for StateVariable { type Instance = StateVarInstance; } impl Clone for StateVariable { fn clone(&self) -> Self { // clone safe des structures protégées par RwLock en prenant un read lock let event_conditions_clone = { // si le lock est "poisoned" on panic - tu peux adapter la gestion si tu veux let guard = self .event_conditions .read().unwrap(); // nécessite que Key: Clone, Value: Clone Arc::new(RwLock::new(guard.clone())) }; let allowed_values_clone = { let guard = self .allowed_values .read().unwrap(); Arc::new(RwLock::new(guard.clone())) }; Self { object: self.object.clone(), value_type: self.value_type.clone(), step: self.step.clone(), modifiable: self.modifiable, event_conditions: event_conditions_clone, description: self.description.clone(), default_value: self.default_value.clone(), value_range: self.value_range.clone(), allowed_values: allowed_values_clone, send_events: self.send_events, // parse et marshal sont typiquement des Arc — on clone l'Arc (shallow). // Deep-cloner une closure ou un trait-objet n'est pas possible en général. parse: self.parse.clone(), marshal: self.marshal.clone(), } } } impl fmt::Debug for StateVariable { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("StateVariable") .field("object", &self.object) .field("value_type", &self.value_type) .field("step", &self.step) .field("modifiable", &self.modifiable) .field( "event_conditions", &format_args!( "len={}", self.event_conditions.read().unwrap().len() ), ) .field("description", &self.description) .field("default_value", &self.default_value) .field("value_range", &self.value_range) .field( "allowed_values", &format_args!( "len={}", self.allowed_values.read().unwrap().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"), ) .finish() } } impl UpnpVariable for StateVariable { fn get_definition(&self) -> &StateVariable { return self; } } impl StateVariable { pub fn new(vartype: StateVarType, name: String) -> StateVariable { Self { object: UpnpObjectType { name, object_type: "StateVariable".to_string(), }, value_type: vartype.clone(), step: None, modifiable: true, event_conditions: Arc::new(RwLock::new(HashMap::new())), description: "".to_string(), default_value: None, value_range: None, allowed_values: Arc::new(RwLock::new(Vec::new())), send_events: false, parse: None, marshal: None, } } pub fn set_step(&mut self, step: StateValue) -> Result<(), StateValueError> { if self.as_state_var_type() != step.as_state_var_type() { return Err(StateValueError::TypeError("Bad step type".to_string())); } self.step = Some(step); Ok(()) } pub fn set_range(&mut self, min: &StateValue, max: &StateValue) -> Result<(), StateValueError> { if self.as_state_var_type() != min.as_state_var_type() { return Err(StateValueError::TypeError("Bad range type".to_string())); } let range = ValueRange::new(min, max)?; // ? propage l'erreur si elle existe self.value_range = Some(range); Ok(()) } pub fn update_minimum(&mut self, min: &StateValue) -> Result<(), StateValueError> { if !self.has_range() { return Err(StateValueError::RangeError( "No range specified for this variable".to_string(), )); } if self .value_range .as_ref() .expect("Range is not defined") .as_state_var_type() != min.as_state_var_type() { return Err(StateValueError::TypeError( "new minimum is not the same than state variable".to_string(), )); } self.value_range .as_mut() .expect("Range is not defined") .set_minimum(min); return Ok(()); } pub fn update_maximum(&mut self, min: &StateValue) -> Result<(), StateValueError> { if !self.has_range() { return Err(StateValueError::RangeError( "No range specified for this variable".to_string(), )); } if self .value_range .as_ref() .expect("Range is not defined") .as_state_var_type() != min.as_state_var_type() { return Err(StateValueError::TypeError( "new minimum is not the same than state variable".to_string(), )); } self.value_range .as_mut() .expect("Range is not defined") .set_maximum(min); return Ok(()); } pub fn get_range(&self) -> Option<&ValueRange> { return self.value_range.as_ref(); } pub fn set_modifiable(&mut self) { self.modifiable = true; } pub fn set_not_modifiable(&mut self) { self.modifiable = false; } pub fn add_event_condition(&self, name: String, func: StateConditionFunc) { // on lock en écriture let mut guard = self.event_conditions.write().unwrap(); 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(); guard.remove(name); } pub fn clear_event_conditions(&mut self) { let mut guard = self.event_conditions.write().unwrap(); guard.clear() } pub fn set_description(&mut self, description: String) { self.description = description; } pub fn set_default(&mut self, value: &StateValue) -> Result<(), StateValueError> { if self.as_state_var_type() != value.as_state_var_type() { return Err(StateValueError::TypeError( "value does not have the right type".to_string(), )); } self.default_value = Some(value.clone()); return Ok(()); } pub fn unset_default(&mut self) { self.default_value = None; } pub fn extend_allowed_values(&mut self, values: &[StateValue]) -> Result<(), StateValueError> { let mut av = self .allowed_values .write().unwrap(); for v in values { if self.as_state_var_type() == v.as_state_var_type() { av.push(v.clone()); } else { return Err(StateValueError::TypeError( "new allowed value does not have the right type".to_string(), )); } } Ok(()) } pub fn push_allowed_value(&mut self, value: &StateValue) -> Result<(), StateValueError> { let mut av = self .allowed_values .write().unwrap(); if self.as_state_var_type() == value.as_state_var_type() { av.push(value.clone()); } else { return Err(StateValueError::TypeError( "new allowed value does not have the right type".to_string(), )); } return Ok(()); } pub fn set_send_notification(&mut self) { self.send_events = true; } pub fn unset_send_notification(&mut self) { self.send_events = false; } pub fn set_value_parser(&mut self, parser: StringValueParser) -> Result<(), StateValueError> { if self.as_state_var_type() == StateVarType::String { self.parse = Some(parser); return Ok(()); } return Err(StateValueError::TypeError( "Only String variables can have a parser".to_string(), )); } pub fn unset_value_parser(&mut self) { self.parse = None; } pub fn set_value_marshaler( &mut self, marshaler: ValueSerializer, ) -> Result<(), StateValueError> { if self.as_state_var_type() == StateVarType::String { self.marshal = Some(marshaler); return Ok(()); } return Err(StateValueError::TypeError( "Only String variables can have a marshaler".to_string(), )); } pub fn unset_value_marshaler(&mut self) { self.marshal = None; } } ``` ## fichier: `pmoupnp/src/lib.rs` ```rust 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; use std::{collections::HashMap, sync::Arc}; use std::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>>, } #[derive(Debug)] pub enum UpnpObjectSetError { AlreadyExists(String), } ``` ## fichier: `pmoupnp/src/value_ranges/methods.rs` ```rust use std::cmp::Ordering; use crate::{ value_ranges::ValueRange, variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType}, }; impl UpnpVarType for ValueRange { fn as_state_var_type(&self) -> StateVarType { self.min.as_state_var_type() // utilise ton From<&StateValue> existant } } impl ValueRange { pub fn new(min: &StateValue, max: &StateValue) -> Result { if min.as_state_var_type() != max.as_state_var_type() { return Err(StateValueError::TypeError( "min and max do not belong the same time".to_string(), )); } // Vérifier que min <= max if let Some(cmp) = min.partial_cmp(max) { if cmp == Ordering::Greater { return Err(StateValueError::RangeError( "Minimum cannot be greater than maximum".to_string(), )); } } Ok(Self { min: min.clone(), max: max.clone(), }) } pub fn get_minimum(self: &ValueRange) -> StateValue { return self.min.clone(); } pub fn set_minimum(&mut self, value: &StateValue) { self.min = value.clone() } pub fn get_maximum(self: &ValueRange) -> StateValue { return self.max.clone(); } pub fn set_maximum(&mut self, value: &StateValue) { self.max = value.clone() } pub fn is_in_range(&self, value: &StateValue) -> bool { if self.as_state_var_type() == value.as_state_var_type() && let Some(cmp) = self.min.partial_cmp(value) { if cmp == Ordering::Greater { return false; } if let Some(cmp2) = self.max.partial_cmp(value) { if cmp2 == Ordering::Less { return false; } return true; } } return false; } } ``` ## fichier: `pmoupnp/src/value_ranges/mod.rs` ```rust mod methods; use crate::variable_types::StateValue; #[derive(Debug, Clone)] pub struct ValueRange { min: StateValue, max: StateValue, } ``` ## fichier: `pmoupnp/src/mediarenderer/mod.rs` ```rust pub mod avtransport; ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/mod.rs` ```rust pub mod variables; pub mod actions; ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs` ```rust 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(|| -> Arc { Arc::new(StateVariable::new(StateVarType::UI4, "A_ARG_TYPE_InstanceID".to_string())) }); ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs` ```rust 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(|| -> Arc { Arc::new(StateVariable::new(StateVarType::String, "A_ARG_TYPE_PlaySpeed".to_string())) }); ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs` ```rust 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(|| -> Arc { let mut sv = StateVariable::new(StateVarType::String, "TransportStatus".to_string()); sv.push_allowed_value(&StateValue::String("OK".to_string())) .expect("Cannot add allowed value"); sv.extend_allowed_values(&[ StateValue::String("OK".to_string()), StateValue::String("ERROR_OCCURRED".to_string()), ]) .expect("Cannt set default value"); Arc::new(sv) }); ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs` ```rust 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(|| -> 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"); Arc::new(sv) }); ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs` ```rust 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(|| -> Arc { Arc::new(StateVariable::new(StateVarType::String, "AVTransportURI".to_string())) }); ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/mod.rs` ```rust mod a_arg_type_instanceid; mod a_arg_type_playspeed; mod avtransporturi; mod avtransporturimetadata; mod currenttrackduration; mod seekmode; mod transportplayspeed; mod transportstate; mod transportstatus; pub use a_arg_type_instanceid::A_ARG_TYPE_INSTANCE_ID; pub use a_arg_type_playspeed::A_ARG_TYPE_PLAY_SPEED; pub use avtransporturi::AVTRANSPORTURI; pub use avtransporturimetadata::AVTRANSPORTURIMETADATA; pub use currenttrackduration::CURRENTTRACKDURATION; pub use seekmode::SEEKMODE; pub use transportplayspeed::TRANSPORTPLAYSPEED; pub use transportstate::TRANSPORTSTATE; pub use transportstatus::TRANSPORTSTATUS; ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs` ```rust 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(|| -> Arc { Arc::new(StateVariable::new(StateVarType::String, "SeekMode".to_string())) }); ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/currenttrackduration.rs` ```rust 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(|| -> Arc { Arc::new(StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string())) }); ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs` ```rust use std::sync::Arc; use crate::state_variables::{StateVariable, StateVariableError}; use crate::variable_types::StateVarType; use bevy_reflect::Reflect; use once_cell::sync::Lazy; use pmodidl::{DIDLLite, MediaMetadataParser}; // func _AVTransportURIMetaDataParser(value string) (interface{}, error) { // log.Debug("[avtransport] Parsing AVTransport)") // didl, err := pmodidl.Parse(value) // if err != nil { // return value, err // } // return didl, nil // } fn avtransporturimetadataparser(value: &str) -> Result, StateVariableError> { // Parse DIDL-Lite let didl = DIDLLite::parse(value) .map_err(|e| StateVariableError::ParseError(format!("Failed to parse DIDL-Lite: {}", e)))?; // Retourne le résultat sous forme de Box Ok(Box::new(didl) as Box) } 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"); Arc::new(sv) }); ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs` ```rust 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(|| -> 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"); sv.extend_allowed_values(&[ StateValue::String("STOPPED".to_string()), StateValue::String("PLAYING".to_string()), StateValue::String("RECORDING".to_string()), StateValue::String("TRANSITIONING".to_string()), StateValue::String("PAUSED_PLAYBACK".to_string()), StateValue::String("PAUSED_RECORDING".to_string()), StateValue::String("NO_MEDIA_PRESENT".to_string()), ]).expect("Cannt set default value"); Arc::new(sv) }); ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/play.rs` ```rust use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTPLAYSPEED}; use crate::define_action; define_action! { pub static PLAY = "Play" { in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, in "Speed" => TRANSPORTPLAYSPEED, } } ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/stop.rs` ```rust use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID; use crate::define_action; define_action! { pub static STOP = "Stop" { in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, } } ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/setavtransporturi.rs` ```rust use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTURI, AVTRANSPORTURIMETADATA}; use crate::define_action; define_action! { pub static SETAVTRANSPORTURI = "SetAVTransportURI" { in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, in "CurrentURI" => AVTRANSPORTURI, in "CurrentURIMetaData" => AVTRANSPORTURIMETADATA, } } ``` ## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/mod.rs` ```rust mod play; mod stop; mod setavtransporturi; pub use play::PLAY; pub use stop::STOP; pub use setavtransporturi::SETAVTRANSPORTURI; ``` ## fichier: `pmoupnp/src/server/mod.rs` ```rust //! # Module Server - API de haut niveau pour Axum //! //! Ce module fournit une abstraction simple et ergonomique pour créer des serveurs HTTP //! avec Axum, en cachant la complexité de la configuration et du routage. //! //! ## Fonctionnalités //! //! - 🚀 **Routes JSON simples** : Ajoutez des endpoints API avec `add_route()` //! - 📁 **Fichiers statiques** : Servez des assets avec `add_dir()` //! - ⚛️ **Applications SPA** : Support pour Vue.js/React avec `add_spa()` //! - 🔀 **Redirections** : Redirigez des routes avec `add_redirect()` //! - 🎯 **Handlers personnalisés** : Support SSE, WebSocket, etc. avec `add_handler_with_state()` //! - 📚 **Documentation API** : OpenAPI/Swagger automatique avec `add_openapi()` //! - ⚡ **Gestion gracieuse** : Arrêt propre sur Ctrl+C pub mod logs; use axum::handler::Handler; use axum::response::Redirect; use axum::routing::get; use axum::{Json, Router}; use axum_embed::ServeEmbed; use pmoconfig::get_config; use rust_embed::RustEmbed; use serde::Serialize; use std::{net::SocketAddr, sync::Arc}; use tokio::{signal, sync::RwLock, task::JoinHandle}; use tracing::{info, warn, debug, error}; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; /// Info serveur sérialisable #[derive(Clone, Serialize, utoipa::ToSchema)] pub struct ServerInfo { /// Nom du serveur pub name: String, /// URL de base pub base_url: String, /// Port HTTP pub http_port: u16, } /// Serveur principal pub struct Server { name: String, base_url: String, http_port: u16, router: Arc>, api_router: Arc>>, join_handle: Option>, } #[derive(RustEmbed, Clone)] #[folder = "webapp/dist"] pub struct Webapp; impl Server { /// Crée une nouvelle instance de serveur /// /// # Arguments /// /// * `name` - Nom du serveur (pour les logs) /// * `base_url` - URL de base (ex: "http://localhost:3000") /// * `http_port` - Port HTTP à écouter /// /// # Exemple /// /// ```rust /// # use pmoupnp::server::Server; /// let server = Server::new("MyAPI", "http://localhost:3000", 3000); /// ``` pub fn new(name: impl Into, base_url: impl Into, http_port: u16) -> Self { Self { name: name.into(), base_url: base_url.into(), http_port, router: Arc::new(RwLock::new(Router::new())), api_router: Arc::new(RwLock::new(None)), join_handle: None, } } pub fn new_configured() -> Self { let config = get_config(); let url = config.get_base_url(); let port = config.get_http_port(); return Self::new("PMO-Music-Server", url, port); } /// Ajoute une route JSON dynamique /// /// Crée un endpoint qui retourne du JSON. La closure fournie sera appelée /// à chaque requête GET sur le chemin spécifié. /// /// # Arguments /// /// * `path` - Chemin de la route (ex: "/api/hello") /// * `f` - Closure async retournant une valeur sérialisable /// /// # Exemple /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// server.add_route("/api/status", || async { /// serde_json::json!({ /// "status": "online", /// "version": "1.0.0" /// }) /// }).await; /// # } /// ``` pub async fn add_route(&mut self, path: &str, f: F) where F: Fn() -> Fut + Send + Sync + 'static, Fut: std::future::Future + Send + 'static, T: Serialize + Send + 'static, { let f = Arc::new(f); let handler = { let f = f.clone(); move || { let f = f.clone(); async move { Json(f().await) } } }; let route = Router::new().route("/", get(handler)); let mut r = self.router.write().await; *r = std::mem::take(&mut *r).nest(path, route); } /// Ajoute un répertoire de fichiers statiques /// /// Sert des fichiers embarqués via `RustEmbed`. Les fichiers sont compilés /// dans le binaire à la compilation. /// /// # Arguments /// /// * `path` - Chemin où monter les fichiers statiques /// /// # Type Parameter /// /// * `E` - Type RustEmbed définissant le répertoire à servir /// /// # Exemple /// /// ```ignore /// use pmoupnp::server::Server; /// use rust_embed::RustEmbed; /// /// #[derive(RustEmbed, Clone)] /// #[folder = "static/"] /// struct Assets; /// /// # #[tokio::main] /// # async fn main() { /// let mut server = Server::new("Test", "http://localhost:3000", 3000); /// server.add_dir::("/assets").await; /// // Les fichiers de static/ sont accessibles via /assets/* /// # } /// ``` pub async fn add_dir(&mut self, path: &str) where E: RustEmbed + Clone + Send + Sync + 'static, { let serve = ServeEmbed::::new(); let mut r = self.router.write().await; if path == "/" { *r = std::mem::take(&mut *r).fallback_service(serve); } else { let route = Router::new().fallback_service(serve); *r = std::mem::take(&mut *r).nest(path, route); } } /// Ajoute une Single Page Application (SPA) /// /// Sert une application JavaScript moderne (Vue.js, React, etc.) avec support /// du routage côté client. Tous les chemins non trouvés renvoient `index.html` /// pour permettre au routeur JavaScript de gérer la navigation. /// /// # Arguments /// /// * `path` - Chemin où monter l'application (souvent "/" ou "/app") /// /// # Type Parameter /// /// * `E` - Type RustEmbed contenant les fichiers de la SPA /// /// # Exemple avec Vue.js /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # use rust_embed::RustEmbed; /// #[derive(RustEmbed, Clone)] /// #[folder = "webapp/dist"] // Build output de Vue.js /// struct WebApp; /// /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// server.add_spa::("/").await; /// // L'app Vue.js gère toutes les routes comme /about, /users, etc. /// # } /// ``` /// /// # Note /// /// Pour Vue.js/Vite, configure le `base` dans `vite.config.js` si tu montes /// sur un sous-chemin : /// ```javascript /// export default { /// base: '/app/' /// } /// ``` pub async fn add_spa(&mut self, path: &str) where E: RustEmbed + Clone + Send + Sync + 'static, { let serve = ServeEmbed::::with_parameters( Some("index.html".to_string()), axum_embed::FallbackBehavior::Ok, Some("index.html".to_string()), ); let mut r = self.router.write().await; if path == "/" { *r = std::mem::take(&mut *r).fallback_service(serve); } else { let route = Router::new().fallback_service(serve); *r = std::mem::take(&mut *r).nest(path, route); } } /// Ajoute un handler Axum personnalisé /// /// Pour des cas d'usage avancés nécessitant un contrôle complet sur le handler. /// /// # Arguments /// /// * `path` - Chemin de la route /// * `handler` - Handler Axum /// /// # Exemple /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # use axum::response::Html; /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// async fn custom_handler() -> Html<&'static str> { /// Html("

Custom Response

") /// } /// /// server.add_handler("/custom", custom_handler).await; /// # } /// ``` pub async fn add_handler(&mut self, path: &str, handler: H) where H: Handler, T: 'static, { let route = Router::new().route("/", get(handler)); let mut r = self.router.write().await; *r = std::mem::take(&mut *r).nest(path, route); } /// Ajoute un handler avec state (pour SSE, extracteurs, etc.) /// /// Permet d'utiliser des extracteurs Axum comme `State`, `Query`, etc. /// Idéal pour Server-Sent Events (SSE), WebSockets ou tout handler nécessitant un état partagé. /// /// # Arguments /// /// * `path` - Chemin de la route /// * `handler` - Handler Axum avec extracteurs /// * `state` - État partagé (doit être Clone + Send + Sync) /// /// # Exemple avec SSE /// /// ```ignore /// use pmoupnp::server::Server; /// use axum::extract::State; /// use axum::response::sse::{Event, Sse, KeepAlive}; /// use tokio::sync::broadcast; /// /// #[derive(Clone)] /// struct LogState { /// tx: broadcast::Sender /// } /// /// impl LogState { /// fn subscribe(&self) -> broadcast::Receiver { /// self.tx.subscribe() /// } /// } /// /// async fn log_sse(State(state): State) -> Sse>> { /// let mut rx = state.subscribe(); /// let stream = async_stream::stream! { /// while let Ok(msg) = rx.recv().await { /// yield Ok(Event::default().data(msg)); /// } /// }; /// Sse::new(stream).keep_alive(KeepAlive::default()) /// } /// /// let log_state = LogState { tx: broadcast::channel(100).0 }; /// server.add_handler_with_state("/logs", log_sse, log_state).await; /// ``` pub async fn add_handler_with_state(&mut self, path: &str, handler: H, state: S) where H: Handler, T: 'static, S: Clone + Send + Sync + 'static, { let route = Router::new() .route("/", get(handler)) .with_state(state); let mut r = self.router.write().await; *r = std::mem::take(&mut *r).nest(path, route); } /// Ajoute un handler POST avec state /// /// Similaire à `add_handler_with_state` mais pour les requêtes POST. /// /// # Arguments /// /// * `path` - Chemin de la route /// * `handler` - Handler Axum pour POST /// * `state` - État partagé pub async fn add_post_handler_with_state(&mut self, path: &str, handler: H, state: S) where H: Handler, T: 'static, S: Clone + Send + Sync + 'static, { let route = Router::new() .route("/", axum::routing::post(handler)) .with_state(state); let mut r = self.router.write().await; *r = std::mem::take(&mut *r).nest(path, route); } /// Ajoute une redirection HTTP /// /// Redirige automatiquement les requêtes d'un chemin vers un autre avec un code 308 (permanent). /// /// # Arguments /// /// * `from` - Chemin source (peut être "/" pour la racine) /// * `to` - Chemin de destination /// /// # Exemple /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// // Rediriger la racine vers /app /// server.add_redirect("/", "/app").await; /// # } /// ``` pub async fn add_redirect(&mut self, from: &str, to: &str) { let to = to.to_string(); let handler = move || { let to = to.clone(); async move { Redirect::permanent(&to) } }; let mut r = self.router.write().await; if from == "/" { // Pour la racine, utiliser merge au lieu de nest let route = Router::new().route("/", get(handler)); *r = std::mem::take(&mut *r).merge(route); } else { let route = Router::new().route("/", get(handler)); *r = std::mem::take(&mut *r).nest(from, route); } } /// Ajoute une API documentée avec OpenAPI /// /// Monte un routeur d'API sous `/api` et active Swagger UI sur `/swagger-ui` /// /// # Arguments /// /// * `api_router` - Router Axum contenant les routes API /// * `openapi` - Spécification OpenAPI générée par utoipa /// /// # Exemple /// /// ```ignore /// use utoipa::OpenApi; /// use axum::{Router, Json, routing::get}; /// use serde::{Serialize, Deserialize}; /// /// #[derive(Serialize, Deserialize, utoipa::ToSchema)] /// struct User { /// id: u64, /// name: String, /// } /// /// #[derive(utoipa::OpenApi)] /// #[openapi( /// paths(get_users), /// components(schemas(User)) /// )] /// struct ApiDoc; /// /// #[utoipa::path( /// get, /// path = "/users", /// responses((status = 200, description = "List users")) /// )] /// async fn get_users() -> Json> { /// Json(vec![]) /// } /// /// let api_router = Router::new() /// .route("/users", get(get_users)); /// /// server.add_openapi(api_router, ApiDoc::openapi()).await; /// ``` pub async fn add_openapi(&mut self, api_router: Router, openapi: utoipa::openapi::OpenApi) { // Stocker le routeur API let mut api_r = self.api_router.write().await; *api_r = Some(api_router); // Ajouter Swagger UI let swagger = SwaggerUi::new("/swagger-ui") .url("/api-docs/openapi.json", openapi); let mut r = self.router.write().await; *r = std::mem::take(&mut *r).merge(swagger); } /// Démarre le serveur HTTP /// /// Lance le serveur sur le port configuré et met en place la gestion /// de Ctrl+C pour un arrêt gracieux. /// /// # Exemple /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// server.start().await; /// server.wait().await; // Attend Ctrl+C /// # } /// ``` pub async fn start(&mut self) { let addr = SocketAddr::from(([0, 0, 0, 0], self.http_port)); info!("Server {} running at [http://{}:{}](http://{}:{})", self.name, self.base_url, self.http_port, self.base_url, self.http_port); // Merger le routeur API si présent let api_router = self.api_router.read().await; if let Some(api_r) = api_router.as_ref() { let mut r = self.router.write().await; *r = std::mem::take(&mut *r).nest("/api", api_r.clone()); } drop(api_router); let router = self.router.clone(); let server_task = tokio::spawn(async move { let r = router.read().await.clone(); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, r.into_make_service()).await.unwrap(); }); let shutdown_task = tokio::spawn(async move { signal::ctrl_c().await.expect("failed to listen for ctrl_c"); info!("Ctrl+C reçu, arrêt gracieux"); }); self.join_handle = Some(tokio::spawn(async move { tokio::select! { _ = server_task => {}, _ = shutdown_task => {}, } })); } /// Attend la fin du serveur pub async fn wait(&mut self) { if let Some(h) = self.join_handle.take() { let _ = h.await; } } /// Récupère les infos du serveur pub fn info(&self) -> ServerInfo { ServerInfo { name: self.name.clone(), base_url: self.base_url.clone(), http_port: self.http_port, } } } /// Builder pattern pub struct ServerBuilder { name: String, base_url: String, http_port: u16, } impl ServerBuilder { /// Crée un nouveau builder /// /// # Arguments /// /// * `name` - Nom du serveur /// * `base_url` - URL de base (ex: "http://localhost:3000") /// * `http_port` - Port HTTP pub fn new(name: impl Into, base_url: impl Into, http_port: u16) -> Self { Self { name: name.into(), base_url: base_url.into(), http_port, } } pub fn new_configured() -> Self { let config = get_config(); Self { name: "PMO-Music-Server".to_string(), base_url: config.get_base_url(), http_port: config.get_http_port() } } /// Construit le serveur /// /// Consomme le builder et retourne une instance de `Server` prête à l'emploi. /// /// # Exemple /// /// ```rust /// # use pmoupnp::server::ServerBuilder; /// let mut server = ServerBuilder::new("MyAPI", "http://localhost:3000", 3000) /// .build(); /// ``` pub fn build(self) -> Server { Server::new(self.name, self.base_url, self.http_port) } }``` ## fichier: `pmoupnp/src/server/logs/mod.rs` ```rust // logs.rs mod sselayer; pub use sselayer::SseLayer; use std::{ collections::VecDeque, sync::{Arc, RwLock}, time::SystemTime, }; use axum::{ Json, extract::{Query, State}, response::{ IntoResponse, sse::{Event, KeepAlive, Sse}, }, }; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; /// Représente une entrée de log #[derive(Debug, Clone, Serialize)] pub struct LogEntry { pub timestamp: SystemTime, pub level: String, pub target: String, pub message: String, } /// Buffer circulaire partagé #[derive(Clone)] pub struct LogState { buffer: Arc>>, tx: broadcast::Sender, } impl LogState { pub fn new(capacity: usize) -> Self { Self { buffer: Arc::new(RwLock::new(VecDeque::with_capacity(capacity))), tx: broadcast::channel(1000).0, } } fn push(&self, entry: LogEntry) { let mut buf = self.buffer.write().unwrap(); if buf.len() == buf.capacity() { buf.pop_front(); } buf.push_back(entry.clone()); let _ = self.tx.send(entry); } pub fn subscribe(&self) -> broadcast::Receiver { self.tx.subscribe() } pub fn dump(&self) -> Vec { self.buffer.read().unwrap().iter().cloned().collect() } } /// Query params pour /log-sse #[derive(Debug, Deserialize)] pub struct LogQuery { #[serde(default)] pub error: Option, #[serde(default)] pub warn: Option, #[serde(default)] pub info: Option, #[serde(default)] pub debug: Option, #[serde(default)] pub trace: Option, #[serde(default)] pub search: Option, } /// Handler SSE // Dans logs.rs pub async fn log_sse( State(state): State, Query(params): Query, ) -> impl IntoResponse { let mut rx = state.subscribe(); // Récupérer l'historique du buffer let history = state.dump(); let stream = async_stream::stream! { // 1. Envoyer d'abord tous les logs historiques for entry in history { if !filter_entry(&entry, ¶ms) { continue; } let json = serde_json::to_string(&entry).unwrap(); yield Ok::<_, axum::Error>(Event::default().data(json)); } // 2. Puis streamer les nouveaux logs en temps réel while let Ok(entry) = rx.recv().await { if !filter_entry(&entry, ¶ms) { continue; } let json = serde_json::to_string(&entry).unwrap(); yield Ok::<_, axum::Error>(Event::default().data(json)); } }; Sse::new(stream).keep_alive(KeepAlive::default()) } /// Handler REST (dump JSON du buffer) pub async fn log_dump(State(state): State) -> impl IntoResponse { Json(state.dump()) } /// Fonction de filtrage fn filter_entry(entry: &LogEntry, q: &LogQuery) -> bool { // Filtrage par niveau let lvl = entry.level.to_lowercase(); let mut allowed = false; if let Some(true) = q.error { allowed |= lvl == "error"; } if let Some(true) = q.warn { allowed |= lvl == "warn"; } if let Some(true) = q.info { allowed |= lvl == "info"; } if let Some(true) = q.debug { allowed |= lvl == "debug"; } if let Some(true) = q.trace { allowed |= lvl == "trace"; } // si aucun flag → tout est autorisé if !(q.error.unwrap_or(false) || q.warn.unwrap_or(false) || q.info.unwrap_or(false) || q.debug.unwrap_or(false) || q.trace.unwrap_or(false)) { allowed = true; } // Filtrage par mot-clé if let Some(search) = &q.search { allowed &= entry.message.contains(search) || entry.target.contains(search); } allowed } ``` ## fichier: `pmoupnp/src/server/logs/sselayer.rs` ```rust use tracing::field::{Field, Visit}; use tracing::{Event, Subscriber}; use tracing_subscriber::{Layer, layer::Context}; use super::{LogEntry, LogState}; use std::time::SystemTime; struct LogVisitor { message: String, } impl LogVisitor { fn new() -> Self { Self { message: String::new(), } } } impl Visit for LogVisitor { fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { // capture le champ "message" ou concatène les autres if field.name() == "message" { self.message = format!("{:?}", value); } else { if !self.message.is_empty() { self.message.push(' '); } self.message .push_str(&format!("{}={:?}", field.name(), value)); } } } /// Layer de tracing qui pousse les events dans le buffer pub struct SseLayer { state: LogState, } impl SseLayer { pub fn new(state: LogState) -> Self { Self { state } } } impl Layer for SseLayer where S: Subscriber, { fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { let mut visitor = LogVisitor::new(); event.record(&mut visitor); let entry = LogEntry { timestamp: SystemTime::now(), level: event.metadata().level().to_string(), target: event.metadata().target().to_string(), message: visitor.message, }; self.state.push(entry); } } ``` ## fichier: `pmoupnp/src/actions/action_instance_set.rs` ```rust use crate::{ UpnpObject, actions::{ActionInstanceSet}, }; use xmltree::{Element,XMLNode}; impl UpnpObject for ActionInstanceSet { // Méthode pour convertir en XML (à implémenter avec une librairie XML) fn to_xml_element(&self) -> Element { let mut elem = Element::new("actionList"); for action in self.all() { let action_elem = action.to_xml_element(); // retourne un complet elem.children.push(XMLNode::Element(action_elem)); } elem } } ``` ## fichier: `pmoupnp/src/actions/action_instance.rs` ```rust use std::sync::Arc; use xmltree::{Element, XMLNode}; use crate::actions::Action; use crate::actions::Argument; use crate::actions::ArgumentSet; use crate::actions::ArgInstanceSet; use crate::actions::ActionInstance; use crate::UpnpInstance; use crate::UpnpObject; use crate::UpnpTyped; use crate::UpnpTypedInstance; use crate::UpnpObjectType; impl UpnpObject for ActionInstance { fn to_xml_element(&self) -> Element { let mut elem = Element::new("action"); // let mut name_elem = Element::new("name"); name_elem.children.push(XMLNode::Text(self.get_name().clone())); elem.children.push(XMLNode::Element(name_elem)); // Utiliser le set d'instances d'arguments let args_container = self.arguments.to_xml_element(); elem.children.push(XMLNode::Element(args_container)); elem } } impl UpnpTyped for ActionInstance { fn as_upnp_object_type(&self) -> &UpnpObjectType { return &self.object; } } impl UpnpInstance for ActionInstance { type Model = Action; fn new(action: &Action) -> Self { // Créer les instances d'arguments let mut arguments = ArgInstanceSet::new(); for arg_model in action.arguments().all() { let arg_instance = Arc::new(crate::actions::ArgumentInstance::new(&*arg_model)); if let Err(e) = arguments.insert(arg_instance) { tracing::error!("Failed to insert argument instance: {:?}", e); } } Self { object: UpnpObjectType { name: action.get_name().clone(), object_type: "ActionInstance".to_string(), }, model: action.clone(), arguments, // ⬅️ Set d'instances, pas le modèle ! } } } impl UpnpTypedInstance for ActionInstance { fn get_model(&self) -> &Self::Model { &self.model } } impl ActionInstance { /// Retourne une instance d'argument par son nom. /// /// # Arguments /// /// * `name` - Nom de l'argument à rechercher /// /// # Returns /// /// `Some(Arc)` si trouvé, `None` sinon. pub fn argument(&self, name: &str) -> Option> { self.arguments.get_by_name(name) } /// Retourne le set d'instances d'arguments. /// /// # Returns /// /// Référence vers le `ArgInstanceSet` contenant toutes les instances. /// /// # Examples /// /// ```ignore /// for arg_instance in action_instance.arguments_set().all() { /// println!("Argument: {}", arg_instance.get_name()); /// if let Some(var) = arg_instance.get_variable_instance() { /// println!(" Variable: {} = {}", var.get_name(), var.value()); /// } /// } /// ``` pub fn arguments_set(&self) -> &ArgInstanceSet { &self.arguments // ⬅️ Retourne les INSTANCES, pas les modèles ! } } #[cfg(test)] mod tests { use super::*; use crate::actions::Action; use crate::UpnpInstance; #[test] fn test_action_instance_creation() { let action = Action::new("Play".to_string()); let instance = ActionInstance::new(&action); assert_eq!(instance.get_name(), "Play"); } #[test] fn test_action_instance_has_argument_instances() { let action = Action::new("Play".to_string()); let instance = ActionInstance::new(&action); // Vérifier que arguments_set() retourne bien des instances assert!(instance.arguments_set().all().iter().all(|arg| { // Chaque argument doit être une ArgumentInstance arg.get_model(); // Cette méthode existe seulement sur les instances true })); } } ``` ## fichier: `pmoupnp/src/actions/arg_set_methods.rs` ```rust use crate::actions::ArgInstanceSet; use crate::UpnpModel; use crate::{ UpnpObject, actions::{ArgumentSet}, }; use xmltree::Element; 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.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é for child in arg_elem.children { elem.children.push(child); } } elem } } impl UpnpModel for ArgumentSet { type Instance = ArgInstanceSet; } ``` ## fichier: `pmoupnp/src/actions/argument_methods.rs` ```rust use std::sync::Arc; use xmltree::{Element, XMLNode}; use crate::{ actions::{Argument, ArgumentInstance}, state_variables::StateVariable, UpnpModel, UpnpObject, UpnpObjectType, UpnpTyped }; impl UpnpTyped for Argument { fn as_upnp_object_type(&self) -> &UpnpObjectType { &self.object } } impl UpnpObject for Argument { fn to_xml_element(&self) -> Element { let mut parent = Element::new("argumentList"); if self.is_in() && self.is_out() { // InOut → deux arguments parent.children.push(XMLNode::Element(make_argument_elem( self.get_name(), "in", self.state_variable().get_name(), ))); parent.children.push(XMLNode::Element(make_argument_elem( self.get_name(), "out", self.state_variable().get_name(), ))); } else { // Cas simple let direction = if self.is_in() { "in" } else { "out" }; parent.children.push(XMLNode::Element(make_argument_elem( self.get_name(), direction, self.state_variable().get_name(), ))); } parent } } impl UpnpModel for Argument { type Instance = ArgumentInstance; } impl Argument { fn new(name: String, state_variable: Arc) -> Self { Self { object: UpnpObjectType { name, object_type: "Argument".to_string(), }, state_variable, is_in: false, is_out: false, } } 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: Arc) -> Self { let mut arg = Self::new(name, state_variable); arg.is_out = true; arg } 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; arg } pub fn state_variable(&self) -> &StateVariable { &self.state_variable } pub fn is_in(&self) -> bool { self.is_in } pub fn is_out(&self) -> bool { self.is_out } } /// Fabrique un complet avec ses sous-éléments fn make_argument_elem(name: &str, direction: &str, state_var_name: &str) -> Element { let mut arg = Element::new("argument"); let mut name_elem = Element::new("name"); name_elem.children.push(XMLNode::Text(name.to_string())); let mut dir_elem = Element::new("direction"); dir_elem.children.push(XMLNode::Text(direction.to_string())); let mut rel_elem = Element::new("relatedStateVariable"); rel_elem .children .push(XMLNode::Text(state_var_name.to_string())); arg.children.push(XMLNode::Element(name_elem)); arg.children.push(XMLNode::Element(dir_elem)); arg.children.push(XMLNode::Element(rel_elem)); arg } ``` ## fichier: `pmoupnp/src/actions/arg_inst_set_methods.rs` ```rust use std::collections::HashMap; use std::sync::RwLock; use xmltree::{Element, XMLNode}; use crate::{actions::{ArgInstanceSet, ArgumentSet}, UpnpObject}; use crate::UpnpInstance; impl UpnpObject for ArgInstanceSet { fn to_xml_element(&self) -> Element { let mut elem = Element::new("serviceStateTable"); for state_var in self.all() { let state_var_elem = state_var.to_xml_element(); // 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()) } } } ``` ## fichier: `pmoupnp/src/actions/mod.rs` ```rust mod errors; mod action_instance; mod action_instance_set; mod action_methods; mod action_set_methods; mod arg_inst_set_methods; mod arg_instance_methods; mod arg_set_methods; mod argument_methods; mod macros; use crate::{ UpnpObjectSet, UpnpObjectType, state_variables::{StateVarInstance, StateVariable}, }; use std::sync::{Arc, RwLock}; pub use errors::ActionError; #[derive(Debug, Clone)] pub struct Action { object: UpnpObjectType, arguments: ArgumentSet, } pub type ActionSet = UpnpObjectSet; #[derive(Debug, Clone)] pub struct ActionInstance { object: UpnpObjectType, model: Action, arguments: ArgInstanceSet, } pub type ActionInstanceSet = UpnpObjectSet; #[derive(Debug, Clone)] pub struct Argument { object: UpnpObjectType, state_variable: Arc, is_in: bool, is_out: bool, } pub type ArgumentSet = UpnpObjectSet; /// Instance d'un argument d'action UPnP. /// /// Un `ArgumentInstance` représente un argument concret utilisé lors de l'exécution /// d'une action. Contrairement au modèle [`Argument`] qui définit la structure, /// l'instance maintient une liaison dynamique vers une [`StateVarInstance`] qui /// contient la valeur runtime. /// /// # Cycle de vie /// /// 1. **Création** : Instanciation via [`UpnpInstance::new`] avec `variable_instance = None` /// 2. **Liaison** : Association à une [`StateVarInstance`] via [`bind_variable`](Self::bind_variable) /// 3. **Utilisation** : Accès à la valeur runtime via [`get_variable_instance`](Self::get_variable_instance) /// /// # Pourquoi `variable_instance` est optionnel ? /// /// La liaison ne peut pas être faite dans le constructeur car : /// - Les `StateVarInstance` sont créées **après** les modèles /// - Les `ActionInstance` sont créées **avant** que toutes les variables soient disponibles /// - La validation des dépendances se fait en deux phases /// /// # Thread-safety /// /// Le champ `variable_instance` est protégé par un `RwLock` pour permettre : /// - La liaison après création (write lock) /// - L'accès concurrent en lecture (read lock) /// - L'utilisation dans un contexte multi-thread /// /// # Examples /// /// ```ignore /// use pmoupnp::actions::{Argument, ArgumentInstance}; /// use pmoupnp::state_variables::StateVarInstance; /// use std::sync::Arc; /// /// // Phase 1 : Créer l'instance (sans liaison) /// let arg_model = Argument::new_in("Volume".to_string(), volume_var); /// let arg_instance = ArgumentInstance::new(&arg_model); /// assert!(arg_instance.get_variable_instance().is_none()); /// /// // Phase 2 : Lier à une variable d'état /// let var_instance = Arc::new(StateVarInstance::new(&volume_var)); /// arg_instance.bind_variable(var_instance.clone()); /// assert!(arg_instance.get_variable_instance().is_some()); /// /// // Phase 3 : Utiliser la valeur runtime /// if let Some(var) = arg_instance.get_variable_instance() { /// println!("Current value: {}", var.value()); /// } /// ``` #[derive(Debug, Clone)] pub struct ArgumentInstance { /// Métadonnées de l'objet UPnP object: UpnpObjectType, /// Référence vers le modèle définissant la structure model: Argument, /// Liaison optionnelle vers l'instance de variable d'état. /// /// - `None` : Pas encore liée (état initial après construction) /// - `Some(Arc)` : Liée et prête à l'emploi /// /// Protégée par `RwLock` pour permettre la liaison post-construction /// et l'accès concurrent en lecture. variable_instance: Arc>>>, } pub type ArgInstanceSet = UpnpObjectSet; ``` ## fichier: `pmoupnp/src/actions/errors.rs` ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum ActionError { #[error("Action error: {0}")] GeneralError(String), #[error("Argument error: {0}")] ArgumentError(String), #[error("Set operation error: {0}")] SetError(String), } 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)) } }``` ## fichier: `pmoupnp/src/actions/macros.rs` ```rust /// 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, /// 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> = once_cell::sync::Lazy::new(|| { std::sync::Arc::new($crate::actions::Action::new($action_name.to_string())) }); }; // Variante avec arguments (pub static $name:ident = $action_name:literal { $( $direction:ident $arg_name:literal => $var_ref:expr ),* $(,)? }) => { pub static $name: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { let mut ac = $crate::actions::Action::new($action_name.to_string()); $( ac.add_argument( define_action!(@arg $direction $arg_name, $var_ref) ); )* std::sync::Arc::new(ac) }); }; // Helper interne pour créer un argument d'entrée (@arg in $name:literal, $var:expr) => { 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) => { std::sync::Arc::new( $crate::actions::Argument::new_out( $name.to_string(), std::sync::Arc::clone(&$var) ) ) }; } /// 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" => INSTANCE_ID, /// } /// /// STOP = "Stop" { /// 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 { $( $direction:ident $arg_name:literal => $var_ref:expr ),* $(,)? } )* ) => { $( define_action! { pub static $name = $action_name { $($direction $arg_name => $var_ref),* } } )* }; // Variante mixte : actions avec et sans arguments ( $( $name:ident = $action_name:literal $({ $( $direction:ident $arg_name:literal => $var_ref:expr ),* $(,)? })? )* ) => { $( $( 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 } )? )* }; }``` ## fichier: `pmoupnp/src/actions/action_set_methods.rs` ```rust use xmltree::{Element, XMLNode}; use crate::actions::ActionSet; use crate::UpnpObject; impl UpnpObject for ActionSet { fn to_xml_element(&self) -> Element { let mut elem = Element::new("actionList"); for action in self.all() { let action_elem = action.to_xml_element(); // retourne un complet elem.children.push(XMLNode::Element(action_elem)); } elem } } ``` ## fichier: `pmoupnp/src/actions/action_methods.rs` ```rust use std::sync::Arc; use xmltree::{Element, XMLNode}; use crate::UpnpModel; use crate::UpnpObject; use crate::UpnpObjectSetError; use crate::UpnpObjectType; use crate::UpnpTyped; use crate::actions::Action; use crate::actions::ActionInstance; use crate::actions::Argument; use crate::actions::ArgumentSet; impl UpnpObject for Action { fn to_xml_element(&self) -> Element { let mut action_elem = Element::new("action"); // let mut name_elem = Element::new("name"); name_elem .children .push(XMLNode::Text(self.get_name().clone())); action_elem.children.push(XMLNode::Element(name_elem)); // let args_elem = self.arguments.to_xml_element(); action_elem.children.push(XMLNode::Element(args_elem)); action_elem } } impl UpnpModel for Action { type Instance = ActionInstance; } impl UpnpTyped for Action { fn as_upnp_object_type(&self) -> &UpnpObjectType { return &self.object; } } impl Action { pub fn new(name: String) -> Action { Self { object: UpnpObjectType { name, object_type: "Action".to_string(), }, arguments: ArgumentSet::new(), } } pub fn add_argument(&mut self, arg: Arc) -> Result<(), UpnpObjectSetError> { self.arguments.insert(arg) } pub fn arguments(&self) -> &ArgumentSet { &self.arguments } } ``` ## fichier: `pmoupnp/src/actions/arg_instance_methods.rs` ```rust use std::sync::{Arc, RwLock}; use xmltree::Element; use crate::{actions::{Argument, ArgumentInstance}, state_variables::StateVarInstance, UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance}; impl UpnpObject for ArgumentInstance { fn to_xml_element(&self) -> Element { self.get_model().to_xml_element() } } impl UpnpTyped for ArgumentInstance { fn as_upnp_object_type(&self) -> &UpnpObjectType { return &self.object; } } /// Implémentation de [`UpnpTypedInstance`] pour [`ArgumentInstance`]. /// /// Cette implémentation permet d'accéder au modèle [`Argument`] depuis l'instance /// via la méthode [`get_model()`](UpnpTypedInstance::get_model). /// /// # Examples /// /// ```ignore /// use pmoupnp::UpnpTypedInstance; /// /// let arg_instance = ArgumentInstance::new(&arg_model); /// /// // Accéder au modèle /// let model = arg_instance.get_model(); /// println!("Direction: in={}, out={}", model.is_in(), model.is_out()); /// println!("Related variable: {}", model.state_variable().get_name()); /// ``` impl UpnpTypedInstance for ArgumentInstance { /// Retourne une référence vers le modèle [`Argument`]. /// /// Permet d'accéder aux métadonnées statiques définies dans le modèle : /// - Direction de l'argument (in/out) /// - Variable d'état associée /// - Nom et type fn get_model(&self) -> &Self::Model { &self.model } } /// Implémentation de [`UpnpInstance`] pour [`ArgumentInstance`]. /// /// Cette implémentation fournit le constructeur standard qui crée une instance /// **non liée** d'un argument. La liaison à une [`StateVarInstance`] doit être /// effectuée séparément via [`bind_variable`](ArgumentInstance::bind_variable). /// /// # Processus de construction en deux phases /// /// ```text /// Phase 1 (new) Phase 2 (bind_variable) /// ┌─────────────────┐ ┌──────────────────────┐ /// │ ArgumentInstance│ │ StateVarInstance │ /// │ │ │ │ /// │ model: Arc<...> │────>│ Liaison établie │ /// │ variable: None │ │ variable: Some(...) │ /// └─────────────────┘ └──────────────────────┘ /// ↓ ↓ /// Création bind_variable(&var) /// ``` /// /// # Pourquoi deux phases ? /// /// 1. **Ordre de création** : Les modèles (`Argument`) existent avant les instances /// 2. **Validation différée** : Les dépendances sont vérifiées après instanciation /// 3. **Découplage** : Permet de créer des arguments même si les variables n'existent pas encore /// /// # Examples /// /// ```ignore /// use pmoupnp::actions::{Argument, ArgumentInstance}; /// use pmoupnp::UpnpInstance; /// /// let arg_model = Argument::new_in("InstanceID".to_string(), instance_id_var); /// /// // Création de l'instance - Phase 1 /// let arg_instance = ArgumentInstance::new(&arg_model); /// /// // À ce stade, l'instance existe mais n'est pas encore liée /// assert_eq!(arg_instance.get_name(), "InstanceID"); /// assert!(arg_instance.get_variable_instance().is_none()); /// /// // La liaison se fera plus tard via bind_variable() /// ``` impl UpnpInstance for ArgumentInstance { type Model = Argument; /// Crée une nouvelle instance d'argument depuis son modèle. /// /// # Arguments /// /// * `from` - Référence vers le modèle [`Argument`] définissant cet argument /// /// # Returns /// /// Une nouvelle `ArgumentInstance` avec : /// - Nom copié depuis le modèle /// - Référence vers le modèle (clone) /// - `variable_instance` initialisé à `None` (liaison non établie) /// /// # État initial /// /// L'instance créée n'est **pas encore liée** à une variable d'état. /// Pour établir la liaison, appelez [`bind_variable`](ArgumentInstance::bind_variable). /// /// # Thread-safety /// /// L'instance retournée est thread-safe et peut être partagée via `Arc`. /// /// # Examples /// /// ```ignore /// use pmoupnp::UpnpInstance; /// /// // Création depuis un modèle /// let instance = ArgumentInstance::new(&arg_model); /// /// // L'instance hérite des propriétés du modèle /// assert_eq!(instance.get_name(), arg_model.get_name()); /// assert_eq!(instance.is_in(), arg_model.is_in()); /// /// // Mais n'a pas encore de valeur runtime /// assert!(instance.get_variable_instance().is_none()); /// ``` fn new(from: &Argument) -> Self { Self { // Copie des métadonnées depuis le modèle object: UpnpObjectType { name: from.get_name().clone(), object_type: "ArgumentInstance".to_string(), }, // Clone du modèle pour référence future model: from.clone(), // Initialisation à None - sera lié plus tard via bind_variable() // Arc> permet la modification thread-safe post-construction variable_instance: Arc::new(RwLock::new(None)), } } } // ============================================================================ // Méthodes de liaison et d'accès // ============================================================================ impl ArgumentInstance { /// Lie cet argument à une instance de variable d'état. /// /// Cette méthode établit la connexion entre l'argument et sa variable d'état, /// permettant l'accès aux valeurs runtime lors de l'exécution d'actions. /// /// # Arguments /// /// * `var_instance` - Instance de la variable d'état à lier /// /// # Thread-safety /// /// Cette méthode acquiert un **write lock** sur `variable_instance` et peut /// bloquer si d'autres threads lisent actuellement la valeur. /// /// # Panics /// /// Panique si le lock est empoisonné (poisoned), ce qui ne devrait jamais /// arriver dans un usage normal. /// /// # Examples /// /// ```ignore /// use std::sync::Arc; /// /// let arg_instance = ArgumentInstance::new(&arg_model); /// let var_instance = Arc::new(StateVarInstance::new(&state_var)); /// /// // Établir la liaison /// arg_instance.bind_variable(var_instance.clone()); /// /// // Vérifier que la liaison est établie /// assert!(arg_instance.get_variable_instance().is_some()); /// ``` /// /// # Note /// /// Cette méthode peut être appelée plusieurs fois pour changer la variable liée, /// bien que ce ne soit généralement pas recommandé dans un usage normal. pub fn bind_variable(&self, var_instance: Arc) { let mut var = self.variable_instance.write().unwrap(); *var = Some(var_instance); } /// Retourne l'instance de variable d'état liée, si elle existe. /// /// # Returns /// /// - `Some(Arc)` si une variable est liée /// - `None` si aucune liaison n'a été établie via [`bind_variable`](Self::bind_variable) /// /// # Thread-safety /// /// Cette méthode acquiert un **read lock** sur `variable_instance`. /// Plusieurs threads peuvent lire simultanément sans blocage. /// /// # Panics /// /// Panique si le lock est empoisonné (poisoned). /// /// # Examples /// /// ```ignore /// // Vérifier si la liaison existe /// if let Some(var) = arg_instance.get_variable_instance() { /// println!("Variable liée : {}", var.get_name()); /// println!("Valeur actuelle : {}", var.value()); /// } else { /// println!("Aucune variable liée"); /// } /// ``` /// /// # Usage dans l'exécution d'actions /// /// ```ignore /// async fn execute_action(action: &ActionInstance) -> Result<(), ActionError> { /// for arg in action.arguments_set().all() { /// if let Some(var) = arg.get_variable_instance() { /// // Utiliser var.value() pour lire/écrire /// println!("Paramètre {} = {}", arg.get_name(), var.value()); /// } else { /// return Err(ActionError::UnboundArgument(arg.get_name().to_string())); /// } /// } /// Ok(()) /// } /// ``` pub fn get_variable_instance(&self) -> Option> { self.variable_instance.read().unwrap().clone() } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_i64.rs` ```rust use crate::variable_types::{StateValue, StateValueError}; use std::convert::TryFrom; impl TryFrom<&StateValue> for i64 { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { // signés StateValue::I1(v) => Ok(*v as i64), StateValue::I2(v) => Ok(*v as i64), StateValue::I4(v) => Ok(*v as i64), StateValue::Int(v) => Ok(*v as i64), // non signés StateValue::UI1(v) => Ok(*v as i64), StateValue::UI2(v) => Ok(*v as i64), StateValue::UI4(v) => Ok(*v as i64), // toujours dans l'intervalle d'un i64 // booléen StateValue::Boolean(v) => Ok(*v as i64), // chaîne → i64 StateValue::String(s) => s .parse::() .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i64", s))), _ => Err(StateValueError::TypeError("Cannot cast to i64".into())), } } } impl TryFrom for i64 { type Error = StateValueError; fn try_from(value: StateValue) -> Result { i64::try_from(&value) } } impl From for StateValue { fn from(value: i64) -> Self { StateValue::Int(value as i32) // ⚠️ choix à discuter : Int est i32, pas i64 } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_f32.rs` ```rust use std::convert::TryFrom; use crate::variable_types::{StateValue, StateValueError}; impl TryFrom<&StateValue> for f32 { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { const MAX_EXACT: i32 = 1 << f32::MANTISSA_DIGITS; match value { // --- Signed integers --- StateValue::I1(v) => Ok(*v as f32), StateValue::I2(v) => Ok(*v as f32), StateValue::I4(v) if *v > -MAX_EXACT && *v < MAX_EXACT => Ok(*v as f32), StateValue::Int(v) if *v >= -MAX_EXACT && *v <= MAX_EXACT as i32 => Ok(*v as f32), // --- Unsigned integers --- StateValue::UI1(v) => Ok(*v as f32), StateValue::UI2(v) => Ok(*v as f32), StateValue::UI4(v) if *v <= MAX_EXACT as u32 => Ok(*v as f32), StateValue::UI4(_) => Err(StateValueError::TypeError( "Cannot cast UI4 to f32: out of range".into(), )), // --- Floats --- StateValue::R4(v) => Ok(*v), // déjà un f32 StateValue::R8(v) if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) => { Ok(*v as f32) } StateValue::R8(_) => Err(StateValueError::TypeError( "Cannot cast R8 to f32: out of range".into(), )), StateValue::Number(v) if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) => { Ok(*v as f32) } StateValue::Number(_) => Err(StateValueError::TypeError( "Cannot cast Number to f32: out of range".into(), )), StateValue::Fixed14_4(v) if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) => { Ok(*v as f32) } StateValue::Fixed14_4(_) => Err(StateValueError::TypeError( "Cannot cast Fixed14_4 to f32: out of range".into(), )), StateValue::Boolean(v) => Ok((*v as i32) as Self), StateValue::String(s) => s .parse::() .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as f32", s))), // --- Par défaut : erreur --- _ => Err(StateValueError::TypeError("Cannot cast to f32".into())), } } } impl TryFrom for f32 { type Error = StateValueError; fn try_from(value: StateValue) -> Result { f32::try_from(&value) } } impl From for StateValue { fn from(value: f32) -> Self { StateValue::R4(value) } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_naivedate.rs` ```rust use crate::variable_types::{StateValue, StateValueError}; use chrono::NaiveDate; use std::convert::TryFrom; impl TryFrom<&StateValue> for NaiveDate { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { StateValue::Date(v) => Ok(v.clone()), StateValue::String(v) => NaiveDate::parse_from_str(v, "%Y-%m-%d").map_err(|e| { StateValueError::ParseError(format!("Cannot parse Date from string '{}': {}", v, e)) }), _ => Err(StateValueError::TypeError( "Cannot cast to NaiveDate".into(), )), } } } impl TryFrom for NaiveDate { type Error = StateValueError; fn try_from(value: StateValue) -> Result { NaiveDate::try_from(&value) } } impl From for StateValue { fn from(value: NaiveDate) -> Self { StateValue::Date(value) } } ``` ## fichier: `pmoupnp/src/variable_types/value_methods.rs` ```rust use std::cmp::Ordering; use crate::variable_types::{StateValue, StateVarType, type_trait::UpnpVarType}; impl UpnpVarType for StateValue { fn as_state_var_type(&self) -> StateVarType { StateVarType::from(self) // utilise ton From<&StateValue> existant } } impl PartialEq for StateValue { fn eq(&self, other: &Self) -> bool { match (self, other) { (a, b) if a.is_integer() && b.is_integer() => { if let (Ok(ia), Ok(ib)) = (i64::try_from(a), i64::try_from(b)) { return ia == ib; }; return false; } (a, b) if a.is_float() && b.is_float() => { if let (Ok(a), Ok(b)) = (f64::try_from(self), f64::try_from(other)) { return a == b; // NaN respecte la sémantique IEEE (NaN != NaN) } return false; } (a, b) if a.is_string() && b.is_string() => { let (a, b) = (self.to_string(), other.to_string()); return a == b; // NaN respecte la sémantique IEEE (NaN != NaN) } (StateValue::Date(a), StateValue::Date(b)) => { return a == b; } (StateValue::Time(a), StateValue::Time(b)) => { return a == b; } (StateValue::DateTime(a), StateValue::DateTime(b)) => { return a == b; } (StateValue::DateTimeTZ(a), StateValue::DateTimeTZ(b)) => { return a == b; } (StateValue::TimeTZ(a), StateValue::TimeTZ(b)) => { return a == b; } (_, _) => return false, } } } impl PartialOrd for StateValue { fn partial_cmp(&self, other: &Self) -> Option { match (self, other) { (a, b) if a.is_integer() && b.is_integer() => { if let (Ok(ia), Ok(ib)) = (i64::try_from(a), i64::try_from(b)) { return Some(ia.cmp(&ib)); }; return None; } (a, b) if a.is_float() && b.is_float() => { if let (Ok(a), Ok(b)) = (f64::try_from(self), f64::try_from(other)) { return a.partial_cmp(&b); // NaN respecte la sémantique IEEE (NaN != NaN) } return None; } (a, b) if a.is_string() && b.is_string() => { let (a, b) = (self.to_string(), other.to_string()); return Some(a.cmp(&b)); // NaN respecte la sémantique IEEE (NaN != NaN) } (StateValue::Date(a), StateValue::Date(b)) => { return Some(a.cmp(&b)); } (StateValue::Time(a), StateValue::Time(b)) => { return Some(a.cmp(&b)); } (StateValue::DateTime(a), StateValue::DateTime(b)) => { return Some(a.cmp(&b)); } (StateValue::DateTimeTZ(a), StateValue::DateTimeTZ(b)) => { return Some(a.cmp(&b)); } (StateValue::TimeTZ(a), StateValue::TimeTZ(b)) => { return Some(a.cmp(&b)); } (_, _) => return None, } } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_type.rs` ```rust use crate::variable_types::{StateValue, StateVarType}; impl From<&StateValue> for StateVarType { fn from(value: &StateValue) -> Self { match value { StateValue::UI1(_) => StateVarType::UI1, StateValue::UI2(_) => StateVarType::UI2, StateValue::UI4(_) => StateVarType::UI4, StateValue::I1(_) => StateVarType::I1, StateValue::I2(_) => StateVarType::I2, StateValue::I4(_) => StateVarType::I4, StateValue::Int(_) => StateVarType::Int, StateValue::R4(_) => StateVarType::R4, StateValue::R8(_) => StateVarType::R8, StateValue::Number(_) => StateVarType::Number, StateValue::Fixed14_4(_) => StateVarType::Fixed14_4, StateValue::Char(_) => StateVarType::Char, StateValue::String(_) => StateVarType::String, StateValue::BinBase64(_) => StateVarType::BinBase64, StateValue::BinHex(_) => StateVarType::BinHex, StateValue::URI(_) => StateVarType::URI, StateValue::UUID(_) => StateVarType::UUID, StateValue::Date(_) => StateVarType::Date, StateValue::DateTime(_) => StateVarType::DateTime, StateValue::DateTimeTZ(_) => StateVarType::DateTimeTZ, StateValue::Time(_) => StateVarType::Time, StateValue::TimeTZ(_) => StateVarType::TimeTZ, StateValue::Boolean(_) => StateVarType::Boolean, } } } impl From for StateVarType { fn from(value: StateValue) -> Self { StateVarType::from(&value) } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_datetime.rs` ```rust use crate::variable_types::{StateValue, StateValueError}; use chrono::{DateTime, FixedOffset}; use std::convert::TryFrom; impl TryFrom<&StateValue> for DateTime { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { StateValue::DateTimeTZ(v) => Ok(v.clone()), StateValue::TimeTZ(v) => Ok(v.clone()), StateValue::String(v) => DateTime::parse_from_rfc3339(v).map_err(|e| { StateValueError::ParseError(format!( "Cannot parse DateTimeTZ from string '{}': {}", v, e )) }), _ => Err(StateValueError::TypeError( "Cannot cast to DateTime".into(), )), } } } impl TryFrom for DateTime { type Error = StateValueError; fn try_from(value: StateValue) -> Result { DateTime::::try_from(&value) } } impl From> for StateValue { fn from(value: DateTime) -> Self { StateValue::DateTimeTZ(value) } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_u16.rs` ```rust use std::convert::TryFrom; use crate::variable_types::{StateValue, StateValueError}; // Implémentations TryFrom pour types numériques impl TryFrom<&StateValue> for u16 { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { StateValue::UI1(v) => Ok(*v as Self), StateValue::UI2(v) => Ok(*v), StateValue::UI4(v) if *v <= i16::MAX as u32 => Ok(*v as Self), StateValue::I1(v) if *v >= 0 => Ok(*v as Self), StateValue::I2(v) if *v >= 0 => Ok(*v as Self), StateValue::I4(v) if *v >= 0 && *v <= u16::MAX as i32 => Ok(*v as Self), StateValue::Int(v) if *v >= 0 && *v <= u16::MAX as i32 => Ok(*v as Self), StateValue::Boolean(v) => Ok(*v as Self), StateValue::String(s) => s .parse::() .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u16", s))), _ => Err(StateValueError::TypeError("Cannot cast to u16".into())), } } } impl TryFrom for u16 { type Error = StateValueError; fn try_from(value: StateValue) -> Result { u16::try_from(&value) } } impl From for StateValue { fn from(value: u16) -> Self { StateValue::UI2(value) } } ``` ## fichier: `pmoupnp/src/variable_types/fromstr.rs` ```rust use crate::variable_types::StateVarType; use std::str::FromStr; impl FromStr for StateVarType { type Err = String; // Type d'erreur personnalisé fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "ui1" => Ok(StateVarType::UI1), "ui2" => Ok(StateVarType::UI2), "ui4" => Ok(StateVarType::UI4), "i1" => Ok(StateVarType::I1), "i2" => Ok(StateVarType::I2), "i4" => Ok(StateVarType::I4), "int" => Ok(StateVarType::Int), "r4" => Ok(StateVarType::R4), "r8" => Ok(StateVarType::R8), "number" => Ok(StateVarType::Number), "fixed.14.4" => Ok(StateVarType::Fixed14_4), "char" => Ok(StateVarType::Char), "string" => Ok(StateVarType::String), "boolean" => Ok(StateVarType::Boolean), "bin.base64" => Ok(StateVarType::BinBase64), "bin.hex" => Ok(StateVarType::BinHex), "date" => Ok(StateVarType::Date), "datetime" => Ok(StateVarType::DateTime), "datetime.tz" => Ok(StateVarType::DateTimeTZ), "time" => Ok(StateVarType::Time), "time.tz" => Ok(StateVarType::TimeTZ), "uuid" => Ok(StateVarType::UUID), "uri" => Ok(StateVarType::URI), _ => Err(format!("Type inconnu: {}", s)), } } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_u32.rs` ```rust use std::convert::TryFrom; use crate::variable_types::{StateValue, StateValueError}; impl TryFrom<&StateValue> for u32 { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { StateValue::UI1(v) => Ok(*v as Self), StateValue::UI2(v) => Ok(*v as Self), StateValue::UI4(v) => Ok(*v), StateValue::I1(v) if *v >= 0 => Ok(*v as Self), StateValue::I2(v) if *v >= 0 => Ok(*v as Self), StateValue::I4(v) if *v >= 0 => Ok(*v as Self), StateValue::Int(v) if *v >= 0 => Ok(*v as Self), StateValue::Boolean(v) => Ok(*v as Self), StateValue::String(s) => s .parse::() .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u32", s))), _ => Err(StateValueError::TypeError("Cannot cast to u32".into())), } } } impl TryFrom for u32 { type Error = StateValueError; fn try_from(value: StateValue) -> Result { u32::try_from(&value) } } impl From for StateValue { fn from(value: u32) -> Self { StateValue::UI4(value) } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_vec_u8.rs` ```rust use crate::variable_types::{StateValue, StateValueError}; use base64::{Engine as _, engine::general_purpose::STANDARD}; use std::convert::TryFrom; impl TryFrom<&StateValue> for Vec { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { // Déjà un vecteur binaire StateValue::BinBase64(v) => STANDARD .decode(v) .map_err(|e| StateValueError::ParseError(format!("Base64 decode error: {}", e))), StateValue::BinHex(v) => hex::decode(v) .map_err(|e| StateValueError::ParseError(format!("BinHex decode error: {}", e))), // Conversion depuis une chaîne encodée StateValue::String(s) => { // Essayer Base64 if let Ok(bytes) = STANDARD.decode(s) { return Ok(bytes); } // Essayer Hex if let Ok(bytes) = hex::decode(s) { return Ok(bytes); } Err(StateValueError::ParseError(format!( "Cannot parse string '{}' as binary", s ))) } _ => Err(StateValueError::TypeError( "Cannot cast to binary Vec".into(), )), } } } impl TryFrom for Vec { type Error = StateValueError; fn try_from(value: StateValue) -> Result { Vec::::try_from(&value) } } ``` ## fichier: `pmoupnp/src/variable_types/default_value.rs` ```rust use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime}; use url::Url; use uuid::Uuid; use crate::variable_types::{StateValue, StateVarType}; impl StateVarType { pub fn default_value(&self) -> StateValue { match self { StateVarType::UI1 => StateValue::UI1(0), StateVarType::UI2 => StateValue::UI2(0), StateVarType::UI4 => StateValue::UI4(0), StateVarType::I1 => StateValue::I1(0), StateVarType::I2 => StateValue::I2(0), StateVarType::I4 => StateValue::I4(0), StateVarType::Int => StateValue::Int(0), StateVarType::R4 => StateValue::R4(0.0), StateVarType::R8 => StateValue::R8(0.0), StateVarType::Number => StateValue::Number(0.0), StateVarType::Fixed14_4 => StateValue::Fixed14_4(0.0), StateVarType::Char => StateValue::Char('\0'), StateVarType::String => StateValue::String(String::new()), StateVarType::Boolean => StateValue::Boolean(false), StateVarType::BinBase64 => StateValue::BinBase64(String::new()), StateVarType::BinHex => StateValue::BinHex(String::new()), StateVarType::Date => StateValue::Date(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()), StateVarType::DateTime => { StateValue::DateTime(DateTime::from_timestamp(0, 0).unwrap().naive_utc().into()) } StateVarType::DateTimeTZ => { StateValue::DateTimeTZ(DateTime::from_naive_utc_and_offset( DateTime::from_timestamp(0, 0).unwrap().naive_utc(), FixedOffset::east_opt(0).unwrap(), )) } StateVarType::Time => StateValue::Time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()), StateVarType::TimeTZ => StateValue::TimeTZ(DateTime::from_naive_utc_and_offset( DateTime::from_timestamp(0, 0).unwrap().naive_utc(), FixedOffset::east_opt(0).unwrap(), )), StateVarType::UUID => StateValue::UUID(Uuid::nil()), StateVarType::URI => StateValue::URI(Url::parse("http://localhost").unwrap()), } } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_str.rs` ```rust use std::convert::TryFrom; use crate::variable_types::{StateValue, StateValueError}; impl TryFrom<&str> for StateValue { type Error = StateValueError; fn try_from(s: &str) -> Result { Ok(StateValue::String(s.to_string())) } } // Conversion depuis String impl TryFrom for StateValue { type Error = StateValueError; fn try_from(s: String) -> Result { Ok(StateValue::String(s)) } }``` ## fichier: `pmoupnp/src/variable_types/display_value.rs` ```rust use base64::Engine; use base64::engine::general_purpose; use std::fmt; use crate::variable_types::StateValue; impl fmt::Display for StateValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { // Numériques StateValue::UI1(v) => write!(f, "{}", v), StateValue::UI2(v) => write!(f, "{}", v), StateValue::UI4(v) => write!(f, "{}", v), StateValue::I1(v) => write!(f, "{}", v), StateValue::I2(v) => write!(f, "{}", v), StateValue::I4(v) => write!(f, "{}", v), StateValue::Int(v) => write!(f, "{}", v), StateValue::R4(v) => write!(f, "{}", v), StateValue::R8(v) => write!(f, "{}", v), StateValue::Number(v) => write!(f, "{}", v), StateValue::Fixed14_4(v) => write!(f, "{}", v), // Types déjà Display StateValue::Char(v) => write!(f, "{}", v), StateValue::String(v) => write!(f, "{}", v), StateValue::UUID(v) => write!(f, "{}", v), StateValue::URI(v) => write!(f, "{}", v), // Booléen : 1 ou 0 StateValue::Boolean(v) => write!(f, "{}", if *v { "1" } else { "0" }), // Encodages binaires StateValue::BinBase64(v) => write!(f, "{}", general_purpose::URL_SAFE.encode(v)), StateValue::BinHex(v) => write!(f, "{}", hex::encode(v)), // Dates et temps StateValue::Date(v) => write!(f, "{}", v.format("%Y-%m-%d")), StateValue::DateTime(v) => write!(f, "{}", v.format("%Y-%m-%dT%H:%M:%S")), StateValue::DateTimeTZ(v) => write!(f, "{}", v.to_rfc3339()), StateValue::Time(v) => write!(f, "{}", v.format("%H:%M:%S")), StateValue::TimeTZ(v) => write!(f, "{}", v.format("%H:%M:%S%z")), } } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_i8.rs` ```rust use std::convert::TryFrom; use crate::variable_types::{StateValue, StateValueError}; // Implémentations TryFrom pour types numériques impl TryFrom<&StateValue> for i8 { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { StateValue::I1(v) if *v >= 0 => Ok(*v as i8), StateValue::I2(v) if *v <= i8::MAX as i16 && *v >= i8::MIN as i16 => Ok(*v as i8), StateValue::I4(v) if *v <= i8::MAX as i32 && *v >= i8::MIN as i32 => Ok(*v as i8), StateValue::Int(v) if *v <= i8::MAX as i32 && *v >= i8::MIN as i32 => Ok(*v as i8), StateValue::UI1(v) if *v <= i8::MAX as u8 => Ok(*v as i8), StateValue::UI2(v) if *v <= i8::MAX as u16 => Ok(*v as i8), StateValue::UI4(v) if *v <= i8::MAX as u32 => Ok(*v as i8), StateValue::Boolean(v) => Ok(*v as Self), StateValue::String(s) => s .parse::() .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i8", s))), _ => Err(StateValueError::TypeError("Cannot cast to i8".into())), } } } impl TryFrom for i8 { type Error = StateValueError; fn try_from(value: StateValue) -> Result { i8::try_from(&value) } } impl From for StateValue { fn from(value: i8) -> Self { StateValue::I1(value) } } ``` ## fichier: `pmoupnp/src/variable_types/cast.rs` ```rust use crate::variable_types::{StateValue, StateValueError, StateVarType}; use std::convert::TryFrom; impl StateValue { pub fn try_cast(&self, target: StateVarType) -> Result { let source = StateVarType::from(self); // Identité (même type) if source == target { return Ok(self.clone()); } match (self, target) { (val, StateVarType::String) => Ok(StateValue::String(val.to_string())), (_, StateVarType::UI1) => Ok(StateValue::UI1(u8::try_from(self)?)), (_, StateVarType::UI2) => Ok(StateValue::UI2(u16::try_from(self)?)), (_, StateVarType::UI4) => Ok(StateValue::UI4(u32::try_from(self)?)), (_, StateVarType::I1) => Ok(StateValue::I1(i8::try_from(self)?)), (_, StateVarType::I2) => Ok(StateValue::I2(i16::try_from(self)?)), (_, StateVarType::I4) => Ok(StateValue::I4(i32::try_from(self)?)), (_, StateVarType::Int) => Ok(StateValue::Int(i32::try_from(self)?)), (_, StateVarType::R8) => Ok(StateValue::R8(f64::try_from(self)?)), (_, StateVarType::Number) => Ok(StateValue::Number(f64::try_from(self)?)), (_, StateVarType::Fixed14_4) => Ok(StateValue::Fixed14_4(f64::try_from(self)?)), (_, StateVarType::R4) => Ok(StateValue::R4(f32::try_from(self)?)), // --- Pas encore implémenté pour les autres types --- (val, target) => Err(StateValueError::TypeError(format!( "Cannot cast {:?} to {:?}", val, target ))), } } } ``` ## fichier: `pmoupnp/src/variable_types/type_methods.rs` ```rust use crate::variable_types::{StateVarType, type_trait::UpnpVarType}; impl UpnpVarType for StateVarType { fn as_state_var_type(&self) -> StateVarType { *self } fn bit_size(&self) -> Option { match self { StateVarType::UI1 | StateVarType::I1 => Some(8), StateVarType::UI2 | StateVarType::I2 => Some(16), StateVarType::UI4 | StateVarType::I4 | StateVarType::Int | StateVarType::R4 => Some(32), StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => Some(64), _ => None, } } fn is_numeric(&self) -> bool { matches!( self, StateVarType::UI1 | StateVarType::UI2 | StateVarType::UI4 | StateVarType::I1 | StateVarType::I2 | StateVarType::I4 | StateVarType::Int | StateVarType::R4 | StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 ) } fn is_integer(&self) -> bool { matches!( self, StateVarType::UI1 | StateVarType::UI2 | StateVarType::UI4 | StateVarType::I1 | StateVarType::I2 | StateVarType::I4 | StateVarType::Int ) } fn is_signed_int(&self) -> bool { matches!( self, StateVarType::I1 | StateVarType::I2 | StateVarType::I4 | StateVarType::Int ) } fn is_unsigned_int(&self) -> bool { matches!( self, StateVarType::UI1 | StateVarType::UI2 | StateVarType::UI4 ) } fn is_float(&self) -> bool { matches!( self, StateVarType::R4 | StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 ) } fn is_bool(&self) -> bool { matches!(self, StateVarType::Boolean) } fn is_string(&self) -> bool { matches!( self, StateVarType::String | StateVarType::Char | StateVarType::BinHex | StateVarType::BinBase64 ) } fn is_time(&self) -> bool { matches!( self, StateVarType::Date | StateVarType::DateTime | StateVarType::DateTimeTZ | StateVarType::Time | StateVarType::TimeTZ ) } fn is_uuid(&self) -> bool { matches!(self, StateVarType::UUID) } fn is_uri(&self) -> bool { matches!(self, StateVarType::URI) } fn is_binary(&self) -> bool { matches!(self, StateVarType::BinBase64 | StateVarType::BinHex) } fn is_comparable(&self) -> bool { !self.is_binary() } } ``` ## fichier: `pmoupnp/src/variable_types/mod.rs` ```rust mod cast; mod default_value; mod display_type; mod display_value; mod errors; mod fromstr; mod type_methods; mod type_trait; mod value_methods; mod value_trait; mod values_from_type; mod values_from_i16; mod values_from_i32; mod values_from_i64; mod values_from_i8; mod values_from_u16; mod values_from_u32; mod values_from_u8; mod values_from_f32; mod values_from_f64; mod values_from_datetime; mod values_from_naivedate; mod values_from_naivedatetime; mod values_from_naivetime; mod values_from_uri; mod values_from_uuid; mod values_from_vec_u8; mod values_from_str; use std::fmt::Debug; use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime}; use url::Url; 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 UI2, // Unsigned 16-bit integer UI4, // Unsigned 32-bit integer I1, // Signed 8-bit integer I2, // Signed 16-bit integer I4, // Signed 32-bit integer Int, // Synonymous with i4 R4, // 32-bit floating point R8, // 64-bit floating point Number, // Synonymous with r8 Fixed14_4, // Fixed-point decimal Char, // Single Unicode character String, // Character string Boolean, // Boolean value BinBase64, // Base64-encoded binary BinHex, // Hex-encoded binary Date, // Date (YYYY-MM-DD) DateTime, // DateTime without timezone DateTimeTZ, // DateTime with timezone Time, // Time without timezone TimeTZ, // Time with timezone UUID, // Universally unique identifier URI, // Uniform Resource Identifier } #[derive(Clone, Debug)] pub enum StateValue { UI1(u8), UI2(u16), UI4(u32), I1(i8), I2(i16), I4(i32), Int(i32), R4(f32), R8(f64), Number(f64), Fixed14_4(f64), Char(char), String(String), Boolean(bool), BinBase64(String), BinHex(String), Date(NaiveDate), DateTime(NaiveDateTime), DateTimeTZ(DateTime), Time(NaiveTime), TimeTZ(DateTime), UUID(Uuid), URI(Url), } ``` ## fichier: `pmoupnp/src/variable_types/values_from_u8.rs` ```rust use std::convert::TryFrom; use crate::variable_types::{StateValue, StateValueError}; // Implémentations TryFrom pour types numériques impl TryFrom<&StateValue> for u8 { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { StateValue::UI1(v) => Ok(*v), StateValue::UI2(v) if *v <= u8::MAX as u16 => Ok(*v as u8), StateValue::UI4(v) if *v <= i8::MAX as u32 => Ok(*v as u8), StateValue::I1(v) if *v >= 0 => Ok(*v as u8), StateValue::I2(v) if *v >= 0 && *v <= u8::MAX as i16 => Ok(*v as u8), StateValue::I4(v) if *v >= 0 && *v <= u8::MAX as i32 => Ok(*v as u8), StateValue::Int(v) if *v >= 0 && *v <= u8::MAX as i32 => Ok(*v as u8), StateValue::Boolean(v) => Ok(*v as Self), StateValue::String(s) => s .parse::() .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u8", s))), _ => Err(StateValueError::TypeError("Cannot cast to u8".into())), } } } impl TryFrom for u8 { type Error = StateValueError; fn try_from(value: StateValue) -> Result { u8::try_from(&value) } } impl From for StateValue { fn from(value: u8) -> Self { StateValue::UI1(value) } } ``` ## fichier: `pmoupnp/src/variable_types/errors.rs` ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum StateValueError { #[error("Conversion error: {0}")] ConversionError(String), #[error("Validation error: {0}")] ValidationError(String), #[error("Range error: {0}")] RangeError(String), #[error("Type error: {0}")] TypeError(String), #[error("Parse error: {0}")] ParseError(String), #[error("Event condition error: {0}")] EventConditionError(String), #[error("Arithmetic error: {0}")] ArithmeticError(String), #[error("Unknown error: {0}")] Unknown(String), } ``` ## fichier: `pmoupnp/src/variable_types/display_type.rs` ```rust use std::fmt; use crate::variable_types::StateVarType; impl fmt::Display for StateVarType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = match self { StateVarType::UI1 => "ui1", StateVarType::UI2 => "ui2", StateVarType::UI4 => "ui4", StateVarType::I1 => "i1", StateVarType::I2 => "i2", StateVarType::I4 => "i4", StateVarType::Int => "int", StateVarType::R4 => "r4", StateVarType::R8 => "r8", StateVarType::Number => "number", StateVarType::Fixed14_4 => "fixed.14.4", StateVarType::Char => "char", StateVarType::String => "string", StateVarType::Boolean => "boolean", StateVarType::BinBase64 => "bin.base64", StateVarType::BinHex => "bin.hex", StateVarType::Date => "date", StateVarType::DateTime => "dateTime", StateVarType::DateTimeTZ => "dateTime.tz", StateVarType::Time => "time", StateVarType::TimeTZ => "time.tz", StateVarType::UUID => "uuid", StateVarType::URI => "uri", }; write!(f, "{}", s) } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_uuid.rs` ```rust use std::convert::TryFrom; use uuid::Uuid; use crate::variable_types::{StateValue, StateValueError}; impl TryFrom for Uuid { type Error = StateValueError; fn try_from(value: StateValue) -> Result { match value { // Si déjà un URI encodé comme StateValue::URI StateValue::UUID(v) => Ok(v), // Si c'est une String, on tente un parse StateValue::String(v) => Uuid::parse_str(&v) .map_err(|_| StateValueError::TypeError("Invalid UUID string".into())), // Autres types : erreur _ => Err(StateValueError::TypeError("Cannot cast to Uuid".into())), } } } impl From for StateValue { fn from(value: Uuid) -> Self { StateValue::UUID(value) } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_uri.rs` ```rust use std::convert::TryFrom; use url::Url; use crate::variable_types::{StateValue, StateValueError}; impl TryFrom for Url { type Error = StateValueError; fn try_from(value: StateValue) -> Result { match value { // Si déjà un URI encodé comme StateValue::URI StateValue::URI(v) => Ok(v), // Si c'est une String, on tente un parse StateValue::String(v) => { Url::parse(&v).map_err(|_| StateValueError::TypeError("Invalid URI string".into())) } // Autres types : erreur _ => Err(StateValueError::TypeError("Cannot cast to Url".into())), } } } impl From for StateValue { fn from(value: Url) -> Self { StateValue::URI(value) } } ``` ## fichier: `pmoupnp/src/variable_types/value_trait.rs` ```rust pub trait UpnpValue: Clone {} ``` ## fichier: `pmoupnp/src/variable_types/values_from_f64.rs` ```rust use std::convert::TryFrom; use crate::variable_types::{StateValue, StateValueError}; impl TryFrom<&StateValue> for f64 { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { // --- Signed integers --- StateValue::I1(v) => Ok(*v as f64), StateValue::I2(v) => Ok(*v as f64), StateValue::I4(v) => Ok(*v as f64), StateValue::Int(v) => Ok(*v as f64), // --- Unsigned integers --- StateValue::UI1(v) => Ok(*v as f64), StateValue::UI2(v) => Ok(*v as f64), StateValue::UI4(v) => Ok(*v as f64), // --- Floats --- StateValue::R4(v) => Ok(*v as f64), StateValue::R8(v) => Ok(*v), StateValue::Number(v) => Ok(*v), StateValue::Fixed14_4(v) => Ok(*v), StateValue::Boolean(v) => Ok((*v as i32) as Self), StateValue::String(s) => s .parse::() .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as f64", s))), // --- Par défaut : erreur --- _ => Err(StateValueError::TypeError("Cannot cast to f64".into())), } } } impl TryFrom for f64 { type Error = StateValueError; fn try_from(value: StateValue) -> Result { f64::try_from(&value) } } impl From for StateValue { fn from(value: f64) -> Self { StateValue::R8(value) } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_naivedatetime.rs` ```rust use crate::variable_types::{StateValue, StateValueError}; use chrono::NaiveDateTime; use std::convert::TryFrom; impl TryFrom<&StateValue> for NaiveDateTime { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { StateValue::DateTime(v) => Ok(v.clone()), StateValue::String(v) => NaiveDateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S") .map_err(|e| { StateValueError::ParseError(format!( "Cannot parse DateTime from string '{}': {}", v, e )) }), _ => Err(StateValueError::TypeError( "Cannot cast to NaiveDateTime".into(), )), } } } impl TryFrom for NaiveDateTime { type Error = StateValueError; fn try_from(value: StateValue) -> Result { NaiveDateTime::try_from(&value) } } impl From for StateValue { fn from(value: NaiveDateTime) -> Self { StateValue::DateTime(value) } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_i32.rs` ```rust use std::convert::TryFrom; use crate::variable_types::{StateValue, StateValueError}; impl TryFrom<&StateValue> for i32 { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { // signés StateValue::I1(v) => Ok(*v as i32), StateValue::I2(v) => Ok(*v as i32), StateValue::I4(v) => Ok(*v), StateValue::Int(v) => Ok(*v), // non signés StateValue::UI1(v) => Ok(*v as i32), StateValue::UI2(v) => Ok(*v as i32), StateValue::UI4(v) if *v <= i32::MAX as u32 => Ok(*v as i32), StateValue::Boolean(v) => Ok(*v as Self), StateValue::String(s) => s .parse::() .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i32", s))), _ => Err(StateValueError::TypeError("Cannot cast to i32".into())), } } } impl TryFrom for i32 { type Error = StateValueError; fn try_from(value: StateValue) -> Result { i32::try_from(&value) } } impl From for StateValue { fn from(value: i32) -> Self { StateValue::I4(value) } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_naivetime.rs` ```rust use crate::variable_types::{StateValue, StateValueError}; use chrono::NaiveTime; use std::convert::TryFrom; impl TryFrom<&StateValue> for NaiveTime { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { StateValue::Time(v) => Ok(v.clone()), StateValue::String(v) => NaiveTime::parse_from_str(&v, "%H:%M:%S").map_err(|e| { StateValueError::ParseError(format!("Cannot parse Time from string '{}': {}", v, e)) }), _ => Err(StateValueError::TypeError( "Cannot cast to NaiveTime".into(), )), } } } impl TryFrom for NaiveTime { type Error = StateValueError; fn try_from(value: StateValue) -> Result { NaiveTime::try_from(&value) } } impl From for StateValue { fn from(value: NaiveTime) -> Self { StateValue::Time(value) } } ``` ## fichier: `pmoupnp/src/variable_types/values_from_i16.rs` ```rust use std::convert::TryFrom; use crate::variable_types::{StateValue, StateValueError}; // Implémentations TryFrom pour types numériques impl TryFrom<&StateValue> for i16 { type Error = StateValueError; fn try_from(value: &StateValue) -> Result { match value { StateValue::I1(v) => Ok(*v as i16), StateValue::I2(v) => Ok(*v), StateValue::I4(v) if *v <= i16::MAX as i32 && *v >= i16::MIN as i32 => Ok(*v as i16), StateValue::Int(v) if *v <= i16::MAX as i32 && *v >= i16::MIN as i32 => Ok(*v as i16), StateValue::UI1(v) => Ok(*v as i16), StateValue::UI2(v) if *v <= i16::MAX as u16 => Ok(*v as i16), StateValue::UI4(v) if *v <= i16::MAX as u32 => Ok(*v as i16), StateValue::Boolean(v) => Ok(*v as Self), StateValue::String(s) => s .parse::() .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i16", s))), _ => Err(StateValueError::TypeError("Cannot cast to i32".into())), } } } impl TryFrom for i16 { type Error = StateValueError; fn try_from(value: StateValue) -> Result { i16::try_from(&value) } } impl From for StateValue { fn from(value: i16) -> Self { StateValue::I2(value) } } ``` ## fichier: `pmoupnp/src/variable_types/type_trait.rs` ```rust use crate::variable_types::StateVarType; pub trait UpnpVarType { fn as_state_var_type(&self) -> StateVarType; fn bit_size(&self) -> Option { self.as_state_var_type().bit_size() } fn is_numeric(&self) -> bool { self.as_state_var_type().is_numeric() } fn is_integer(&self) -> bool { self.as_state_var_type().is_integer() } fn is_signed_int(&self) -> bool { self.as_state_var_type().is_signed_int() } fn is_unsigned_int(&self) -> bool { self.as_state_var_type().is_unsigned_int() } fn is_float(&self) -> bool { self.as_state_var_type().is_float() } fn is_bool(&self) -> bool { self.as_state_var_type().is_bool() } fn is_string(&self) -> bool { self.as_state_var_type().is_string() } fn is_time(&self) -> bool { self.as_state_var_type().is_time() } fn is_uuid(&self) -> bool { self.as_state_var_type().is_uuid() } fn is_uri(&self) -> bool { self.as_state_var_type().is_uri() } fn is_binary(&self) -> bool { self.as_state_var_type().is_binary() } fn is_comparable(&self) -> bool { self.as_state_var_type().is_comparable() } } ``` ## fichier: `pmoupnp/src/object_trait.rs` ```rust //! ## 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; /// 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. 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 /// // /// ``` fn to_xml(&self) -> String { let elem = self.to_xml_element(); let config = EmitterConfig::new() .perform_indent(true) .indent_string(" "); let mut buf = Vec::new(); elem.write_with_config(&mut buf, config) .expect("Failed to write XML"); let mut xml_string = "\n".to_string(); xml_string.push_str(&String::from_utf8(buf).expect("Invalid UTF-8")); xml_string } /// 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` /// ``` fn to_markdown(&self) -> String { let elem = self.to_xml_element(); let mut md = String::new(); fn is_url(s: &str) -> bool { s.starts_with("http://") || s.starts_with("https://") || s.starts_with("urn:") } fn is_image_url(s: &str) -> bool { let s = s.to_lowercase(); s.ends_with(".png") || s.ends_with(".jpg") || s.ends_with(".jpeg") || s.ends_with(".gif") || s.ends_with(".svg") || s.ends_with(".webp") } fn format_value(v: &str) -> String { let v = v.trim().to_string(); if is_url(&v) { if is_image_url(&v) { format!("[{}]({})
![]({})", v, v, v) } else { format!("[{}]({})", v, v) } } else { format!("`{}`", v) } } fn recurse(elem: &xmltree::Element, md: &mut String, depth: usize) { let indent = " ".repeat(depth); md.push_str(&format!("{}- **{}**", indent, elem.name)); if !elem.attributes.is_empty() { let attrs: Vec = elem .attributes .iter() .map(|(k, v)| format!("{}={}", k, format_value(v))) .collect(); md.push_str(&format!(" ({})", attrs.join(", "))); } 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'); for child in &elem.children { if let xmltree::XMLNode::Element(child_elem) = child { recurse(child_elem, md, depth + 1); } } } md.push_str("# UPnP XML (Markdown view)\n\n"); recurse(&elem, &mut md, 0); md } /// 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 {} ``` ## fichier: `pmoupnp/src/services/service_instance.rs` ```rust //! Implémentation de ServiceInstance. use std::{ collections::HashMap, sync::{Arc, Mutex, RwLock}, time::Duration, }; use axum::{ extract::{Request, State}, http::{HeaderMap, StatusCode}, response::{IntoResponse, Response}, body::Body, }; use tokio::time; use tracing::{info, warn, error}; use xmltree::{Element, XMLNode, EmitterConfig}; use crate::{ services::{Service, ServiceError}, actions::{ActionInstance, ActionInstanceSet}, state_variables::{StateVarInstance, StateVarInstanceSet, UpnpVariable}, UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType, }; /// Méthodes HTTP pour les événements UPnP. pub const METHOD_SUBSCRIBE: &str = "SUBSCRIBE"; pub const METHOD_UNSUBSCRIBE: &str = "UNSUBSCRIBE"; /// Instance de service UPnP. /// /// Représente une instance concrète d'un service UPnP, attachée à un device. /// Gère l'exécution des actions, les notifications d'événements et les abonnements. /// /// # Fonctionnalités /// /// - Exécution d'actions via SOAP /// - Gestion des abonnements aux événements (SUBSCRIBE/UNSUBSCRIBE) /// - Notifications automatiques des changements d'état /// - Génération de la description SCPD /// /// # Cycle de vie /// /// 1. Création via [`Service::create_instance`](crate::UpnpModel::create_instance) /// 2. Enregistrement des URLs avec [`register_urls`](Self::register_urls) /// 3. Démarrage du notifier avec [`start_notifier`](Self::start_notifier) /// /// # Examples /// /// ```rust,no_run /// # use pmoupnp::services::Service; /// # use pmoupnp::server::Server; /// # use std::time::Duration; /// # #[tokio::main] /// # async fn main() { /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// /// // Enregistrer les endpoints /// let mut server = Server::new("test", "http://localhost:8080", 8080); /// instance.register_urls(&mut server).await.unwrap(); /// /// // Démarrer les notifications /// let _handle = instance.start_notifier(Duration::from_secs(5)); /// # } /// ``` #[derive(Clone)] pub struct ServiceInstance { /// Métadonnées de l'objet object: UpnpObjectType, /// Référence vers le modèle model: Arc, /// Identifiant du service identifier: String, /// Device parent (optionnel) device: Option>, /// Variables d'état instanciées statevariables: StateVarInstanceSet, /// Actions instanciées actions: ActionInstanceSet, /// Abonnés aux événements (SID -> Callback URL) subscribers: Arc>>, /// Buffer des changements en attente de notification changed_buffer: Arc>>, /// Compteurs de séquence par abonné seqid: Arc>>, } // Stub temporaire pour DeviceInstance // TODO: Remplacer par la vraie implémentation quand le module devices sera créé #[derive(Debug, Clone)] pub struct DeviceStub { name: String, udn: String, } impl DeviceStub { 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() } } impl std::fmt::Debug for ServiceInstance { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ServiceInstance") .field("object", &self.object) .field("identifier", &self.identifier) .field("device", &self.device) .field("statevariables", &self.statevariables) .field("actions", &self.actions) .finish() } } impl UpnpTyped for ServiceInstance { fn as_upnp_object_type(&self) -> &UpnpObjectType { &self.object } } impl UpnpInstance for ServiceInstance { type Model = Service; fn new(model: &Service) -> Self { // Phase 1 : Créer les instances de variables d'état let mut statevariables = StateVarInstanceSet::new(); for v in model.variables() { if let Err(e) = statevariables.insert(Arc::new(StateVarInstance::new(&*v))) { error!("Failed to insert state variable: {:?}", e); } } // Phase 2 : Créer les instances d'actions avec validation let mut actions = ActionInstanceSet::new(); for a in model.actions() { // Vérifier que toutes les variables référencées existent let mut missing_vars = Vec::new(); for arg in a.arguments().all() { let related_var_name = arg.state_variable().get_name(); if statevariables.get_by_name(related_var_name).is_none() { 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; } // Créer l'instance d'action let action_instance = Arc::new(ActionInstance::new(&*a)); // Phase 3 : Lier les arguments aux instances de variables // Note : Nécessite que ArgumentInstance ait une méthode bind_variable() // et que variable_instance soit dans un RwLock pour modification après création for arg_instance in action_instance.arguments_set().all() { let var_name = arg_instance.get_model().state_variable().get_name(); if let Some(var_instance) = statevariables.get_by_name(var_name) { // Appeler bind_variable() si elle existe // arg_instance.bind_variable(var_instance); // ⚠️ TODO: Cette ligne nécessite les modifications dans ArgumentInstance } } if let Err(e) = actions.insert(action_instance) { error!("Failed to insert action '{}': {:?}", a.get_name(), e); } } Self { object: UpnpObjectType { name: model.name().to_string(), object_type: "ServiceInstance".to_string(), }, model: Arc::new(model.clone()), identifier: model.identifier().to_string(), 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())), } } } impl UpnpTypedInstance for ServiceInstance { fn get_model(&self) -> &Self::Model { &self.model } } impl UpnpObject for ServiceInstance { fn to_xml_element(&self) -> Element { let mut elem = Element::new("service"); let mut service_type = Element::new("serviceType"); service_type.children.push(XMLNode::Text(self.service_type())); elem.children.push(XMLNode::Element(service_type)); let mut service_id = Element::new("serviceId"); service_id.children.push(XMLNode::Text(self.service_id())); elem.children.push(XMLNode::Element(service_id)); let mut scpd_url = Element::new("SCPDURL"); scpd_url.children.push(XMLNode::Text(self.scpd_url())); elem.children.push(XMLNode::Element(scpd_url)); let mut control_url = Element::new("controlURL"); control_url.children.push(XMLNode::Text(self.control_url())); elem.children.push(XMLNode::Element(control_url)); let mut event_sub_url = Element::new("eventSubURL"); event_sub_url.children.push(XMLNode::Text(self.event_sub_url())); elem.children.push(XMLNode::Element(event_sub_url)); elem } } impl ServiceInstance { /// Retourne l'identifiant du service. pub fn identifier(&self) -> &str { &self.identifier } /// Retourne le type de service UPnP. /// /// Format: `urn:schemas-upnp-org:service:{name}:{version}` pub fn service_type(&self) -> String { self.model.service_type() } /// Retourne l'ID de service UPnP. /// /// Format: `urn:upnp-org:serviceId:{identifier}` pub fn service_id(&self) -> String { format!("urn:upnp-org:serviceId:{}", self.identifier) } /// Retourne la route de base du service. pub fn base_route(&self) -> String { match &self.device { Some(device) => format!("{}/service/{}", device.base_route(), self.get_name()), None => format!("/service/{}", self.get_name()), } } /// Retourne l'URL de contrôle SOAP. pub fn control_url(&self) -> String { format!("{}/control", self.base_route()) } /// Retourne l'URL de souscription aux événements. pub fn event_sub_url(&self) -> String { format!("{}/event", self.base_route()) } /// Retourne l'URL de la description SCPD. pub fn scpd_url(&self) -> String { format!("{}/desc.xml", self.base_route()) } /// Retourne l'USN (Unique Service Name). 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()), } } /// Retourne les variables d'état. pub fn statevariables(&self) -> &StateVarInstanceSet { &self.statevariables } /// Retourne les actions. pub fn actions(&self) -> &ActionInstanceSet { &self.actions } /// Enregistre les routes UPnP dans le serveur Axum. /// /// # Errors /// /// Retourne une erreur si l'enregistrement des routes échoue. pub async fn register_urls(&self, server: &mut crate::server::Server) -> Result<(), ServiceError> { info!( "✅ Service description for {}:{} available at : {}{}", self.device.as_ref().map(|d| d.name()).unwrap_or("unknown"), self.get_name(), self.device.as_ref().map(|d| d.server_base_url()).unwrap_or_default(), self.scpd_url(), ); // Handler 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 control let instance_control = self.clone(); server.add_post_handler_with_state( &self.control_url(), control_handler, instance_control, ).await; // Handler é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) -> Element { let mut elem = Element::new("scpd"); elem.attributes.insert( "xmlns".to_string(), "urn:schemas-upnp-org:service-1-0".to_string(), ); // specVersion let mut spec = Element::new("specVersion"); let mut major = Element::new("major"); major.children.push(XMLNode::Text("1".to_string())); spec.children.push(XMLNode::Element(major)); let mut minor = Element::new("minor"); minor.children.push(XMLNode::Text("0".to_string())); spec.children.push(XMLNode::Element(minor)); elem.children.push(XMLNode::Element(spec)); // actionList if !self.actions.all().is_empty() { elem.children.push(XMLNode::Element( self.actions.to_xml_element() )); } // serviceStateTable if !self.statevariables.all().is_empty() { elem.children.push(XMLNode::Element( self.statevariables.to_xml_element() )); } elem } /// Handler pour la description SCPD. async fn scpd_handler(&self) -> Response { let elem = self.scpd_element(); let config = EmitterConfig::new() .perform_indent(true) .indent_string(" "); let mut xml_output = Vec::new(); if let Err(e) = elem.write_with_config(&mut xml_output, config) { error!("Failed to serialize SCPD XML: {}", e); return StatusCode::INTERNAL_SERVER_ERROR.into_response(); } let mut xml = String::from_utf8_lossy(&xml_output).to_string(); // Ajouter l'en-tête XML xml.insert_str(0, "\n"); ( StatusCode::OK, [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], xml, ).into_response() } /// Ajoute un abonné aux événements. pub async fn add_subscriber(&self, sid: String, callback: String) { let mut subscribers = self.subscribers.write().unwrap(); subscribers.insert(sid, callback); } /// Renouvelle un abonnement. pub async fn renew_subscriber(&self, sid: &str, timeout: &str) { info!("♻️ Renewed SID {} for timeout {}", sid, timeout); } /// Supprime un abonné. pub async fn remove_subscriber(&self, sid: &str) { let mut subscribers = self.subscribers.write().unwrap(); subscribers.remove(sid); } /// Envoie l'événement initial à un nouvel abonné. pub async fn send_initial_event(&self, sid: String) { let callback = { let subscribers = self.subscribers.read().unwrap(); subscribers.get(&sid).cloned() }; if let Some(callback) = callback { let mut changed = HashMap::new(); for sv in self.statevariables.all() { if sv.is_sending_notification() { 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) .send() .await { Ok(resp) => { info!("✅ Initial event sent to {}, status={}", callback, resp.status()); } Err(e) => { error!("Failed to send initial event to {}: {}", callback, e); } } }); } } /// Marque un changement à notifier. pub fn event_to_be_sent(&self, name: String, value: String) { let mut buffer = self.changed_buffer.lock().unwrap(); buffer.insert(name, value); } /// Récupère le prochain numéro de séquence pour un abonné. 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() } /// Notifie tous les abonnés des changements. pub async fn notify_subscribers(&self) { let subscribers_copy = { let subscribers = self.subscribers.read().unwrap(); 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); } } }); } } /// Démarre le notifier périodique. /// /// # Arguments /// /// * `interval` - Intervalle entre les notifications /// /// # Returns /// /// Un handle vers la tâche tokio du notifier. 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.get_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 souscription 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"), axum::http::HeaderValue::from_str(&response_sid).unwrap() ), ( axum::http::header::HeaderName::from_static("timeout"), axum::http::HeaderValue::from_str(&response_timeout).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.get_name()); // TODO: Parser le SOAP et appeler l'action correspondante let response_xml = format!( r#" "#, instance.service_type() ); ( StatusCode::OK, [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], response_xml, ).into_response() } #[cfg(test)] mod tests { use super::*; use crate::services::Service; #[test] fn test_service_instance_creation() { let service = Service::new("AVTransport".to_string()); let instance = ServiceInstance::new(&service); assert_eq!(instance.get_name(), "AVTransport"); assert_eq!(instance.identifier(), "AVTransport"); } #[test] fn test_service_urls() { let service = Service::new("AVTransport".to_string()); let instance = ServiceInstance::new(&service); assert_eq!(instance.base_route(), "/service/AVTransport"); assert_eq!(instance.control_url(), "/service/AVTransport/control"); assert_eq!(instance.event_sub_url(), "/service/AVTransport/event"); assert_eq!(instance.scpd_url(), "/service/AVTransport/desc.xml"); } #[test] fn test_service_type() { let mut service = Service::new("AVTransport".to_string()); service.set_version(2).unwrap(); let instance = ServiceInstance::new(&service); assert_eq!( instance.service_type(), "urn:schemas-upnp-org:service:AVTransport:2" ); } }``` ## fichier: `pmoupnp/src/services/mod.rs` ```rust //! # Module Services - Gestion des services UPnP //! //! Ce module implémente les services UPnP selon la spécification UPnP Device Architecture. //! Un service UPnP contient des actions (méthodes appelables) et des variables d'état //! (propriétés observables). //! //! ## Architecture //! //! - [`Service`] : Modèle définissant la structure d'un service //! - [`ServiceInstance`] : Instance concrète d'un service attachée à un device //! //! ## Fonctionnalités //! //! - ✅ Actions UPnP avec arguments typés //! - ✅ Variables d'état avec notifications d'événements //! - ✅ Génération SCPD (Service Control Protocol Description) //! - ✅ Endpoints SOAP pour le contrôle //! - ✅ Gestion des abonnements aux événements (SUBSCRIBE/UNSUBSCRIBE) //! - ✅ Notifications automatiques des changements d'état //! //! ## Examples //! //! ```rust //! use pmoupnp::services::Service; //! use pmoupnp::state_variables::StateVariable; //! use pmoupnp::variable_types::StateVarType; //! use std::sync::Arc; //! //! // Créer un service //! let mut service = Service::new("AVTransport".to_string()); //! service.set_version(1).unwrap(); //! //! // Ajouter une variable d'état //! let transport_state = Arc::new( //! StateVariable::new(StateVarType::String, "TransportState".to_string()) //! ); //! service.add_variable(transport_state); //! //! // Créer une instance //! let instance = service.create_instance(); //! ``` mod errors; mod service_methods; mod service_instance; use std::sync::Arc; pub use errors::ServiceError; pub use service_instance::ServiceInstance; use crate::{ actions::ActionSet, state_variables::StateVariableSet, UpnpObjectType, }; /// Service UPnP (modèle). /// /// Représente la définition d'un service UPnP avec ses actions et variables d'état. /// Un service est attaché à un device et expose des fonctionnalités via SOAP. /// /// # Structure /// /// Un service UPnP contient : /// - Un identifiant unique (`identifier`) /// - Une version (ex: 1, 2, 3...) /// - Un ensemble d'actions ([`ActionSet`]) /// - Une table de variables d'état ([`StateVariableSet`]) /// /// # Cycle de vie /// /// 1. Création avec [`Service::new`] /// 2. Configuration (ajout d'actions et variables) /// 3. Instanciation avec [`create_instance`](crate::UpnpModel::create_instance) /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::state_variables::StateVariable; /// # use pmoupnp::variable_types::StateVarType; /// # use std::sync::Arc; /// let mut service = Service::new("ContentDirectory".to_string()); /// service.set_identifier("urn:upnp-org:serviceId:ContentDirectory".to_string()); /// service.set_version(1).unwrap(); /// /// // Ajouter une variable d'état /// let search_caps = Arc::new( /// StateVariable::new(StateVarType::String, "SearchCapabilities".to_string()) /// ); /// service.add_variable(search_caps); /// ``` #[derive(Debug, Clone)] pub struct Service { /// Métadonnées de l'objet UPnP object: UpnpObjectType, /// Identifiant du service (ex: "urn:upnp-org:serviceId:AVTransport") identifier: String, /// Version du service (>= 1) version: u32, /// Actions disponibles dans ce service actions: ActionSet, /// Variables d'état du service state_table: StateVariableSet, } impl Service { /// Crée un nouveau service UPnP. /// /// # Arguments /// /// * `name` - Nom du service (ex: "AVTransport", "RenderingControl") /// /// # Returns /// /// Un nouveau service avec : /// - Identifiant initialisé au nom /// - Version 1 par défaut /// - Collections vides d'actions et de variables /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// let service = Service::new("AVTransport".to_string()); /// assert_eq!(service.name(), "AVTransport"); /// assert_eq!(service.version(), 1); /// ``` pub fn new(name: String) -> Self { Self { object: UpnpObjectType { name: name.clone(), object_type: "Service".to_string(), }, identifier: name, version: 1, state_table: StateVariableSet::new(), actions: ActionSet::new(), } } /// Retourne le nom du service. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// let service = Service::new("AVTransport".to_string()); /// assert_eq!(service.name(), "AVTransport"); /// ``` pub fn name(&self) -> &str { &self.object.name } /// Retourne le type d'objet. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// let service = Service::new("AVTransport".to_string()); /// assert_eq!(service.type_id(), "Service"); /// ``` pub fn type_id(&self) -> &str { &self.object.object_type } /// Retourne l'identifiant du service. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// let mut service = Service::new("AVTransport".to_string()); /// service.set_identifier("urn:upnp-org:serviceId:AVTransport".to_string()); /// assert_eq!(service.identifier(), "urn:upnp-org:serviceId:AVTransport"); /// ``` pub fn identifier(&self) -> &str { &self.identifier } /// Définit l'identifiant du service. /// /// # Arguments /// /// * `id` - Nouvel identifiant (typiquement un URN UPnP) /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// let mut service = Service::new("AVTransport".to_string()); /// service.set_identifier("urn:upnp-org:serviceId:AVTransport".to_string()); /// ``` pub fn set_identifier(&mut self, id: String) { self.identifier = id; } /// Retourne la version du service. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// let service = Service::new("AVTransport".to_string()); /// assert_eq!(service.version(), 1); /// ``` pub fn version(&self) -> u32 { self.version } /// Définit la version du service. /// /// # Arguments /// /// * `version` - Numéro de version (doit être >= 1) /// /// # Errors /// /// Retourne une erreur si la version est < 1. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// let mut service = Service::new("AVTransport".to_string()); /// assert!(service.set_version(2).is_ok()); /// assert!(service.set_version(0).is_err()); /// ``` pub fn set_version(&mut self, version: u32) -> Result<(), ServiceError> { if version < 1 { return Err(ServiceError::ValidationError( "Version must be >= 1".to_string() )); } self.version = version; Ok(()) } /// Ajoute une variable d'état au service. /// /// # Arguments /// /// * `sv` - Variable d'état à ajouter /// /// # Errors /// /// Retourne une erreur si une variable avec le même nom existe déjà. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::state_variables::StateVariable; /// # use pmoupnp::variable_types::StateVarType; /// # use std::sync::Arc; /// let mut service = Service::new("AVTransport".to_string()); /// let var = Arc::new( /// StateVariable::new(StateVarType::String, "TransportState".to_string()) /// ); /// service.add_variable(var).unwrap(); /// ``` pub fn add_variable(&mut self, sv: Arc) -> Result<(), ServiceError> { self.state_table .insert(sv) .map_err(|e| ServiceError::SetError(format!("Failed to add variable: {:?}", e))) } /// Vérifie si une variable d'état existe dans le service. /// /// # Arguments /// /// * `sv` - Variable à rechercher /// /// # Returns /// /// `true` si la variable existe, `false` sinon. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::state_variables::StateVariable; /// # use pmoupnp::variable_types::StateVarType; /// # use std::sync::Arc; /// let mut service = Service::new("AVTransport".to_string()); /// let var = Arc::new( /// StateVariable::new(StateVarType::String, "TransportState".to_string()) /// ); /// service.add_variable(var.clone()).unwrap(); /// assert!(service.contains_variable(var)); /// ``` pub fn contains_variable(&self, sv: Arc) -> bool { self.state_table.contains(sv) } /// Retourne toutes les variables d'état du service. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// let service = Service::new("AVTransport".to_string()); /// for var in service.variables() { /// println!("Variable: {}", var.get_name()); /// } /// ``` pub fn variables(&self) -> Vec> { self.state_table.all() } /// Ajoute une action au service. /// /// # Arguments /// /// * `action` - Action à ajouter /// /// # Errors /// /// Retourne une erreur si une action avec le même nom existe déjà. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::actions::Action; /// # use std::sync::Arc; /// let mut service = Service::new("AVTransport".to_string()); /// let action = Arc::new(Action::new("Play".to_string())); /// service.add_action(action).unwrap(); /// ``` pub fn add_action(&mut self, action: Arc) -> Result<(), ServiceError> { self.actions .insert(action) .map_err(|e| ServiceError::SetError(format!("Failed to add action: {:?}", e))) } /// Retourne toutes les actions du service. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// let service = Service::new("AVTransport".to_string()); /// for action in service.actions() { /// println!("Action: {}", action.get_name()); /// } /// ``` pub fn actions(&self) -> Vec> { self.actions.all() } /// Retourne le type de service UPnP. /// /// Format: `urn:schemas-upnp-org:service:{name}:{version}` /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// let service = Service::new("AVTransport".to_string()); /// assert_eq!( /// service.service_type(), /// "urn:schemas-upnp-org:service:AVTransport:1" /// ); /// ``` pub fn service_type(&self) -> String { format!("urn:schemas-upnp-org:service:{}:{}", self.name(), self.version) } } #[cfg(test)] mod tests { use super::*; use crate::state_variables::StateVariable; use crate::variable_types::StateVarType; use crate::actions::Action; #[test] fn test_service_new() { let service = Service::new("AVTransport".to_string()); assert_eq!(service.name(), "AVTransport"); assert_eq!(service.type_id(), "Service"); assert_eq!(service.version(), 1); assert_eq!(service.identifier(), "AVTransport"); } #[test] fn test_service_set_version() { let mut service = Service::new("AVTransport".to_string()); assert!(service.set_version(2).is_ok()); assert_eq!(service.version(), 2); // Version 0 devrait échouer assert!(service.set_version(0).is_err()); } #[test] fn test_service_add_variable() { let mut service = Service::new("AVTransport".to_string()); let var = Arc::new( StateVariable::new(StateVarType::String, "TransportState".to_string()) ); assert!(service.add_variable(var.clone()).is_ok()); assert!(service.contains_variable(var)); } #[test] fn test_service_add_action() { let mut service = Service::new("AVTransport".to_string()); let action = Arc::new(Action::new("Play".to_string())); assert!(service.add_action(action).is_ok()); assert_eq!(service.actions().len(), 1); } #[test] fn test_service_type() { let mut service = Service::new("AVTransport".to_string()); service.set_version(2).unwrap(); assert_eq!( service.service_type(), "urn:schemas-upnp-org:service:AVTransport:2" ); } }``` ## fichier: `pmoupnp/src/services/errors.rs` ```rust //! Erreurs du module services. use thiserror::Error; /// Erreurs liées aux services UPnP. /// /// Cette énumération couvre toutes les erreurs possibles lors de la manipulation /// de services UPnP, incluant les erreurs de validation, de configuration et d'exécution. #[derive(Error, Debug)] pub enum ServiceError { /// Erreur générale du service. #[error("Service error: {0}")] GeneralError(String), /// Erreur de validation (paramètres invalides). #[error("Validation error: {0}")] ValidationError(String), /// Erreur lors d'une opération sur un ensemble (Set). #[error("Set operation error: {0}")] SetError(String), /// Erreur liée à une action. #[error("Action error: {0}")] ActionError(String), /// Erreur liée à une variable d'état. #[error("State variable error: {0}")] StateVariableError(String), /// Erreur de configuration. #[error("Configuration error: {0}")] ConfigError(String), /// Erreur réseau ou HTTP. #[error("Network error: {0}")] NetworkError(String), /// Erreur de sérialisation XML. #[error("XML serialization error: {0}")] XmlError(String), /// Erreur lors du traitement SOAP. #[error("SOAP error: {0}")] SoapError(String), } impl From for ServiceError { fn from(err: std::io::Error) -> Self { ServiceError::GeneralError(format!("IO error: {}", err)) } } impl From for ServiceError { fn from(err: crate::UpnpObjectSetError) -> Self { match err { crate::UpnpObjectSetError::AlreadyExists(name) => { ServiceError::SetError(format!("Object already exists: {}", name)) } } } }``` ## fichier: `pmoupnp/src/services/service_methods.rs` ```rust //! Implémentation des traits UPnP pour Service. use xmltree::{Element, XMLNode}; use crate::{ services::{Service, ServiceInstance}, UpnpObject, UpnpModel, UpnpTyped, UpnpObjectType, }; impl UpnpTyped for Service { fn as_upnp_object_type(&self) -> &UpnpObjectType { &self.object } } impl UpnpObject for Service { fn to_xml_element(&self) -> Element { let mut elem = Element::new("service"); // serviceType let mut service_type = Element::new("serviceType"); service_type.children.push(XMLNode::Text(self.service_type())); elem.children.push(XMLNode::Element(service_type)); // serviceId let mut service_id = Element::new("serviceId"); service_id.children.push(XMLNode::Text(self.identifier().to_string())); elem.children.push(XMLNode::Element(service_id)); elem } } impl UpnpModel for Service { type Instance = ServiceInstance; }``` ## fichier: `pmoupnp/src/devices/mod.rs` ```rust ``` ## fichier: `pmoutils/Cargo.toml` ```toml [package] name = "pmoutils" version = "0.1.0" edition = "2024" [dependencies] get_if_addrs = "0.5.3"``` ## fichier: `pmoutils/src/lib.rs` ```rust /// 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;``` ## fichier: `pmoutils/src/ip_utils.rs` ```rust 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 { match UdpSocket::bind("0.0.0.0:0") { Ok(socket) => { if socket.connect("8.8.8.8:80").is_ok() { if let Ok(local_addr) = socket.local_addr() { return local_addr.ip().to_string(); } } "127.0.0.1".to_string() } 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 pub fn list_all_ips() -> std::collections::HashMap> { let mut result = std::collections::HashMap::new(); if let Ok(interfaces) = get_if_addrs() { for iface in interfaces { let ip = iface.ip(); if ip.is_loopback() { continue; } if ip.is_ipv4() { result .entry(iface.name) .or_insert_with(Vec::new) .push(ip.to_string()); } } } else { result.insert( "error".to_string(), vec!["Failed to get interfaces".to_string()], ); } 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 } }```