Files
pmomusic/Blackboard/Architecture/pmoconfig_ext.md
Eric Coissac c250801a9f Documentation complète des patterns d'extension pmoconfig, pmoserver_ext et implémentation MusicSource
Ajout de la documentation complète pour les patterns d'extension pmoconfig, pmoserver_ext et l'implémentation d'une nouvelle MusicSource, incluant les guides détaillés, exemples de code et checklists d'implémentation.
2026-01-15 08:18:15 +01:00

1075 lines
30 KiB
Markdown

# 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<Type>;
/// Documentation de la méthode setter
fn set_xxx_value(&self, value: Type) -> Result<()>;
}
impl XxxConfigExt for Config {
fn get_xxx_value(&self) -> Result<Type> {
// 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<Type> {
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<bool> {
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<Option<String>> {
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<String> {
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<EnumType> {
match self.get_value(&["path", "to", "value"]) {
Ok(Value::String(s)) => {
match s.parse::<EnumType>() {
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<Arc<Cache>> {
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<XxxClient> {
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<String> {
// 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<Value>
// É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<String>
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<String>
pub fn decrypt_password(encrypted: &str) -> Result<String>
pub fn get_password(value: &str) -> Result<String> // 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<String> {
self.get_cache_dir("audio_cache", DEFAULT_AUDIO_CACHE_DIR)
}
fn create_audio_cache(&self) -> Result<Arc<Cache>> {
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<String>;
fn get_qobuz_password(&self) -> Result<String>; // Auto-decrypt
fn get_qobuz_credentials(&self) -> Result<(String, String)>;
// Tokens d'authentification
fn get_qobuz_auth_token(&self) -> Result<Option<String>>;
fn get_qobuz_user_id(&self) -> Result<Option<String>>;
fn get_qobuz_token_expires_at(&self) -> Result<Option<u64>>;
// 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<Option<usize>>;
fn set_qobuz_rate_limit_max_concurrent(&self, max: usize) -> Result<()>;
fn get_qobuz_rate_limit_min_delay_ms(&self) -> Result<Option<u64>>;
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<bool>;
fn set_paradise_enabled(&self, enabled: bool) -> Result<()>;
// Configuration minimale avec valeurs intelligentes par défaut
fn get_paradise_default_channel(&self) -> Result<u8>;
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<String>;
fn set_upnp_manufacturer(&self, manufacturer: String) -> Result<()>;
fn get_upnp_udn_prefix(&self) -> Result<String>;
fn set_upnp_udn_prefix(&self, prefix: String) -> Result<()>;
fn get_upnp_model_name_prefix(&self) -> Result<String>;
fn set_upnp_model_name_prefix(&self, prefix: String) -> Result<()>;
fn get_upnp_friendly_name_prefix(&self) -> Result<String>;
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<String>
fn get_cache_dir(&self, cache_type: &str, default: &str) -> Result<String>
fn get_paradise_enabled(&self) -> Result<bool>
// ❌ MAUVAIS : Nom trop générique
fn get_username(&self) -> Result<String>
fn get_directory(&self) -> Result<String>
fn is_enabled(&self) -> bool
```
### 2. Gestion des erreurs
```rust
// ✅ BON : Retourner Result pour les valeurs obligatoires
fn get_xxx_username(&self) -> Result<String> {
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<Option<String>>
// ✅ 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<usize>
// ❌ MAUVAIS : Magic numbers
fn get_cache_size(&self) -> Result<usize> {
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<Type>;
```
### 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<String>;
fn set_qobuz_username(&self, username: &str) -> Result<()>;
fn get_qobuz_password(&self) -> Result<String>;
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<QobuzClient>;
// 5. Méthodes de nettoyage
fn clear_qobuz_auth_info(&self) -> Result<()>;
}
```
### 6. Types de retour
```rust
// ✅ BON : Result<T> pour les opérations qui peuvent échouer
fn get_xxx_username(&self) -> Result<String>
// ✅ BON : Result<Option<T>> pour les valeurs optionnelles
fn get_xxx_token(&self) -> Result<Option<String>>
// ✅ 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<Result<T>> (ordre inversé)
fn get_xxx_value(&self) -> Option<Result<String>>
```
### 7. Conversion de types
```rust
// ✅ BON : Gérer plusieurs types d'entrée
fn get_xxx_value(&self) -> Result<u64> {
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::<u64>()
.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<String>;
fn set_covers_dir(&self, directory: String) -> Result<()>;
fn get_covers_size(&self) -> Result<usize>;
fn set_covers_size(&self, size: usize) -> Result<()>;
fn create_cover_cache(&self) -> Result<Arc<crate::Cache>>;
}
impl CoverCacheConfigExt for Config {
fn get_covers_dir(&self) -> Result<String> {
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<usize> {
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<Arc<crate::Cache>> {
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<String>;
fn set_qobuz_username(&self, username: &str) -> Result<()>;
fn get_qobuz_password(&self) -> Result<String>;
fn set_qobuz_password(&self, password: &str) -> Result<()>;
fn get_qobuz_credentials(&self) -> Result<(String, String)>;
// Authentification
fn get_qobuz_auth_token(&self) -> Result<Option<String>>;
fn get_qobuz_user_id(&self) -> Result<Option<String>>;
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<String> {
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<String> {
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<Option<String>> {
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<Option<String>> {
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<bool>;
fn set_paradise_enabled(&self, enabled: bool) -> Result<()>;
fn get_paradise_default_channel(&self) -> Result<u8>;
fn set_paradise_default_channel(&self, channel: u8) -> Result<()>;
}
impl RadioParadiseConfigExt for Config {
fn get_paradise_enabled(&self) -> Result<bool> {
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<u8> {
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<String> {
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