design-patterns

Par rtk-ai · rtk

Patrons de conception Rust pour RTK. Newtype, Builder, RAII, Trait Objects, State Machine. Appliqués aux modules de filtre CLI. À utiliser lors de la conception de nouveaux modules ou de la refactorisation de modules existants.

npx skills add https://github.com/rtk-ai/rtk --skill design-patterns

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>; }

Skills similaires