Files
pmomusic/pmoupnp/src/services/mod.rs

583 lines
17 KiB
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 pmoupnp::UpnpModel;
//! 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 macros;
mod service_instance;
mod service_methods;
use std::sync::Arc;
pub use errors::ServiceError;
pub use service_instance::ServiceInstance;
use xmltree::{Element, EmitterConfig, XMLNode};
use crate::{UpnpObject, UpnpObjectType, actions::ActionSet, state_variables::StateVariableSet};
/// 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<crate::state_variables::StateVariable>,
) -> 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<crate::state_variables::StateVariable>) -> bool {
self.state_table.contains(sv)
}
/// Retourne toutes les variables d'état du service.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpTyped;
/// let service = Service::new("AVTransport".to_string());
/// for var in service.variables() {
/// println!("Variable: {}", var.get_name());
/// }
/// ```
pub fn variables(&self) -> Vec<Arc<crate::state_variables::StateVariable>> {
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<crate::actions::Action>) -> 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;
/// # use pmoupnp::UpnpTyped;
/// let service = Service::new("AVTransport".to_string());
/// for action in service.actions() {
/// println!("Action: {}", action.get_name());
/// }
/// ```
pub fn actions(&self) -> Vec<Arc<crate::actions::Action>> {
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
)
}
/// Retourne l'ideintifiant du service UPnP.
///
/// Format: `urn:schemas-upnp-org:serviceId:{name}`
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// let service = Service::new("AVTransport".to_string());
/// assert_eq!(
/// service.service_id(),
/// "urn:schemas-upnp-org:serviceId:AVTransport"
/// );
/// ```
pub fn service_id(&self) -> String {
format!("urn:schemas-upnp-org:serviceId:{}", self.name())
}
/// Retourne l'URL de base du service.
///
/// Cette méthode est utilisée en interne pour construire les routes du service.
fn service_base_url(&self) -> String {
format!("/service/{}", self.name())
}
/// Retourne la route de la description SCPD.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// let service = Service::new("AVTransport".to_string());
/// assert_eq!(service.scpd_route(), "/service/AVTransport/desc.xml");
/// ```
pub fn scpd_route(&self) -> String {
format!("{}/desc.xml", self.service_base_url())
}
/// Retourne la route de contrôle SOAP.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// let service = Service::new("AVTransport".to_string());
/// assert_eq!(service.control_route(), "/service/AVTransport/control");
/// ```
pub fn control_route(&self) -> String {
format!("{}/control", self.service_base_url())
}
/// Retourne la route de souscription aux événements.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// let service = Service::new("AVTransport".to_string());
/// assert_eq!(service.event_route(), "/service/AVTransport/event");
/// ```
pub fn event_route(&self) -> String {
format!("{}/event", self.service_base_url())
}
/// Génère l'élément XML de la description SCPD (Service Control Protocol Description).
///
/// Cette méthode crée un élément XML conforme à la spécification UPnP qui décrit
/// le service, ses actions et ses variables d'état.
///
/// # Returns
///
/// Un élément `xmltree::Element` représentant le document SCPD.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// let service = Service::new("AVTransport".to_string());
/// let scpd = service.scpd_element();
/// assert_eq!(scpd.name, "scpd");
/// ```
pub fn scpd_element(&self) -> Element {
let mut scpd = Element::new("scpd");
scpd.attributes.insert(
"xmlns".to_string(),
"urn:schemas-upnp-org:service-1-0".to_string(),
);
let mut specversion = Element::new("specVersion");
let mut major = Element::new("major");
major.children.push(XMLNode::Text("1".to_string()));
specversion.children.push(XMLNode::Element(major));
let mut minor = Element::new("minor");
minor.children.push(XMLNode::Text("0".to_string()));
specversion.children.push(XMLNode::Element(minor));
scpd.children.push(XMLNode::Element(specversion));
scpd.children
.push(XMLNode::Element(self.actions.to_xml_element()));
scpd.children
.push(XMLNode::Element(self.state_table.to_xml_element()));
scpd
}
/// Génère la chaîne XML de la description SCPD.
///
/// Cette méthode produit un document XML complet et formaté décrivant le service
/// selon la spécification UPnP.
///
/// # Returns
///
/// Une chaîne de caractères contenant le XML formaté de la description SCPD.
///
/// # Panics
///
/// Panique si la sérialisation XML échoue ou produit un UTF-8 invalide.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// let service = Service::new("AVTransport".to_string());
/// let xml = service.scpd_xml();
/// assert!(xml.contains("<scpd"));
/// ```
pub fn scpd_xml(&self) -> String {
let elem = self.scpd_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");
String::from_utf8(buf).expect("Invalid UTF-8")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::actions::Action;
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
#[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"
);
}
}