Ajoute la notion de reflexive value au statevariableinstance

This commit is contained in:
2025-10-11 10:08:28 +02:00
parent 6a98fcc2b9
commit 24068b6eb0
7 changed files with 370 additions and 10 deletions

15
Cargo.lock generated
View File

@@ -1958,7 +1958,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
dependencies = [
"base64",
"indexmap",
"quick-xml",
"quick-xml 0.38.3",
"serde",
"time",
]
@@ -2022,7 +2022,7 @@ version = "0.1.0"
dependencies = [
"bevy_reflect",
"bevy_reflect_derive",
"quick-xml",
"quick-xml 0.38.3",
"serde",
"utoipa",
"utoipa-swagger-ui",
@@ -2066,6 +2066,7 @@ dependencies = [
"pmodidl",
"pmoserver",
"pmoutils",
"quick-xml 0.37.5",
"reqwest",
"serde",
"serde_json",
@@ -2189,6 +2190,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.37.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "quick-xml"
version = "0.38.3"

View File

@@ -19,6 +19,7 @@ axum = "0.8.4"
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
quick-xml = { version = "0.37.0", features = ["serialize"] }
chrono = { version = "0.4.42", features = ["serde"] }
once_cell = "1.20"
parking_lot = "0.12"

View File

@@ -34,6 +34,7 @@ use axum::{
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
};
use bevy_reflect::Reflect;
use std::{
collections::HashMap,
future::Future,
@@ -44,6 +45,7 @@ use std::{
use tokio::time;
use tracing::{debug, error, info, warn};
use xmltree::{Element, EmitterConfig, XMLNode};
use quick_xml::escape::escape;
use crate::{
UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance,
@@ -118,8 +120,8 @@ pub struct ServiceInstance {
/// Abonnés aux événements (SID -> Callback URL)
subscribers: Arc<RwLock<HashMap<String, String>>>,
/// Buffer des changements en attente de notification
changed_buffer: Arc<Mutex<HashMap<String, String>>>,
/// Buffer des changements en attente de notification (nom de variable -> valeur réflexive)
changed_buffer: Arc<Mutex<HashMap<String, Arc<dyn Reflect>>>>,
/// Compteurs de séquence par abonné
seqid: Arc<Mutex<HashMap<String, u32>>>,
@@ -849,14 +851,15 @@ impl ServiceInstance {
///
/// # Examples
///
/// ```rust
/// ```rust,ignore
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// instance.event_to_be_sent("TransportState".to_string(), "PLAYING".to_string());
/// let value = Arc::new("PLAYING".to_string()) as Arc<dyn Reflect>;
/// instance.event_to_be_sent("TransportState".to_string(), value);
/// ```
pub fn event_to_be_sent(&self, name: String, value: String) {
pub fn event_to_be_sent(&self, name: String, value: Arc<dyn Reflect>) {
let mut buffer = self.changed_buffer.lock().unwrap();
buffer.insert(name, value);
}
@@ -895,7 +898,10 @@ impl ServiceInstance {
/// # async fn main() {
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// instance.event_to_be_sent("TransportState".to_string(), "PLAYING".to_string());
/// # use std::sync::Arc;
/// # use bevy_reflect::Reflect;
/// let value = Arc::new("PLAYING".to_string()) as Arc<dyn Reflect>;
/// instance.event_to_be_sent("TransportState".to_string(), value);
/// instance.notify_subscribers().await;
/// # }
/// ```
@@ -926,9 +932,11 @@ impl ServiceInstance {
let mut body =
r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"#.to_string();
for (name, val) in changed_clone {
// Convertir la valeur Reflect en String
let val_str = Self::reflect_to_string(&*val);
body.push_str(&format!(
"<e:property><{0}>{1}</{0}></e:property>",
name, val
name, val_str
));
}
body.push_str("</e:propertyset>");
@@ -956,6 +964,131 @@ impl ServiceInstance {
}
}
/// Convertit une valeur Reflect en String pour la notification UPnP.
///
/// Cette fonction gère plusieurs cas :
/// - Types primitifs : formatage direct
/// - Structures serde (pmodidl, etc.) : sérialisation XML
/// - Autres types : fallback sur Debug
///
/// Le résultat est déjà échappé XML-safe selon les normes UPnP.
fn reflect_to_string(value: &dyn Reflect) -> String {
use std::any::Any;
use bevy_reflect::ReflectRef;
// Essayer de downcaster vers des types primitifs courants
if let Some(v) = value.as_any().downcast_ref::<String>() {
// Échapper les caractères XML spéciaux
return escape(v).to_string();
} else if let Some(v) = value.as_any().downcast_ref::<u8>() {
return v.to_string();
} else if let Some(v) = value.as_any().downcast_ref::<u16>() {
return v.to_string();
} else if let Some(v) = value.as_any().downcast_ref::<u32>() {
return v.to_string();
} else if let Some(v) = value.as_any().downcast_ref::<i8>() {
return v.to_string();
} else if let Some(v) = value.as_any().downcast_ref::<i16>() {
return v.to_string();
} else if let Some(v) = value.as_any().downcast_ref::<i32>() {
return v.to_string();
} else if let Some(v) = value.as_any().downcast_ref::<f32>() {
return v.to_string();
} else if let Some(v) = value.as_any().downcast_ref::<f64>() {
return v.to_string();
} else if let Some(v) = value.as_any().downcast_ref::<bool>() {
return if *v { "1" } else { "0" }.to_string();
} else if let Some(v) = value.as_any().downcast_ref::<char>() {
return escape(&v.to_string()).to_string();
}
// Pour les structures complexes, essayer de sérialiser avec bevy_reflect
match value.reflect_ref() {
ReflectRef::Struct(s) => {
// Construire un XML simple pour la struct
Self::serialize_struct_to_xml(s)
}
ReflectRef::TupleStruct(ts) => {
// Pour les tuple structs, essayer d'extraire la valeur si c'est un wrapper
if ts.field_len() == 1 {
if let Some(inner) = ts.field(0) {
// Convertir PartialReflect en Reflect si possible
if let Some(reflect_val) = inner.try_as_reflect() {
Self::reflect_to_string(reflect_val)
} else {
format!("{:?}", value)
}
} else {
format!("{:?}", value)
}
} else {
format!("{:?}", value)
}
}
ReflectRef::Enum(e) => {
// Pour les enums, formater comme "Variant(value)"
let variant_name = e.variant_name();
if e.field_len() == 1 {
if let Some(field) = e.field_at(0) {
// Convertir PartialReflect en Reflect si possible
if let Some(reflect_val) = field.try_as_reflect() {
format!("{}", Self::reflect_to_string(reflect_val))
} else {
variant_name.to_string()
}
} else {
variant_name.to_string()
}
} else {
variant_name.to_string()
}
}
_ => {
// Fallback: utiliser Debug et échapper
let debug_str = format!("{:?}", value);
escape(&debug_str).to_string()
}
}
}
/// Sérialise une structure Reflect en XML simple.
fn serialize_struct_to_xml(s: &dyn bevy_reflect::Struct) -> String {
use std::fmt::Write;
use bevy_reflect::TypeInfo;
let mut xml = String::new();
// Commencer par ouvrir la balise avec le nom du type
let type_name = s.get_represented_type_info()
.and_then(|ti| {
if let TypeInfo::Struct(si) = ti {
Some(si.type_path_table().short_path())
} else {
None
}
})
.unwrap_or("struct");
let _ = write!(&mut xml, "<{}>", type_name);
// Ajouter chaque champ
for i in 0..s.field_len() {
if let Some(field_name) = s.name_at(i) {
if let Some(field_value) = s.field_at(i) {
// Convertir PartialReflect en Reflect si possible
if let Some(reflect_val) = field_value.try_as_reflect() {
let value_str = Self::reflect_to_string(reflect_val);
let _ = write!(&mut xml, "<{}>{}</{}>", field_name, value_str, field_name);
}
}
}
}
let _ = write!(&mut xml, "</{}>", type_name);
xml
}
/// Démarre le notifier périodique.
///
/// # Arguments
@@ -1239,6 +1372,97 @@ async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: Str
mod tests {
use super::*;
use crate::services::Service;
use bevy_reflect::Reflect;
#[test]
fn test_reflect_to_string_primitives() {
// Test des types primitifs
assert_eq!(ServiceInstance::reflect_to_string(&42i32), "42");
assert_eq!(ServiceInstance::reflect_to_string(&3.14f64), "3.14");
assert_eq!(ServiceInstance::reflect_to_string(&true), "1");
assert_eq!(ServiceInstance::reflect_to_string(&false), "0");
assert_eq!(ServiceInstance::reflect_to_string(&'a'), "a");
}
#[test]
fn test_reflect_to_string_xml_escaping() {
// Test de l'échappement XML
let test_str = "Test <tag> & \"quotes\"".to_string();
let result = ServiceInstance::reflect_to_string(&test_str);
// Vérifier que les caractères sont échappés
assert!(result.contains("&lt;"));
assert!(result.contains("&gt;"));
assert!(result.contains("&amp;"));
assert!(result.contains("&quot;"));
}
#[test]
fn test_reflect_to_string_struct() {
#[derive(Debug, Clone, Reflect)]
struct TestStruct {
name: String,
value: i32,
}
let test = TestStruct {
name: "Test".to_string(),
value: 42,
};
let result = ServiceInstance::reflect_to_string(&test);
// Vérifier que c'est du XML
assert!(result.starts_with("<"));
assert!(result.ends_with(">"));
assert!(result.contains("name"));
assert!(result.contains("value"));
assert!(result.contains("Test"));
assert!(result.contains("42"));
println!("Serialized struct: {}", result);
}
#[test]
fn test_reflect_to_string_nested_struct() {
#[derive(Debug, Clone, Reflect)]
struct Address {
street: String,
city: String,
}
#[derive(Debug, Clone, Reflect)]
struct Person {
name: String,
age: u32,
address: Address,
}
let person = Person {
name: "John <Doe>".to_string(), // Test XML escaping
age: 30,
address: Address {
street: "123 Main St & Ave".to_string(),
city: "Springfield".to_string(),
},
};
let result = ServiceInstance::reflect_to_string(&person);
// Vérifier la structure XML
assert!(result.contains("<Person>"));
assert!(result.contains("</Person>"));
assert!(result.contains("name"));
assert!(result.contains("age"));
assert!(result.contains("address"));
// Vérifier l'échappement XML dans les valeurs imbriquées
assert!(result.contains("&lt;"));
assert!(result.contains("&gt;"));
assert!(result.contains("&amp;"));
println!("Nested struct XML: {}", result);
}
#[test]
fn test_service_instance_creation() {

View File

@@ -1,5 +1,7 @@
use std::fmt;
use std::sync::Arc;
use bevy_reflect::Reflect;
use chrono::{DateTime, Utc};
use std::sync::RwLock;
use xmltree::Element;
@@ -44,6 +46,7 @@ impl UpnpInstance for StateVarInstance {
last_modified: RwLock::new(Utc::now()),
last_notification: RwLock::new(Utc::now()),
service: RwLock::new(None),
reflexive_cache: RwLock::new(None),
}
}
@@ -85,6 +88,7 @@ impl Clone for StateVarInstance {
last_modified: RwLock::new(self.last_modified.read().unwrap().clone()),
last_notification: RwLock::new(self.last_notification.read().unwrap().clone()),
service: RwLock::new(self.service.read().unwrap().clone()),
reflexive_cache: RwLock::new(None), // Le cache n'est pas cloné, il sera recalculé si nécessaire
}
}
}
@@ -131,6 +135,12 @@ impl StateVarInstance {
*val = new_value.clone();
*modified = Utc::now();
// Invalider le cache réflexif
{
let mut cache = self.reflexive_cache.write().unwrap();
*cache = None;
}
// Notifier le service parent si la variable envoie des événements
if self.is_sending_notification() {
// Relâcher les locks avant d'appeler le service
@@ -140,7 +150,10 @@ impl StateVarInstance {
if let Some(weak_service) = self.service.read().unwrap().as_ref() {
if let Some(service) = weak_service.upgrade() {
service.event_to_be_sent(self.get_name().to_string(), new_value.to_string());
// Obtenir la valeur réflexive (sans propager l'erreur car on est dans une notification)
if let Ok(reflected_value) = self.reflexive_value() {
service.event_to_be_sent(self.get_name().to_string(), reflected_value);
}
}
}
}
@@ -156,4 +169,68 @@ impl StateVarInstance {
pub fn last_modified(&self) -> DateTime<Utc> {
self.last_modified.read().unwrap().clone()
}
/// Retourne la valeur sous forme réflexive (Reflect).
///
/// Cette méthode utilise un cache pour optimiser les performances lorsqu'un parser
/// est défini. Si la variable a un parser, la valeur String sera parsée et le résultat
/// sera mis en cache. Sinon, la StateValue brute est retournée directement comme Reflect.
///
/// Le cache est invalidé automatiquement lors de `set_value()`.
///
/// # Returns
///
/// Un `Arc<dyn Reflect>` contenant soit:
/// - La valeur parsée (si un parser est défini)
/// - La StateValue brute (sinon)
///
/// # Examples
///
/// ```rust,ignore
/// let var = StateVarInstance::new(&variable);
/// let reflected = var.reflexive_value();
/// // reflected peut maintenant être inspecté avec l'API Reflect
/// ```
pub fn reflexive_value(&self) -> Result<Arc<dyn Reflect>, crate::state_variables::StateVariableError> {
// Vérifier si on a un cache valide
{
let cache = self.reflexive_cache.read().unwrap();
if let Some(cached) = cache.as_ref() {
return Ok(Arc::clone(cached));
}
}
// Pas de cache, il faut calculer la valeur
let value = self.value.read().unwrap().clone();
// Si la variable a un parser, l'utiliser
if let Some(parser) = &self.model.parse {
// La valeur doit être une String pour être parsée
if let crate::variable_types::StateValue::String(s) = &value {
match parser(s) {
Ok(parsed) => {
// Convertir Box<dyn Reflect> en Arc<dyn Reflect>
let arc_reflect: Arc<dyn Reflect> = Arc::from(parsed);
// Mettre en cache
let mut cache = self.reflexive_cache.write().unwrap();
*cache = Some(Arc::clone(&arc_reflect));
return Ok(arc_reflect);
}
Err(e) => return Err(e),
}
}
}
// Pas de parser ou la valeur n'est pas une String: convertir la StateValue en Reflect
let reflected = value.to_reflect();
let arc_reflect: Arc<dyn Reflect> = Arc::from(reflected);
// Mettre en cache
let mut cache = self.reflexive_cache.write().unwrap();
*cache = Some(Arc::clone(&arc_reflect));
Ok(arc_reflect)
}
}

View File

@@ -60,6 +60,8 @@ pub struct StateVarInstance {
last_notification: RwLock<DateTime<Utc>>,
/// Pointeur vers le service parent (interior mutability)
service: RwLock<Option<std::sync::Weak<crate::services::ServiceInstance>>>,
/// Cache pour la valeur réflexive (utilisé quand un parser est défini)
reflexive_cache: RwLock<Option<Arc<dyn Reflect>>>,
}
pub type StateVarInstanceSet = UpnpObjectSet<StateVarInstance>;

View File

@@ -4,6 +4,7 @@ mod display_type;
mod display_value;
mod errors;
mod fromstr;
mod reflect_impl;
mod type_methods;
mod type_trait;
mod value_methods;

View File

@@ -0,0 +1,44 @@
// Ce module permet de convertir StateValue en valeurs Reflect
//
// Étant donné que StateValue contient des types qui n'implémentent pas tous Reflect
// (comme Uuid, Url, et certains types chrono), nous fournissons des méthodes de conversion
// vers des types primitifs qui supportent Reflect.
use bevy_reflect::Reflect;
use crate::variable_types::StateValue;
impl StateValue {
/// Convertit la StateValue en une valeur Reflect.
///
/// Cette méthode crée un Box<dyn Reflect> contenant la valeur sous-jacente.
/// Pour les types qui n'implémentent pas Reflect nativement (Uuid, Url, dates complexes),
/// on retourne leur représentation String.
pub fn to_reflect(&self) -> Box<dyn Reflect> {
match self {
StateValue::UI1(v) => Box::new(*v),
StateValue::UI2(v) => Box::new(*v),
StateValue::UI4(v) => Box::new(*v),
StateValue::I1(v) => Box::new(*v),
StateValue::I2(v) => Box::new(*v),
StateValue::I4(v) => Box::new(*v),
StateValue::Int(v) => Box::new(*v),
StateValue::R4(v) => Box::new(*v),
StateValue::R8(v) => Box::new(*v),
StateValue::Number(v) => Box::new(*v),
StateValue::Fixed14_4(v) => Box::new(*v),
StateValue::Char(v) => Box::new(*v),
StateValue::String(v) => Box::new(v.clone()),
StateValue::Boolean(v) => Box::new(*v),
StateValue::BinBase64(v) => Box::new(v.clone()),
StateValue::BinHex(v) => Box::new(v.clone()),
// Pour les types complexes, on utilise leur représentation String
StateValue::Date(v) => Box::new(v.to_string()),
StateValue::DateTime(v) => Box::new(v.to_string()),
StateValue::DateTimeTZ(v) => Box::new(v.to_string()),
StateValue::Time(v) => Box::new(v.to_string()),
StateValue::TimeTZ(v) => Box::new(v.to_string()),
StateValue::UUID(v) => Box::new(v.to_string()),
StateValue::URI(v) => Box::new(v.to_string()),
}
}
}