# Pattern d'extension de pmoconfig::Config ## Vue d'ensemble Ce document décrit le pattern architectural utilisé dans PMOMusic pour étendre la configuration centralisée (`pmoconfig::Config`) avec des fonctionnalités spécifiques à chaque crate. ### Objectif Permettre à chaque crate du projet d'ajouter ses propres méthodes de configuration sans modifier directement `pmoconfig`, tout en maintenant une interface cohérente et type-safe. ### Principe Chaque crate qui nécessite un accès à la configuration implémente un **trait d'extension** pour `pmoconfig::Config`. Ce trait définit des méthodes helpers spécifiques au domaine du crate (cache, authentification, UPnP, etc.). ## Architecture du pattern ### Structure de base ``` pmoconfig/ # Crate de configuration centralisée ├── Config # Struct principale avec get_value/set_value génériques └── encryption # Module de chiffrement des mots de passe pmocrate/ # Crate spécialisé (audio, qobuz, upnp, etc.) └── config_ext.rs # Trait d'extension pour Config ├── DEFAULT_* # Constantes pour valeurs par défaut ├── XxxConfigExt # Trait d'extension └── impl # Implémentation du trait pour Config ``` ### Flux de données ``` Application ↓ Trait d'extension spécialisé (QobuzConfigExt, CacheConfigExt, etc.) ↓ pmoconfig::Config (get_value/set_value génériques) ↓ Fichier config.yaml ``` ## Implémentation d'un trait d'extension ### 1. Structure du fichier config_ext.rs ```rust //! Extension pour intégrer [fonctionnalité] dans pmoconfig //! //! Ce module fournit le trait `XxxConfigExt` qui permet d'ajouter facilement //! des méthodes de gestion de [fonctionnalité] à pmoconfig::Config. use anyhow::Result; use pmoconfig::Config; use serde_yaml::Value; // Constantes pour valeurs par défaut const DEFAULT_XXX_DIR: &str = "cache_xxx"; const DEFAULT_XXX_SIZE: usize = 1000; /// Trait d'extension pour gérer [fonctionnalité] dans pmoconfig /// /// Ce trait étend `pmoconfig::Config` avec des méthodes spécifiques /// à la gestion de [fonctionnalité]. /// /// # Exemple /// /// ```rust,ignore /// use pmoconfig::get_config; /// use pmoxxx::XxxConfigExt; /// /// let config = get_config(); /// let value = config.get_xxx_value()?; /// ``` pub trait XxxConfigExt { /// Documentation de la méthode getter fn get_xxx_value(&self) -> Result; /// Documentation de la méthode setter fn set_xxx_value(&self, value: Type) -> Result<()>; } impl XxxConfigExt for Config { fn get_xxx_value(&self) -> Result { // Implémentation } fn set_xxx_value(&self, value: Type) -> Result<()> { // Implémentation } } ``` ### 2. Patterns de chemins YAML Les chemins dans la configuration suivent une hiérarchie logique : #### Configuration hôte/système ```rust // Chemins sous "host" &["host", "cache_type", "directory"] // Répertoires de cache &["host", "cache_type", "size"] // Tailles de cache &["host", "upnp", "manufacturer"] // Configuration UPnP ``` #### Configuration comptes/services ```rust // Chemins sous "accounts" &["accounts", "service", "username"] &["accounts", "service", "password"] &["accounts", "service", "auth_token"] ``` #### Configuration sources ```rust // Chemins sous "sources" &["sources", "source_name", "enabled"] &["sources", "source_name", "default_channel"] ``` ### 3. Patterns de getters #### Getter simple avec valeur par défaut ```rust fn get_xxx_value(&self) -> Result { match self.get_value(&["path", "to", "value"]) { Ok(Value::Type(v)) => Ok(v), _ => Ok(DEFAULT_VALUE), } } ``` #### Getter avec auto-persistence Pour les valeurs qui doivent être visibles dans le fichier YAML, le getter persiste automatiquement la valeur par défaut : ```rust fn get_xxx_enabled(&self) -> Result { match self.get_value(&["path", "to", "enabled"]) { Ok(Value::Bool(b)) => Ok(b), _ => { // Auto-persist la valeur par défaut self.set_xxx_enabled(true)?; Ok(true) } } } ``` **Avantage** : L'utilisateur voit la configuration effective dans le YAML et peut la modifier facilement. #### Getter optionnel Pour les valeurs vraiment optionnelles (pas de défaut significatif) : ```rust fn get_xxx_optional(&self) -> Result> { match self.get_value(&["path", "to", "optional"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // String vide Ok(_) => Ok(None), // Mauvais type Err(_) => Ok(None), // Non configuré } } ``` #### Getter avec traitement spécial ##### Déchiffrement de mots de passe ```rust fn get_xxx_password(&self) -> Result { match self.get_value(&["accounts", "xxx", "password"])? { Value::String(s) => { // Déchiffrement automatique si chiffré pmoconfig::encryption::get_password(&s) .map_err(|e| anyhow!("Failed to decrypt password: {}", e)) } _ => Err(anyhow!("Password not configured")), } } ``` ##### Parsing avec fallback ```rust fn get_xxx_enum_value(&self) -> Result { match self.get_value(&["path", "to", "value"]) { Ok(Value::String(s)) => { match s.parse::() { Ok(kind) => Ok(kind), Err(_) => { // Valeur invalide, utiliser et persister le défaut self.set_xxx_enum_value(DEFAULT_ENUM)?; Ok(DEFAULT_ENUM) } } } Ok(Value::Number(n)) => { // Accepter aussi les valeurs numériques if let Some(id) = n.as_u64() { EnumType::from_id(id as u8) } else { self.set_xxx_enum_value(DEFAULT_ENUM)?; Ok(DEFAULT_ENUM) } } _ => { // Non configuré, persister le défaut self.set_xxx_enum_value(DEFAULT_ENUM)?; Ok(DEFAULT_ENUM) } } } ``` ### 4. Patterns de setters #### Setter simple ```rust fn set_xxx_value(&self, value: Type) -> Result<()> { self.set_value( &["path", "to", "value"], Value::Type(value.into()) ) } ``` #### Setter avec transformation ```rust fn set_xxx_enum(&self, variant: EnumVariant) -> Result<()> { // Stocker sous forme conviviale (string) plutôt que numérique let name = variant.as_str(); self.set_value( &["path", "to", "enum"], Value::String(name.to_string()) ) } ``` #### Setter multiple (transaction) ```rust fn set_xxx_auth_info( &self, token: &str, user_id: &str, expires_at: u64, ) -> Result<()> { // Grouper les modifications liées self.set_value( &["accounts", "xxx", "auth_token"], Value::String(token.to_string()) )?; self.set_value( &["accounts", "xxx", "user_id"], Value::String(user_id.to_string()) )?; self.set_value( &["accounts", "xxx", "token_expires_at"], Value::Number(serde_yaml::Number::from(expires_at)) )?; Ok(()) } ``` #### Setter de nettoyage ```rust fn clear_xxx_info(&self) -> Result<()> { // Ne pas propager les erreurs (valeurs peuvent ne pas exister) let _ = self.set_value(&["path", "to", "field1"], Value::String(String::new())); let _ = self.set_value(&["path", "to", "field2"], Value::Number(Number::from(0))); Ok(()) } ``` ### 5. Helpers de haut niveau #### Helper de validation ```rust fn is_xxx_valid(&self) -> bool { // Vérifier plusieurs conditions sans Result let has_token = self .get_xxx_token() .ok() .flatten() .map(|t| !t.is_empty()) .unwrap_or(false); let has_user = self .get_xxx_user() .ok() .flatten() .map(|u| !u.is_empty()) .unwrap_or(false); has_token && has_user } ``` #### Factory method Pour les crates qui fournissent des objets complexes configurables : ```rust fn create_xxx_cache(&self) -> Result> { let dir = self.get_xxx_dir()?; let size = self.get_xxx_size()?; Ok(Arc::new(crate::new_cache(&dir, size)?)) } fn create_xxx_client(&self) -> Result { let (username, password) = self.get_xxx_credentials()?; XxxClient::builder() .credentials(username, password) .cache_dir(self.get_xxx_cache_dir()?) .build() } ``` #### Getter combiné ```rust fn get_xxx_credentials(&self) -> Result<(String, String)> { let username = self.get_xxx_username()?; let password = self.get_xxx_password()?; Ok((username, password)) } ``` ### 6. Utilisation des méthodes pmoconfig #### Répertoires managés Pour les répertoires qui doivent être créés automatiquement : ```rust fn get_xxx_cache_dir(&self) -> Result { // get_managed_dir crée le répertoire s'il n'existe pas self.get_managed_dir(&["host", "xxx_cache", "directory"], "cache_xxx") } fn set_xxx_cache_dir(&self, directory: String) -> Result<()> { self.set_managed_dir(&["host", "xxx_cache", "directory"], directory) } ``` #### Méthodes génériques de Config utilisables ```rust // Lecture de valeur générique pub fn get_value(&self, path: &[&str]) -> Result // Écriture de valeur générique pub fn set_value(&self, path: &[&str], value: Value) -> Result<()> // Répertoires managés (création auto) pub fn get_managed_dir(&self, path: &[&str], default: &str) -> Result pub fn set_managed_dir(&self, path: &[&str], directory: String) -> Result<()> // Déchiffrement de mots de passe pub mod encryption { pub fn encrypt_password(password: &str) -> Result pub fn decrypt_password(encrypted: &str) -> Result pub fn get_password(value: &str) -> Result // Auto-détection pub fn is_encrypted(value: &str) -> bool } ``` ## Patterns spécialisés ### Pattern cache (pmocache) Le crate `pmocache` fournit un trait générique `CacheConfigExt` que les autres crates de cache peuvent utiliser : ```rust use pmocache::CacheConfigExt; impl AudioCacheConfigExt for Config { fn get_audiocache_dir(&self) -> Result { self.get_cache_dir("audio_cache", DEFAULT_AUDIO_CACHE_DIR) } fn create_audio_cache(&self) -> Result> { let dir = self.get_audiocache_dir()?; let size = self.get_audiocache_size()?; Ok(Arc::new(crate::cache::new_cache(&dir, size)?)) } } ``` **Avantage** : Cohérence entre tous les caches (audio, covers, qobuz, etc.) ### Pattern authentification (pmoqobuz) Pour les services nécessitant une authentification : ```rust pub trait QobuzConfigExt { // Credentials de base fn get_qobuz_username(&self) -> Result; fn get_qobuz_password(&self) -> Result; // Auto-decrypt fn get_qobuz_credentials(&self) -> Result<(String, String)>; // Tokens d'authentification fn get_qobuz_auth_token(&self) -> Result>; fn get_qobuz_user_id(&self) -> Result>; fn get_qobuz_token_expires_at(&self) -> Result>; // Gestion d'authentification groupée fn set_qobuz_auth_info( &self, token: &str, user_id: &str, expires_at: u64 ) -> Result<()>; fn clear_qobuz_auth_info(&self) -> Result<()>; // Validation fn is_qobuz_auth_valid(&self) -> bool; } ``` ### Pattern rate limiting (pmoqobuz) Pour les services avec rate limiting : ```rust pub trait QobuzConfigExt { fn get_qobuz_rate_limit_max_concurrent(&self) -> Result>; fn set_qobuz_rate_limit_max_concurrent(&self, max: usize) -> Result<()>; fn get_qobuz_rate_limit_min_delay_ms(&self) -> Result>; fn set_qobuz_rate_limit_min_delay_ms(&self, delay_ms: u64) -> Result<()>; fn is_qobuz_rate_limiting_enabled(&self) -> bool; fn set_qobuz_rate_limiting_enabled(&self, enabled: bool) -> Result<()>; } ``` ### Pattern configuration minimale (pmoparadise) Pour les sources qui nécessitent peu de configuration : ```rust pub trait RadioParadiseConfigExt { // Juste enable/disable fn get_paradise_enabled(&self) -> Result; fn set_paradise_enabled(&self, enabled: bool) -> Result<()>; // Configuration minimale avec valeurs intelligentes par défaut fn get_paradise_default_channel(&self) -> Result; fn set_paradise_default_channel(&self, channel: u8) -> Result<()>; } ``` **Philosophie** : Ne configurer que ce qui doit vraiment l'être. Éviter la sur-configuration. ### Pattern UPnP (pmoupnp) Pour la configuration des devices UPnP : ```rust pub trait UpnpConfigExt { fn get_upnp_manufacturer(&self) -> Result; fn set_upnp_manufacturer(&self, manufacturer: String) -> Result<()>; fn get_upnp_udn_prefix(&self) -> Result; fn set_upnp_udn_prefix(&self, prefix: String) -> Result<()>; fn get_upnp_model_name_prefix(&self) -> Result; fn set_upnp_model_name_prefix(&self, prefix: String) -> Result<()>; fn get_upnp_friendly_name_prefix(&self) -> Result; fn set_upnp_friendly_name_prefix(&self, prefix: String) -> Result<()>; } ``` **Usage** : Différencier plusieurs instances du serveur (dev, prod, test). ## Bonnes pratiques ### 1. Nommage des méthodes ```rust // ✅ BON : Préfixer avec le nom du service/composant fn get_qobuz_username(&self) -> Result fn get_cache_dir(&self, cache_type: &str, default: &str) -> Result fn get_paradise_enabled(&self) -> Result // ❌ MAUVAIS : Nom trop générique fn get_username(&self) -> Result fn get_directory(&self) -> Result fn is_enabled(&self) -> bool ``` ### 2. Gestion des erreurs ```rust // ✅ BON : Retourner Result pour les valeurs obligatoires fn get_xxx_username(&self) -> Result { match self.get_value(&["accounts", "xxx", "username"])? { Value::String(s) => Ok(s), _ => Err(anyhow!("XXX username not configured")), } } // ✅ BON : Retourner Option pour les valeurs optionnelles fn get_xxx_token(&self) -> Result> // ✅ BON : Retourner bool pour les checks (sans erreur) fn is_xxx_valid(&self) -> bool // ❌ MAUVAIS : Panic ou unwrap fn get_xxx_value(&self) -> String { self.get_value(&["path"]).unwrap().as_str().unwrap() } ``` ### 3. Valeurs par défaut ```rust // ✅ BON : Constantes en haut du fichier const DEFAULT_CACHE_SIZE: usize = 500; const DEFAULT_CACHE_DIR: &str = "cache_audio"; // ✅ BON : Valeurs par défaut documentées /// Récupère la taille du cache /// /// # Returns /// /// Le nombre maximal d'éléments (default: 500) fn get_cache_size(&self) -> Result // ❌ MAUVAIS : Magic numbers fn get_cache_size(&self) -> Result { match self.get_value(&["cache", "size"]) { Ok(Value::Number(n)) => Ok(n.as_u64().unwrap() as usize), _ => Ok(500), // Où vient ce 500 ? } } ``` ### 4. Documentation Chaque méthode doit avoir : ```rust /// Description courte de ce que fait la méthode /// /// # Arguments (si applicable) /// /// * `param` - Description du paramètre /// /// # Returns /// /// Description de ce qui est retourné (avec valeur par défaut si applicable) /// /// # Errors (si Result) /// /// Description des cas d'erreur /// /// # Exemple /// /// ```rust,ignore /// use pmoconfig::get_config; /// use pmoxxx::XxxConfigExt; /// /// let config = get_config(); /// let value = config.get_xxx_value()?; /// ``` fn get_xxx_value(&self) -> Result; ``` ### 5. Organisation du code #### Structure du fichier config_ext.rs ```rust //! Documentation du module // Imports use anyhow::Result; use pmoconfig::Config; use serde_yaml::Value; // Constantes const DEFAULT_XXX: Type = value; // Trait pub trait XxxConfigExt { // Méthodes groupées logiquement } // Implémentation impl XxxConfigExt for Config { // Méthodes dans le même ordre que le trait } // Tests (optionnel) #[cfg(test)] mod tests { use super::*; } ``` #### Ordre des méthodes dans le trait 1. Getters/setters simples 2. Getters/setters combinés 3. Helpers de validation 4. Factory methods 5. Méthodes de nettoyage ```rust pub trait QobuzConfigExt { // 1. Getters/setters simples fn get_qobuz_username(&self) -> Result; fn set_qobuz_username(&self, username: &str) -> Result<()>; fn get_qobuz_password(&self) -> Result; fn set_qobuz_password(&self, password: &str) -> Result<()>; // 2. Getters/setters combinés fn get_qobuz_credentials(&self) -> Result<(String, String)>; fn set_qobuz_auth_info(&self, ...) -> Result<()>; // 3. Helpers de validation fn is_qobuz_auth_valid(&self) -> bool; // 4. Factory methods fn create_qobuz_client(&self) -> Result; // 5. Méthodes de nettoyage fn clear_qobuz_auth_info(&self) -> Result<()>; } ``` ### 6. Types de retour ```rust // ✅ BON : Result pour les opérations qui peuvent échouer fn get_xxx_username(&self) -> Result // ✅ BON : Result> pour les valeurs optionnelles fn get_xxx_token(&self) -> Result> // ✅ BON : bool pour les checks simples fn is_xxx_enabled(&self) -> bool // ✅ BON : Result<(T1, T2)> pour retourner plusieurs valeurs liées fn get_xxx_credentials(&self) -> Result<(String, String)> // ❌ MAUVAIS : Option> (ordre inversé) fn get_xxx_value(&self) -> Option> ``` ### 7. Conversion de types ```rust // ✅ BON : Gérer plusieurs types d'entrée fn get_xxx_value(&self) -> Result { match self.get_value(&["path", "to", "value"]) { Ok(Value::Number(n)) if n.is_u64() => Ok(n.as_u64().unwrap()), Ok(Value::Number(n)) if n.is_i64() => Ok(n.as_i64().unwrap() as u64), Ok(Value::String(s)) => s.parse::() .map_err(|e| anyhow!("Invalid number: {}", e)), _ => Err(anyhow!("Value not configured")), } } // ✅ BON : Convertir en format convivial pour l'utilisateur fn set_xxx_channel(&self, channel: u8) -> Result<()> { // Stocker "main" au lieu de "0" dans le YAML let name = match channel { 0 => "main", 1 => "mellow", 2 => "rock", _ => return Err(anyhow!("Invalid channel")), }; self.set_value(&["path"], Value::String(name.to_string())) } ``` ## Exemples d'implémentation complète ### Exemple 1 : Cache simple (pmocovers) ```rust //! Extension pour intégrer le cache de couvertures dans pmoconfig use anyhow::Result; use pmocache::CacheConfigExt; use pmoconfig::Config; use std::sync::Arc; const DEFAULT_COVER_CACHE_DIR: &str = "cache_covers"; const DEFAULT_COVER_CACHE_SIZE: usize = 2000; pub trait CoverCacheConfigExt { fn get_covers_dir(&self) -> Result; fn set_covers_dir(&self, directory: String) -> Result<()>; fn get_covers_size(&self) -> Result; fn set_covers_size(&self, size: usize) -> Result<()>; fn create_cover_cache(&self) -> Result>; } impl CoverCacheConfigExt for Config { fn get_covers_dir(&self) -> Result { self.get_cache_dir("cover_cache", DEFAULT_COVER_CACHE_DIR) } fn set_covers_dir(&self, directory: String) -> Result<()> { self.set_cache_dir("cover_cache", directory) } fn get_covers_size(&self) -> Result { self.get_cache_size("cover_cache", DEFAULT_COVER_CACHE_SIZE) } fn set_covers_size(&self, size: usize) -> Result<()> { self.set_cache_size("cover_cache", size) } fn create_cover_cache(&self) -> Result> { let dir = self.get_covers_dir()?; let size = self.get_covers_size()?; Ok(Arc::new(crate::cache::new_cache(&dir, size)?)) } } ``` ### Exemple 2 : Service avec authentification (pmoqobuz - simplifié) ```rust //! Extension pour intégrer la configuration Qobuz dans pmoconfig use anyhow::{anyhow, Result}; use pmoconfig::Config; use serde_yaml::Value; pub trait QobuzConfigExt { // Credentials fn get_qobuz_username(&self) -> Result; fn set_qobuz_username(&self, username: &str) -> Result<()>; fn get_qobuz_password(&self) -> Result; fn set_qobuz_password(&self, password: &str) -> Result<()>; fn get_qobuz_credentials(&self) -> Result<(String, String)>; // Authentification fn get_qobuz_auth_token(&self) -> Result>; fn get_qobuz_user_id(&self) -> Result>; fn set_qobuz_auth_info(&self, token: &str, user_id: &str) -> Result<()>; fn clear_qobuz_auth_info(&self) -> Result<()>; fn is_qobuz_auth_valid(&self) -> bool; } impl QobuzConfigExt for Config { fn get_qobuz_username(&self) -> Result { match self.get_value(&["accounts", "qobuz", "username"])? { Value::String(s) => Ok(s), _ => Err(anyhow!("Qobuz username not configured")), } } fn set_qobuz_username(&self, username: &str) -> Result<()> { self.set_value( &["accounts", "qobuz", "username"], Value::String(username.to_string()), ) } fn get_qobuz_password(&self) -> Result { match self.get_value(&["accounts", "qobuz", "password"])? { Value::String(s) => { // Déchiffrement automatique pmoconfig::encryption::get_password(&s) .map_err(|e| anyhow!("Failed to decrypt password: {}", e)) } _ => Err(anyhow!("Qobuz password not configured")), } } fn set_qobuz_password(&self, password: &str) -> Result<()> { self.set_value( &["accounts", "qobuz", "password"], Value::String(password.to_string()), ) } fn get_qobuz_credentials(&self) -> Result<(String, String)> { let username = self.get_qobuz_username()?; let password = self.get_qobuz_password()?; Ok((username, password)) } fn get_qobuz_auth_token(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "auth_token"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), _ => Ok(None), } } fn get_qobuz_user_id(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "user_id"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), _ => Ok(None), } } fn set_qobuz_auth_info(&self, token: &str, user_id: &str) -> Result<()> { self.set_value( &["accounts", "qobuz", "auth_token"], Value::String(token.to_string()), )?; self.set_value( &["accounts", "qobuz", "user_id"], Value::String(user_id.to_string()), )?; Ok(()) } fn clear_qobuz_auth_info(&self) -> Result<()> { let _ = self.set_value( &["accounts", "qobuz", "auth_token"], Value::String(String::new()), ); let _ = self.set_value( &["accounts", "qobuz", "user_id"], Value::String(String::new()), ); Ok(()) } fn is_qobuz_auth_valid(&self) -> bool { self.get_qobuz_auth_token() .ok() .flatten() .map(|t| !t.is_empty()) .unwrap_or(false) && self .get_qobuz_user_id() .ok() .flatten() .map(|u| !u.is_empty()) .unwrap_or(false) } } ``` ### Exemple 3 : Configuration minimale (pmoparadise) ```rust //! Extension pour intégrer Radio Paradise dans pmoconfig use anyhow::Result; use pmoconfig::Config; use serde_yaml::Value; pub trait RadioParadiseConfigExt { fn get_paradise_enabled(&self) -> Result; fn set_paradise_enabled(&self, enabled: bool) -> Result<()>; fn get_paradise_default_channel(&self) -> Result; fn set_paradise_default_channel(&self, channel: u8) -> Result<()>; } impl RadioParadiseConfigExt for Config { fn get_paradise_enabled(&self) -> Result { match self.get_value(&["sources", "radio_paradise", "enabled"]) { Ok(Value::Bool(b)) => Ok(b), _ => { // Auto-persist le défaut self.set_paradise_enabled(true)?; Ok(true) } } } fn set_paradise_enabled(&self, enabled: bool) -> Result<()> { self.set_value( &["sources", "radio_paradise", "enabled"], Value::Bool(enabled), ) } fn get_paradise_default_channel(&self) -> Result { match self.get_value(&["sources", "radio_paradise", "default_channel"]) { Ok(Value::String(s)) => { // Accepter les noms conviviaux match s.as_str() { "main" => Ok(0), "mellow" => Ok(1), "rock" => Ok(2), "eclectic" => Ok(3), _ => { self.set_paradise_default_channel(0)?; Ok(0) } } } Ok(Value::Number(n)) if n.is_u64() => { let ch = n.as_u64().unwrap(); if ch <= 3 { Ok(ch as u8) } else { self.set_paradise_default_channel(0)?; Ok(0) } } _ => { self.set_value( &["sources", "radio_paradise", "default_channel"], Value::String("main".to_string()), )?; Ok(0) } } } fn set_paradise_default_channel(&self, channel: u8) -> Result<()> { let name = match channel { 0 => "main", 1 => "mellow", 2 => "rock", 3 => "eclectic", _ => return Err(anyhow::anyhow!("Invalid channel ID: {}", channel)), }; self.set_value( &["sources", "radio_paradise", "default_channel"], Value::String(name.to_string()), ) } } ``` ## Intégration dans un crate ### Structure recommandée ``` pmoxxx/ ├── Cargo.toml ├── src/ │ ├── lib.rs # Exporte le trait d'extension │ ├── config_ext.rs # Implémentation du trait │ └── ... # Reste du code du crate ``` ### Dans Cargo.toml ```toml [dependencies] pmoconfig = { path = "../pmoconfig" } anyhow = "1.0" serde_yaml = "0.9" # Si c'est un cache, inclure pmocache pmocache = { path = "../pmocache", optional = false } ``` ### Dans lib.rs ```rust // Exporter le trait pour qu'il soit utilisable pub mod config_ext; pub use config_ext::XxxConfigExt; // Le reste du code du crate // ... ``` ### Utilisation dans le code applicatif ```rust use pmoconfig::get_config; use pmoxxx::XxxConfigExt; fn main() -> anyhow::Result<()> { let config = get_config(); // Utiliser les méthodes du trait d'extension let value = config.get_xxx_value()?; config.set_xxx_value(new_value)?; // Factory method let client = config.create_xxx_client()?; Ok(()) } ``` ## Checklist pour créer un nouveau trait d'extension - [ ] Créer le fichier `src/config_ext.rs` dans le crate - [ ] Définir les constantes pour les valeurs par défaut - [ ] Créer le trait `XxxConfigExt` avec documentation - [ ] Implémenter les getters avec gestion d'erreur appropriée - [ ] Implémenter les setters - [ ] Ajouter les helpers de validation si nécessaire - [ ] Ajouter les factory methods si applicable - [ ] Documenter chaque méthode avec exemples - [ ] Exporter le trait dans `lib.rs` - [ ] Ajouter `pmoconfig` dans `Cargo.toml` - [ ] Tester l'intégration ## Philosophie du pattern ### Avantages 1. **Séparation des préoccupations** : Chaque crate gère sa propre configuration 2. **Type safety** : Les erreurs de type sont détectées à la compilation 3. **Extensibilité** : Facile d'ajouter de nouveaux crates sans modifier pmoconfig 4. **Cohérence** : Pattern uniforme dans tout le projet 5. **Documentation** : Interface self-documenting avec exemples ### Principes directeurs 1. **Minimalisme** : Ne configurer que ce qui doit vraiment l'être 2. **Defaults intelligents** : Valeurs par défaut sensées et documentées 3. **Auto-persistence** : Les valeurs importantes sont persistées automatiquement 4. **User-friendly** : Noms conviviaux dans le YAML (strings au lieu de nombres) 5. **Fail-safe** : Gestion des erreurs gracieuse avec fallback sur défauts 6. **Zero surprise** : Comportement prévisible et cohérent ## Sécurité : Chiffrement des mots de passe Tous les mots de passe dans la configuration doivent pouvoir être chiffrés. Voir `pmoconfig/PASSWORD_ENCRYPTION.md` pour les détails. ### Pattern pour les mots de passe ```rust fn get_xxx_password(&self) -> Result { match self.get_value(&["accounts", "xxx", "password"])? { Value::String(s) => { // Déchiffrement automatique pmoconfig::encryption::get_password(&s) .map_err(|e| anyhow!("Failed to decrypt password: {}", e)) } _ => Err(anyhow!("Password not configured")), } } fn set_xxx_password(&self, password: &str) -> Result<()> { // Le chiffrement est fait manuellement par l'utilisateur avec l'outil self.set_value( &["accounts", "xxx", "password"], Value::String(password.to_string()), ) } ``` **Important** : Le setter stocke le mot de passe tel quel. C'est l'utilisateur qui décide de le chiffrer ou non avec l'outil `encrypt_password`. ## Références ### Fichiers d'exemple à consulter - **Cache générique** : `pmocache/src/config_ext.rs` - **Cache spécialisé** : `pmocovers/src/config_ext.rs` ou `pmoaudiocache/src/config_ext.rs` - **Service avec auth** : `pmoqobuz/src/config_ext.rs` - **Configuration minimale** : `pmoparadise/src/config_ext.rs` - **Configuration UPnP** : `pmoupnp/src/config_ext.rs` - **Chiffrement** : `pmoconfig/PASSWORD_ENCRYPTION.md` ### Documentation pmoconfig - `pmoconfig::Config::get_value()` - `pmoconfig::Config::set_value()` - `pmoconfig::Config::get_managed_dir()` - `pmoconfig::encryption` module