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
//! 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
// Chemins sous "host"
&["host", "cache_type", "directory"] // Répertoires de cache
&["host", "cache_type", "size"] // Tailles de cache
&["host", "upnp", "manufacturer"] // Configuration UPnPConfiguration comptes/services
// Chemins sous "accounts"
&["accounts", "service", "username"]
&["accounts", "service", "password"]
&["accounts", "service", "auth_token"]Configuration sources
// Chemins sous "sources"
&["sources", "source_name", "enabled"]
&["sources", "source_name", "default_channel"]3. Patterns de getters
Getter simple avec valeur par défaut
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 :
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) :
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
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
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
fn set_xxx_value(&self, value: Type) -> Result<()> {
self.set_value(
&["path", "to", "value"],
Value::Type(value.into())
)
}Setter avec transformation
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)
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
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
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 :
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é
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 :
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
// 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 :
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 :
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 :
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 :
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 :
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
// ✅ 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) -> bool2. Gestion des erreurs
// ✅ 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
// ✅ 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 :
/// 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
//! 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
- Getters/setters simples
- Getters/setters combinés
- Helpers de validation
- Factory methods
- Méthodes de nettoyage
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
// ✅ 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
// ✅ 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)
//! 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é)
//! 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)
//! 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
[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
// 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
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
Philosophie du pattern
Avantages
- Séparation des préoccupations : Chaque crate gère sa propre configuration
- Type safety : Les erreurs de type sont détectées à la compilation
- Extensibilité : Facile d’ajouter de nouveaux crates sans modifier pmoconfig
- Cohérence : Pattern uniforme dans tout le projet
- Documentation : Interface self-documenting avec exemples
Principes directeurs
- Minimalisme : Ne configurer que ce qui doit vraiment l’être
- Defaults intelligents : Valeurs par défaut sensées et documentées
- Auto-persistence : Les valeurs importantes sont persistées automatiquement
- User-friendly : Noms conviviaux dans le YAML (strings au lieu de nombres)
- Fail-safe : Gestion des erreurs gracieuse avec fallback sur défauts
- 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
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.rsoupmoaudiocache/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::encryptionmodule