PluginBench
Skill
Pass
Audit score 90

design-patterns

rtk-ai/rtk

Rust design patterns for RTK CLI filter modules: Newtype, Builder, RAII, Trait Objects, State Machine.

What is design-patterns?

A guide to applying proven Rust design patterns within RTK's filter module architecture. Covers type-safe wrappers, configuration builders, stateful parsing, resource management, and command dispatch—with practical examples and anti-patterns to avoid in a CLI tool context.

  • Apply Newtype pattern for type-safe wrappers around primitives and strings
  • Use Builder pattern for structs with 4+ optional configuration fields
  • Implement State Machine for multi-phase output parsing (test results, build logs)
  • Leverage Trait Objects for dynamic command dispatch when match arms exceed ~20
  • Manage resources with RAII and Drop for cleanup (temp files, connections)
  • Extend external types like &str with RTK-specific methods via Extension Trait

How to install design-patterns

npx skills add null --skill design-patterns
Claude Code
Cursor
Windsurf
Cline

How to use design-patterns

  1. 1.Read the pattern overview that matches your design challenge (Newtype, Builder, State Machine, etc.)
  2. 2.Review the practical RTK example code for that pattern
  3. 3.Check the Pattern Selection Guide table to confirm it fits your use case
  4. 4.Implement the pattern in your new or refactored module
  5. 5.Avoid the listed anti-patterns (over-engineering, global state, unnecessary async)

Use cases

Good for
  • Designing a new filter module for a command family (git, cargo, pytest)
  • Refactoring complex configuration structs to use Builder pattern
  • Parsing multi-section output where behavior changes based on context (failure summaries, test phases)
  • Adding type safety to command names and token counts to prevent silent bugs
  • Building a dynamic filter registry if RTK expands beyond static match dispatch
Who it's for
  • Rust developers building or maintaining RTK filter modules
  • Contributors designing new CLI command handlers
  • Teams refactoring existing RTK code for clarity and safety

design-patterns FAQ

When should I use Builder vs. a simple struct literal?

Use Builder when a struct has 4+ optional fields with defaults. For 1–3 fields with obvious meaning, a struct literal is simpler and less over-engineering.

Should I use Trait Objects for dynamic dispatch in RTK?

Only if your match arm count exceeds ~20 commands. RTK's current static dispatch in main.rs is intentional—zero overhead and easy to trace. Move to trait objects only if the filter registry becomes dynamic (e.g., TOML-loaded plugins).

How do I add methods to types I don't own, like &str?

Use the Extension Trait pattern. Define a trait with your custom methods, implement it for the external type, and use it anywhere that type is in scope.

What's the difference between State Machine and Strategy patterns?

State Machine models sequential phases where context changes behavior (parsing multi-section output). Strategy is for swappable algorithms at the same level (compact vs. verbose filtering mode).

Do I need to implement Drop for RAII?

Only if your resource requires explicit cleanup (e.g., deleting temp files). If the resource persists intentionally (like RTK's tee files), Drop is not needed.

Full instructions (SKILL.md)

Source of truth, from rtk-ai/rtk.


name: design-patterns description: Rust design patterns for RTK. Newtype, Builder, RAII, Trait Objects, State Machine. Applied to CLI filter modules. Use when designing new modules or refactoring existing ones. triggers:

  • "design pattern"
  • "how to structure"
  • "best pattern for"
  • "refactor to pattern" allowed-tools:
  • Read
  • Grep
  • Glob effort: medium tags: [rust, design-patterns, architecture, newtype, builder, rtk]

RTK Rust Design Patterns

Patterns that apply to RTK's filter module architecture. Focused on CLI tool patterns, not web/service patterns.

Pattern 1: Newtype (Type Safety)

Use when: wrapping primitive types to prevent misuse (command names, paths, token counts).

// Without Newtype — easy to mix up
fn track(input_tokens: usize, output_tokens: usize) { ... }
track(output_tokens, input_tokens);  // Silent bug!

// With Newtype — compile error on swap
pub struct InputTokens(pub usize);
pub struct OutputTokens(pub usize);
fn track(input: InputTokens, output: OutputTokens) { ... }
track(OutputTokens(100), InputTokens(400));  // Compile error ✅
// Practical RTK example: command name validation
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 }
}

Pattern 2: Builder (Complex Configuration)

Use when: a struct has 4+ optional fields, many with defaults.

#[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 }
}

// Usage — readable, no positional arg confusion
let config = FilterConfig::new()
    .max_lines(50)
    .strip_ansi(true)
    .show_warnings(false);

When NOT to use Builder: if the struct has 1-3 fields with obvious meaning. Over-engineering for simple cases.

Pattern 3: State Machine (Parser/Filter Flows)

Use when: parsing multi-section output (test results, build output) where context changes behavior.

// RTK example: pytest output parsing
#[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")
}

Pattern 4: Trait Object (Command Dispatch)

Use when: different command families need the same interface. Avoids massive match arms.

// Define a common interface for filters
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 currently uses match-based dispatch in main.rs (simpler, no dynamic dispatch overhead)
// Trait objects are useful if filter registry becomes dynamic (e.g., TOML-loaded plugins)

Note: RTK's current match dispatch in main.rs is intentional — static dispatch, zero overhead. Only move to trait objects if the match arm count exceeds ~20 commands.

Pattern 5: RAII (Resource Management)

Use when: managing resources that need cleanup (temp files, SQLite connections).

// RTK tee.rs — RAII for temp output files
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 }
}

// No explicit cleanup needed — file persists intentionally (rotation handled separately)
// If cleanup were needed: impl Drop { fn drop(&mut self) { let _ = fs::remove_file(&self.path); } }

Pattern 6: Strategy (Swappable Filter Logic)

Use when: a command has multiple filtering modes (e.g., compact vs. verbose).

pub enum FilterMode {
    Compact,    // Show only failures/errors
    Summary,    // Show counts + top errors
    Full,       // Pass through unchanged
}

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(),
    }
}

Pattern 7: Extension Trait (Add Methods to External Types)

Use when: you need to add methods to types you don't own (like &str for RTK-specific parsing).

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()
    }
}

// Usage
if line.is_error_line() { ... }
let tokens = output.token_count();

RTK Pattern Selection Guide

SituationPatternAvoid
New *_cmd.rs filter moduleStandard module pattern (see CLAUDE.md)Over-abstracting
4+ optional config fieldsBuilderStruct literal
Multi-phase output parsingState MachineNested if/else
Type-safe wrapper around stringNewtypeRaw String
Adding methods to &strExtension TraitFree functions
Resource with cleanupRAII / DropManual cleanup
Dynamic filter registryTrait ObjectMatch sprawl

Anti-Patterns in RTK Context

// ❌ Generic over-engineering for one command
pub trait Filterable<T: CommandArgs + Send + Sync + 'static> { ... }

// ✅ Just write the function
pub fn filter_git_log(input: &str) -> Result<String> { ... }

// ❌ Singleton registry with global state
static FILTER_REGISTRY: Mutex<HashMap<String, Box<dyn Filter>>> = ...;

// ✅ Match in main.rs — simple, zero overhead, easy to trace

// ❌ Async traits for "future-proofing"
#[async_trait]
pub trait Filter { async fn apply(&self, input: &str) -> Result<String>; }

// ✅ Synchronous — RTK is single-threaded by design
pub trait Filter { fn apply(&self, input: &str) -> Result<String>; }