Modèles de conception Rust RTK
Modèles qui s'appliquent à l'architecture du module filter de RTK. Axés sur les modèles d'outils CLI, pas sur les modèles web/service.
Modèle 1 : Newtype (Sécurité des types)
À utiliser quand : envelopper des types primitifs pour prévenir les mauvais usages (noms de commande, chemins, nombre de tokens).
// Sans Newtype — facile de confondre
fn track(input_tokens: usize, output_tokens: usize) { ... }
track(output_tokens, input_tokens); // Bug silencieux !
// Avec Newtype — erreur à la compilation sur l'inversion
pub struct InputTokens(pub usize);
pub struct OutputTokens(pub usize);
fn track(input: InputTokens, output: OutputTokens) { ... }
track(OutputTokens(100), InputTokens(400)); // Erreur de compilation ✅
// Exemple pratique RTK : validation du nom de commande
pub struct CommandName(String);
impl CommandName {
pub fn new(s: &str) -> Result<Self> {
if s.contains(';') || s.contains('|') || s.contains('`') {
anyhow::bail!("Invalid command name: shell metacharacters");
}
Ok(Self(s.to_string()))
}
pub fn as_str(&self) -> &str { &self.0 }
}
Modèle 2 : Builder (Configuration complexe)
À utiliser quand : une struct a 4+ champs optionnels, beaucoup avec des valeurs par défaut.
#[derive(Default)]
pub struct FilterConfig {
max_lines: Option<usize>,
strip_ansi: bool,
show_warnings: bool,
truncate_at: Option<usize>,
}
impl FilterConfig {
pub fn new() -> Self { Self::default() }
pub fn max_lines(mut self, n: usize) -> Self { self.max_lines = Some(n); self }
pub fn strip_ansi(mut self, v: bool) -> Self { self.strip_ansi = v; self }
pub fn show_warnings(mut self, v: bool) -> Self { self.show_warnings = v; self }
}
// Utilisation — lisible, pas de confusion avec les arguments positionnels
let config = FilterConfig::new()
.max_lines(50)
.strip_ansi(true)
.show_warnings(false);
À ne pas utiliser quand : la struct a 1-3 champs avec une signification évidente. Sur-ingénierie pour les cas simples.
Modèle 3 : Machine à états (Flux Parser/Filter)
À utiliser quand : analyser une sortie multi-section (résultats de tests, sortie de build) où le contexte change le comportement.
// Exemple RTK : analyse de la sortie pytest
#[derive(Debug, PartialEq)]
enum ParseState {
LookingForTests,
InTestOutput,
InFailureSummary,
Done,
}
fn parse_pytest(input: &str) -> String {
let mut state = ParseState::LookingForTests;
let mut failures = Vec::new();
for line in input.lines() {
match state {
ParseState::LookingForTests => {
if line.contains("FAILED") || line.contains("ERROR") {
state = ParseState::InFailureSummary;
failures.push(line);
}
}
ParseState::InFailureSummary => {
if line.starts_with("=====") { state = ParseState::Done; }
else { failures.push(line); }
}
ParseState::Done => break,
_ => {}
}
}
failures.join("\n")
}
Modèle 4 : Trait Object (Dispatching de commandes)
À utiliser quand : différentes familles de commandes ont besoin de la même interface. Évite les énormes branches match.
// Définir une interface commune pour les filtres
pub trait OutputFilter {
fn filter(&self, input: &str) -> Result<String>;
fn command_name(&self) -> &str;
}
pub struct GitFilter;
pub struct CargoFilter;
impl OutputFilter for GitFilter {
fn filter(&self, input: &str) -> Result<String> { filter_git(input) }
fn command_name(&self) -> &str { "git" }
}
// RTK utilise actuellement le dispatching basé sur match dans main.rs (plus simple, pas de surcharge de dispatching dynamique)
// Les trait objects sont utiles si le registre de filtres devient dynamique (par ex, plugins chargés depuis TOML)
Note : le dispatching match actuel de RTK dans main.rs est intentionnel — dispatching statique, zéro surcharge. Passer aux trait objects uniquement si le nombre de branches match dépasse ~20 commandes.
Modèle 5 : RAII (Gestion des ressources)
À utiliser quand : gérer des ressources qui ont besoin de nettoyage (fichiers temporaires, connexions SQLite).
// RTK tee.rs — RAII pour les fichiers de sortie temporaires
pub struct TeeFile {
path: PathBuf,
}
impl TeeFile {
pub fn create(content: &str) -> Result<Self> {
let path = tee_path()?;
fs::write(&path, content)
.with_context(|| format!("Failed to write tee file: {}", path.display()))?;
Ok(Self { path })
}
pub fn path(&self) -> &Path { &self.path }
}
// Pas de nettoyage explicite nécessaire — le fichier persiste intentionnellement (rotation gérée séparément)
// Si le nettoyage était nécessaire : impl Drop { fn drop(&mut self) { let _ = fs::remove_file(&self.path); } }
Modèle 6 : Strategy (Logique de filtre interchangeable)
À utiliser quand : une commande a plusieurs modes de filtrage (par ex, compact vs. verbeux).
pub enum FilterMode {
Compact, // Afficher uniquement les échecs/erreurs
Summary, // Afficher les comptages + les principales erreurs
Full, // Passer inchangé
}
pub fn apply_filter(input: &str, mode: FilterMode) -> String {
match mode {
FilterMode::Compact => filter_compact(input),
FilterMode::Summary => filter_summary(input),
FilterMode::Full => input.to_string(),
}
}
Modèle 7 : Extension Trait (Ajouter des méthodes aux types externes)
À utiliser quand : vous avez besoin d'ajouter des méthodes à des types que vous ne possédez pas (comme &str pour l'analyse spécifique à RTK).
pub trait RtkStrExt {
fn is_error_line(&self) -> bool;
fn is_warning_line(&self) -> bool;
fn token_count(&self) -> usize;
}
impl RtkStrExt for str {
fn is_error_line(&self) -> bool {
self.starts_with("error") || self.contains("[E")
}
fn is_warning_line(&self) -> bool {
self.starts_with("warning")
}
fn token_count(&self) -> usize {
self.split_whitespace().count()
}
}
// Utilisation
if line.is_error_line() { ... }
let tokens = output.token_count();
Guide de sélection des modèles RTK
| Situation | Modèle | À éviter |
|---|---|---|
Nouveau module filter *_cmd.rs |
Modèle standard de module (voir CLAUDE.md) | Sur-abstraction |
| 4+ champs de config optionnels | Builder | Littéral de struct |
| Analyse multi-phase de la sortie | Machine à états | If/else imbriqués |
| Wrapper type-safe autour d'une string | Newtype | Raw String |
Ajouter des méthodes à &str |
Extension Trait | Fonctions libres |
| Ressource avec nettoyage | RAII / Drop | Nettoyage manuel |
| Registre de filtres dynamique | Trait Object | Prolifération de match |
Anti-modèles dans le contexte de RTK
// ❌ Sur-ingénierie générique pour une commande
pub trait Filterable<T: CommandArgs + Send + Sync + 'static> { ... }
// ✅ Écrire simplement la fonction
pub fn filter_git_log(input: &str) -> Result<String> { ... }
// ❌ Registre singleton avec état global
static FILTER_REGISTRY: Mutex<HashMap<String, Box<dyn Filter>>> = ...;
// ✅ Match dans main.rs — simple, zéro surcharge, facile à tracer
// ❌ Traits async pour « prouver la durabilité future »
#[async_trait]
pub trait Filter { async fn apply(&self, input: &str) -> Result<String>; }
// ✅ Synchrone — RTK est mono-thread par conception
pub trait Filter { fn apply(&self, input: &str) -> Result<String>; }