Skip to content

Latest commit

 

History

History
1258 lines (961 loc) · 32 KB

File metadata and controls

1258 lines (961 loc) · 32 KB

Coding Tutor Principles (Rust Edition)

These principles guide how technical tutorials and code should be written for this Rust project. Use this as a reference when creating tutorials, explaining concepts, or writing code.

Feed this document to Claude at the start of coding sessions to maintain consistent quality and approach.

Core Philosophy

Goal: Help developers understand and implement features efficiently in Rust, writing maintainable, idiomatic code without over-engineering.

Values:

  • Clarity over cleverness
  • Simplicity over flexibility
  • Working code over theory
  • Understanding over memorization
  • Rust idioms over porting patterns from other languages

Key Principles

1. Top-Down Approach

Start with the big picture, then drill down into details.

Structure:

  1. Show the end goal - What are we building?
  2. Explain the architecture - How do components fit together?
  3. Provide implementation - Actual working code
  4. Cover edge cases - Optional advanced topics

Example:

## Building a Task Tracker CLI

**Goal:** Command-line tool where users can add, list, and complete tasks with an interactive TUI.

**Architecture:**
┌──────────────┐
│ CLI Parser   │ (clap - command parsing)
└──────┬───────┘
       ↓
┌──────────────┐
│ TUI Layer    │ (ratatui - interactive display)
└──────┬───────┘
       ↓
┌──────────────┐
│ Task Manager │ (business logic)
└──────┬───────┘
       ↓
┌──────────────┐
│ Storage      │ (JSON file persistence)
└──────────────┘

**Implementation:**
[Complete working code...]

**Advanced:**
- Cloud sync via API
- Task dependencies
- Recurring tasks

Why this works:

  • Gives context before diving into code
  • Shows how pieces connect
  • Demonstrates layered architecture common in Rust CLI apps
  • Allows skipping advanced topics if not needed

2. Write Idiomatic Rust

Follow Rust conventions and embrace the language's philosophy. This is critical for writing maintainable Rust code.

Rust Idioms:

  • ✅ Use Result<T, E> for errors, not exceptions or null
  • ✅ Leverage ownership and borrowing instead of cloning everywhere
  • ✅ Use pattern matching with match and if let
  • ✅ Prefer iterators over manual loops
  • ✅ Use traits for abstraction, not inheritance
  • ✅ Follow naming conventions: snake_case for functions/variables, CamelCase for types
  • ✅ Use ? operator for error propagation
  • ✅ Prefer &str for string slices, String for owned strings
  • ✅ Use Option<T> instead of null/None checks
  • ✅ Return early with guard clauses instead of deep nesting

For beginners:

When you're new to Rust, these patterns may feel unfamiliar. That's normal! Don't fight the language:

  • Compiler errors are your friend - read them carefully
  • If you're cloning everywhere, there's probably a better way
  • If you're using unwrap() frequently, use proper error handling instead
  • When the borrow checker complains, it's preventing real bugs

Example:

// Good: Idiomatic Rust with Result, borrowing, and iterators
pub fn process_items(items: &[Item]) -> Result<Vec<String>, ProcessError> {
    items
        .iter()
        .filter(|item| item.is_active)
        .map(|item| item.format())
        .collect()
}

// Bad: Fighting Rust's idioms
pub fn process_items(items: Vec<Item>) -> Vec<String> {
    let mut result = Vec::new();
    for i in 0..items.len() {
        let item = items[i].clone();  // Unnecessary clone
        if item.is_active {
            result.push(item.format().unwrap());  // Panics on error
        }
    }
    result
}

Common Rust patterns for beginners:

// Pattern 1: Use enums to represent states
#[derive(Debug)]
pub enum ConnectionState {
    Disconnected,
    Connecting { retry_count: u32 },
    Connected { session_id: String },
    Failed { reason: String },
}

impl ConnectionState {
    pub fn is_active(&self) -> bool {
        matches!(self, Self::Connected { .. })
    }
}

// Pattern 2: Result with ? operator for error propagation
fn load_tasks() -> Result<Vec<Task>, Box<dyn std::error::Error>> {
    let contents = std::fs::read_to_string("tasks.json")?;
    let tasks = serde_json::from_str(&contents)?;
    Ok(tasks)
}

// Pattern 3: Builder pattern with consuming methods
pub struct TaskBuilder {
    title: String,
    description: Option<String>,
    priority: Priority,
}

impl TaskBuilder {
    pub fn new(title: impl Into<String>) -> Self {
        Self {
            title: title.into(),
            description: None,
            priority: Priority::Normal,
        }
    }

    pub fn description(mut self, desc: impl Into<String>) -> Self {
        self.description = Some(desc.into());
        self
    }

    pub fn priority(mut self, priority: Priority) -> Self {
        self.priority = priority;
        self
    }

    pub fn build(self) -> Task {
        Task {
            title: self.title,
            description: self.description,
            priority: self.priority,
        }
    }
}

// Usage:
let task = TaskBuilder::new("Write docs")
    .description("Update the README")
    .priority(Priority::High)
    .build();

Why this matters:

  • Rust's design prevents entire classes of bugs (null pointers, data races, use-after-free)
  • Idiomatic code is easier for other Rust developers to understand
  • Fighting the language leads to verbose, buggy code
  • The compiler is teaching you to write better code

3. Proper Componentization

Break code into logical, reusable pieces, but avoid premature abstraction.

Good componentization:

  • ✅ Separate concerns (UI, logic, API, storage)
  • ✅ Each module has a single responsibility
  • ✅ Clear interfaces between modules (use traits)
  • ✅ Easy to test independently
  • ✅ Reusable when actually needed

Bad componentization:

  • ❌ Over-abstraction (generic handlers for one-time use)
  • ❌ God structs that do everything
  • ❌ Excessive trait hierarchies
  • ❌ Excessive indirection
  • ❌ "Future-proofing" that never gets used

Rule of thumb:

  • Once: Inline it
  • Twice: Consider extracting
  • Three times: Definitely extract

Example:

// Good: Clear separation via modules and focused types
// src/storage/mod.rs
pub struct TaskStore {
    path: PathBuf,
}

impl TaskStore {
    pub fn load(&self) -> Result<Vec<Task>, StorageError> {
        let contents = std::fs::read_to_string(&self.path)?;
        Ok(serde_json::from_str(&contents)?)
    }

    pub fn save(&self, tasks: &[Task]) -> Result<(), StorageError> {
        let json = serde_json::to_string_pretty(tasks)?;
        std::fs::write(&self.path, json)?;
        Ok(())
    }
}

// src/manager/mod.rs
pub struct TaskManager {
    store: TaskStore,
}

impl TaskManager {
    pub fn add_task(&mut self, task: Task) -> Result<(), ManagerError> {
        let mut tasks = self.store.load()?;
        tasks.push(task);
        self.store.save(&tasks)?;
        Ok(())
    }

    pub fn list_active(&self) -> Result<Vec<&Task>, ManagerError> {
        let tasks = self.store.load()?;
        Ok(tasks.iter().filter(|t| !t.completed).collect())
    }
}

// Bad: God struct doing everything
pub struct TaskManager {
    // Also handles CLI parsing, rendering, validation, logging, metrics, etc.
    // Too many responsibilities in one place
}

Rust-specific tip:

Use modules (mod) to organize code by domain, making each module's public API clear:

// src/lib.rs or src/main.rs
mod storage;
mod manager;
mod cli;
mod ui;

pub use manager::TaskManager;
pub use storage::TaskStore;

Why this matters:

  • Easier to understand small, focused modules
  • Can change one part without breaking others
  • Testing is simpler
  • But over-abstraction makes code hard to follow

4. No Unnecessary Complexity

Keep it simple. Don't add features "just in case."

Avoid:

  • ❌ Configuration for things that don't need configuring
  • ❌ Abstractions for "future flexibility" that may never come
  • ❌ Complex trait hierarchies for simple operations
  • ❌ Generic utilities that could be one-liners
  • ❌ Over-engineered error handling for impossible scenarios
  • ❌ Elaborate state machines for simple flows

Embrace:

  • ✅ Direct, straightforward code
  • ✅ Clear, explicit logic
  • ✅ Simple data structures (structs, enums)
  • ✅ Standard patterns
  • ✅ Error handling only where errors can actually occur

Example:

// Good: Simple and clear
pub fn format_date(date: &DateTime<Utc>) -> String {
    date.format("%Y-%m-%d").to_string()
}

// Bad: Over-engineered
pub struct DateFormatter {
    config: FormatterConfig,
    plugins: Vec<Box<dyn Plugin>>,
}

impl DateFormatter {
    pub fn format(&self, date: &DateTime<Utc>, options: Option<FormatOptions>) -> String {
        // 50 lines of complexity for basic date formatting
    }
}

Rust-specific tip:

Rust's type system lets you express constraints at compile time. Use it, but don't go overboard:

// Good: Simple generic constraint
pub fn print_item<T: Display>(item: &T) {
    println!("{}", item);
}

// Overkill for most cases
pub fn print_item<T>(item: &T)
where
    T: Display + Debug + Clone + Send + Sync + 'static,
{
    println!("{}", item);
}

Why this matters:

  • Simpler code is easier to understand and maintain
  • Less code means fewer bugs
  • Complexity should match the actual problem
  • Rust's zero-cost abstractions mean you don't pay for what you don't use

5. Complete Working Code Examples

Provide real, runnable code rather than pseudo-code or fragments.

Do this:

// src/task.rs
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::fs;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
    pub id: u64,
    pub title: String,
    pub completed: bool,
}

#[derive(Debug)]
pub enum TaskError {
    IoError(std::io::Error),
    ParseError(serde_json::Error),
    NotFound(u64),
}

impl From<std::io::Error> for TaskError {
    fn from(err: std::io::Error) -> Self {
        TaskError::IoError(err)
    }
}

impl From<serde_json::Error> for TaskError {
    fn from(err: serde_json::Error) -> Self {
        TaskError::ParseError(err)
    }
}

pub struct TaskRepo {
    path: PathBuf,
}

impl TaskRepo {
    pub fn new(path: PathBuf) -> Self {
        Self { path }
    }

    pub fn load_all(&self) -> Result<Vec<Task>, TaskError> {
        if !self.path.exists() {
            return Ok(Vec::new());
        }
        let contents = fs::read_to_string(&self.path)?;
        let tasks = serde_json::from_str(&contents)?;
        Ok(tasks)
    }

    pub fn save_all(&self, tasks: &[Task]) -> Result<(), TaskError> {
        let json = serde_json::to_string_pretty(tasks)?;
        fs::write(&self.path, json)?;
        Ok(())
    }

    pub fn find_by_id(&self, id: u64) -> Result<Task, TaskError> {
        let tasks = self.load_all()?;
        tasks.into_iter()
            .find(|t| t.id == id)
            .ok_or(TaskError::NotFound(id))
    }
}

Not this:

// Pseudo-code
struct TaskRepo {
    // ... fields
}

impl TaskRepo {
    fn load_all() {
        // Read from file
        // Parse JSON
        // Return tasks
    }
}

For Rust beginners:

Complete examples help you understand:

  • What traits need to be derived (Serialize, Deserialize, Debug, Clone)
  • How to define custom error enums
  • How to implement From for error conversion
  • Error propagation with ? operator
  • When to return Result<T, E>
  • Pattern matching with find() and ok_or()

Why this matters:

  • Developers can copy-paste and run immediately
  • Shows all the details (imports, derives, error handling)
  • No ambiguity about implementation
  • Demonstrates real Rust patterns (error enums, From trait, Result)

6. Use Popular Crates (Don't Reinvent the Wheel)

Leverage well-tested, community-maintained crates for common tasks.

Use existing solutions for:

  • ✅ HTTP requests (reqwest, hyper)
  • ✅ CLI frameworks (clap, structopt)
  • ✅ Terminal UI (ratatui, crossterm)
  • ✅ Testing (cargo test, built-in)
  • ✅ Date/time (chrono)
  • ✅ Serialization (serde, serde_json, toml)
  • ✅ Error handling (anyhow, thiserror)
  • ✅ Async runtime (tokio, async-std)

Consider building custom when:

  • Crate adds too much complexity for your needs
  • No good crate exists for your specific use case
  • Crate is unmaintained or has security issues
  • Performance is critical and crate is too slow

Example:

// Good: Use clap for CLI argument parsing
use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "tasks")]
#[command(about = "A simple task manager")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Add a new task
    Add { title: String },
    /// List all tasks
    List,
    /// Complete a task
    Complete { id: u64 },
}

fn main() {
    let cli = Cli::parse();
    match cli.command {
        Commands::Add { title } => add_task(&title),
        Commands::List => list_tasks(),
        Commands::Complete { id } => complete_task(id),
    }
}

// Bad: Manual argument parsing
fn main() {
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 2 {
        eprintln!("Usage: tasks <command>");
        return;
    }
    // 100+ lines of manual parsing, validation, help text...
}

Rust-specific tip:

Add crates to Cargo.toml:

[dependencies]
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
thiserror = "1.0"

# Optional: for async operations
tokio = { version = "1.0", features = ["full"] }

# Optional: for HTTP
reqwest = { version = "0.12", features = ["json"] }

Why this matters:

  • Popular crates are battle-tested
  • Community provides support and updates
  • Security vulnerabilities get fixed
  • Focus on your unique business logic, not infrastructure
  • Rust's package manager (Cargo) makes dependencies easy

7. Explain Why, Not Just How

Help learners understand the reasoning behind decisions.

When introducing a new concept:

  • Explain the problem it solves
  • Show alternatives and trade-offs
  • Justify the chosen approach

Good:

We use an enum to represent task status instead of strings or booleans because:

```rust
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TaskStatus {
    Todo,
    InProgress { started_at: SystemTime },
    Done { completed_at: SystemTime },
}
  1. Type safety - Compiler prevents invalid states (can't have "Dne" typo)
  2. Pattern matching - Exhaustive matching catches missing cases at compile time
  3. Associated data - Each variant can carry relevant data
  4. Self-documenting - Clear what states exist without reading docs

Alternative: Could use String status, but then:

  • Risk of typos and invalid values
  • Need runtime validation
  • Can't attach variant-specific data cleanly

In Rust, enums are powerful because:

  • Zero-cost abstraction - No runtime overhead vs. manual tagging
  • Match exhaustiveness - Compiler ensures you handle all cases
  • Memory efficiency - Uses only space needed for largest variant + discriminant

**Not enough:**

```markdown
Here's the TaskStatus enum.

For Rust beginners, also explain:

  • Why enums are better than booleans/strings for state
  • How pattern matching provides compile-time guarantees
  • When to use Option vs custom enums
  • How to add data to enum variants

When to skip explanations:

  • Reader has stated knowledge level
  • Concept is industry-standard (e.g., "REST API")
  • Documentation is reference material, not tutorial
  • Rust basics already covered in earlier tutorials

Why this matters:

  • Understanding principles > memorizing patterns
  • Developers can apply knowledge to new situations
  • Easier to evaluate if approach fits their needs
  • Rust's unique features (ownership, traits) need explanation for beginners

8. Progressive Disclosure

Start simple, add complexity only when needed.

Tutorial flow:

## Step 1: Basic Login (minimal, working)

[Simple code that works - even if it uses `.unwrap()` temporarily]

## Step 2: Add Proper Error Handling (practical necessity)

[Code with `Result` and `?` operator]

## Step 3: Add Loading States (better UX)

[Code with async progress indicators]

## Step 4: Make It Safe and Robust

[Code with proper ownership, no panics]

## Optional: Advanced Topics

- Token refresh with async
- Session management
- OAuth integration

For Rust beginners:

It's okay to start with .unwrap() in early examples to focus on the main concept, then show proper error handling later. Just mark it clearly:

// Step 1: Focus on the main logic (we'll improve error handling next)
let config = load_config().unwrap();  // TODO: proper error handling in step 2

Why this works:

  • Can stop at any level if it meets needs
  • Not overwhelmed by advanced features upfront
  • Clear progression of complexity
  • Beginners can learn ownership gradually

9. Provide Context

Show how pieces fit into the larger system.

Always include:

  • ✅ File paths (// src/api/client.rs)
  • ✅ All necessary imports and use statements
  • ✅ Cargo.toml dependencies needed
  • ✅ Where the code runs (binary, library, module)
  • ✅ What other parts interact with this code
  • ✅ Module structure context

Example:

// src/commands/add.rs
// This command is called from src/main.rs when user runs: tasks add "Buy milk"
// It uses TaskManager from src/manager/mod.rs

use crate::manager::TaskManager;
use crate::task::{Task, TaskStatus};
use anyhow::Result;

pub fn execute(manager: &mut TaskManager, title: String) -> Result<()> {
    let task = Task {
        id: manager.next_id(),
        title,
        status: TaskStatus::Todo,
    };

    manager.add(task)?;
    println!("✓ Task added successfully");
    Ok(())
}

Cargo.toml context:

[package]
name = "tasks"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"

Module structure context:

src/
├── main.rs           # CLI entry point, uses clap
├── lib.rs            # Public API for library users
├── task.rs           # Task struct and TaskStatus enum
├── manager/
│   ├── mod.rs        # TaskManager struct
│   └── filters.rs    # Query filtering logic
├── storage/
│   ├── mod.rs        # TaskStore for persistence
│   └── json.rs       # JSON serialization
└── commands/
    ├── mod.rs
    ├── add.rs        ← We are here
    ├── list.rs
    └── complete.rs

Why this matters:

  • Understand where code lives in the project
  • Know how to import and use the code
  • See the bigger picture
  • Rust's module system can be confusing for beginners

10. Use Consistent Patterns

Follow project conventions and established Rust patterns.

In Rust projects:

  • Commands: src/commands/<group>/<command>.rs
  • Types: src/types.rs or co-located in modules
  • Modules: src/<module>/mod.rs or src/<module>.rs
  • Tests: #[cfg(test)] mod tests in same file, or tests/ directory
  • Binaries: src/main.rs or src/bin/<name>.rs
  • Libraries: src/lib.rs

Rust conventions:

  • snake_case for functions, variables, modules
  • CamelCase for types, structs, enums, traits
  • SCREAMING_SNAKE_CASE for constants
  • Use Result<T, E> for fallible operations
  • Use Option<T> for optional values
  • Implement Display for user-facing output
  • Implement Debug for debugging
  • Use #[derive(...)] for common traits

Example:

// Following Rust conventions
const MAX_RETRIES: u32 = 3;  // SCREAMING_SNAKE_CASE

pub struct ApiClient {  // CamelCase for types
    base_url: String,   // snake_case for fields
}

impl ApiClient {
    pub fn new(base_url: String) -> Self {  // snake_case for methods
        Self { base_url }
    }

    pub async fn fetch_data(&self) -> Result<Data, ApiError> {  // Result for errors
        // Implementation
    }
}

Why this matters:

  • Predictable structure
  • Easier onboarding
  • Consistent codebase
  • Follows Rust community standards
  • Cargo and tooling expect these patterns

11. Test-Friendly Code

Write code that's easy to test, and show how to test it.

Testable code characteristics:

  • Dependencies injected (not hardcoded)
  • Pure functions where possible
  • Side effects isolated
  • Clear input/output contracts
  • Use traits for mockability

Example:

// src/manager.rs
use crate::storage::Storage;
use crate::task::{Task, TaskStatus};

pub struct TaskManager<S> {
    storage: S,
    next_id: u64,
}

impl<S: Storage> TaskManager<S> {
    pub fn new(storage: S) -> Self {
        Self {
            storage,
            next_id: 1,
        }
    }

    pub fn add(&mut self, mut task: Task) -> Result<(), String> {
        task.id = self.next_id;
        self.next_id += 1;
        self.storage.save(&task)?;
        Ok(())
    }

    pub fn complete(&mut self, id: u64) -> Result<(), String> {
        let mut task = self.storage.load(id)?;
        task.status = TaskStatus::Done {
            completed_at: std::time::SystemTime::now(),
        };
        self.storage.save(&task)?;
        Ok(())
    }

    pub fn count_active(&self) -> usize {
        self.storage.all()
            .into_iter()
            .filter(|t| !matches!(t.status, TaskStatus::Done { .. }))
            .count()
    }
}

// Test with in-memory storage
#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    struct InMemoryStorage {
        tasks: HashMap<u64, Task>,
    }

    impl InMemoryStorage {
        fn new() -> Self {
            Self { tasks: HashMap::new() }
        }
    }

    impl Storage for InMemoryStorage {
        fn save(&mut self, task: &Task) -> Result<(), String> {
            self.tasks.insert(task.id, task.clone());
            Ok(())
        }

        fn load(&self, id: u64) -> Result<Task, String> {
            self.tasks.get(&id).cloned()
                .ok_or_else(|| format!("Task {} not found", id))
        }

        fn all(&self) -> Vec<Task> {
            self.tasks.values().cloned().collect()
        }
    }

    #[test]
    fn test_add_task() {
        let storage = InMemoryStorage::new();
        let mut manager = TaskManager::new(storage);

        let task = Task {
            id: 0, // Will be overwritten
            title: "Test task".to_string(),
            status: TaskStatus::Todo,
        };

        assert!(manager.add(task).is_ok());
        assert_eq!(manager.count_active(), 1);
    }

    #[test]
    fn test_complete_task() {
        let storage = InMemoryStorage::new();
        let mut manager = TaskManager::new(storage);

        let task = Task {
            id: 0,
            title: "Test".to_string(),
            status: TaskStatus::Todo,
        };
        manager.add(task).unwrap();

        assert!(manager.complete(1).is_ok());
        assert_eq!(manager.count_active(), 0);
    }
}

Key testing principles in Rust:

  • Use traits to abstract dependencies (file system, network, etc.)
  • Create simple in-memory implementations for tests
  • Test business logic without external dependencies
  • Use #[cfg(test)] to keep test code separate

Why this matters:

  • Confidence in refactoring
  • Catch bugs at compile time (Rust's type system)
  • Living documentation
  • Rust's testing is built-in with cargo test

12. Provide Multiple Learning Paths

Different people learn differently.

Include:

  • Quick Start: "Just show me the code"
  • Tutorial: Step-by-step walkthrough
  • Explanation: Why and how it works (especially Rust-specific features)
  • Reference: Complete API documentation

Example structure:

## Quick Start (for experienced Rust developers)

[Complete code example]

## Tutorial (for Rust beginners)

### Step 1: Understanding Ownership in This Context
[Explain relevant Rust concepts]

### Step 2: Setting Up the Structure
[Build it step by step]

### Step 3: Adding Functionality
[Complete the implementation]

## How It Works (for the curious)

[Deep dive into internals, lifetime annotations, trait bounds]

## Common Rust Pitfalls

[Ownership issues, borrow checker errors, common mistakes]

## API Reference (for reference)

[Complete function signatures, trait bounds, documentation]

Tutorial Structure Template

Use this template for technical tutorials:

# [Feature Name]

Brief description of what we're building and why (1-2 sentences).

## What We're Building

- Clear goal statement
- What problem does this solve?
- Expected outcome

## Architecture

[Visual diagram or description of components and their relationships]

**Why this architecture:**
- Reason for design choices
- Trade-offs considered
- How ownership flows through the system

## Prerequisites

**Required:**
- Rust version (e.g., "Rust 1.70 or later")
- Crates to add to Cargo.toml
- Previous tutorials to complete

**Assumed knowledge:**
- "Basic Rust" (can skip explanations of ownership basics)
- "Familiar with async/await" (can skip tokio basics)

## Implementation

### Step 1: [Setup/Foundation]
**Goal:** [What this step achieves]

[Complete code with file path]

**Why this approach:**
[Brief explanation, including Rust-specific reasoning]

**Cargo.toml additions:**
```toml
[dependencies]
required-crate = "version"

Step 2: [Core Logic]

[Same structure...]

Common Rust issues at this step:

  • [Borrow checker errors and how to fix them]
  • [Lifetime annotations explained]

Step 3: [Integration]

[Same structure...]

Testing

How to test the implementation (with examples).

#[cfg(test)]
mod tests {
    // Complete test examples
}

Run with: cargo test

Common Issues

Issue: [Problem description]

  • Solution: [How to fix]
  • Why it happens: [Explanation, especially Rust-specific]
  • Compiler says: [What error message looks like]

Issue: "cannot borrow as mutable"

  • Solution: Use interior mutability (RefCell, Mutex) or restructure ownership
  • Why it happens: Violating borrowing rules
  • Compiler says: cannot borrow 'x' as mutable because it is also borrowed as immutable

Enhancements (Optional)

Ideas for extending the feature.

Next Steps

  • What to build next
  • How to extend further
  • Related tutorials

References

  • Rust documentation links
  • Related crates documentation
  • External resources

## Code Examples Best Practices

### Always Include:

- ✅ File path: `// src/api/client.rs`
- ✅ All necessary `use` statements
- ✅ Complete type definitions with derives
- ✅ Error handling with `Result`
- ✅ Comments for non-obvious Rust-specific logic
- ✅ Cargo.toml dependencies needed

### Avoid:

- ❌ `// ... rest of the code`
- ❌ `// TODO: implement this`
- ❌ Partial code that won't compile
- ❌ Unexplained `.unwrap()` calls (explain or use `?`)
- ❌ Over-commenting obvious code
- ❌ Using `panic!` without explanation

### Format:

```rust
// src/path/to/file.rs
use std::fmt;
use serde::{Deserialize, Serialize};

/// Does something useful
///
/// # Arguments
/// * `input` - Description of input
///
/// # Returns
/// Description of return value
///
/// # Errors
/// Returns error if X happens
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    pub field: String,
}

impl Config {
    pub fn new(field: String) -> Self {
        Self { field }
    }
}

impl fmt::Display for Config {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Config: {}", self.field)
    }
}

Explaining Concepts: When to Elaborate vs. Skip

Elaborate (explain why + how) when:

  • ✅ Concept is new to the reader (check prerequisites)
  • ✅ Concept is project-specific
  • ✅ Concept is Rust-specific (ownership, lifetimes, traits)
  • ✅ Multiple approaches exist (explain trade-offs)
  • ✅ Design decisions are non-obvious
  • ✅ Common pitfalls exist
  • ✅ Borrow checker implications

Can skip or briefly mention when:

  • ✅ Reader has stated knowledge level
  • ✅ Industry-standard pattern (e.g., REST, MVC)
  • ✅ Well-documented in official Rust docs (link instead)
  • ✅ Concept already explained in previous tutorial

Example:

// Good: Assumes basic Rust knowledge, explains design
We use dependency injection with traits here to make the code testable:

```rust
pub struct ApiClient<H: HttpClient> {
    http_client: H,  // Trait bound allows mocking
}

This allows us to pass a mock implementation in tests without changing production code. The trait bound H: HttpClient means we can use any type that implements HttpClient.

Alternative: Could use concrete types, but would need #[cfg(test)] conditionals everywhere.


// Bad: Over-explaining Rust basics to intermediate developers
Rust uses angle brackets for generics. The `H: HttpClient` syntax means H is a generic
type parameter that must implement the HttpClient trait. Traits are like interfaces...

When to Deviate

These principles are guidelines, not laws. Deviate when:

  1. Security concerns: Add complexity if needed for security
  2. Performance critical: Optimize even if it adds complexity (Rust excels here)
  3. External requirements: Library/API constraints may require specific patterns
  4. Team standards: Follow agreed-upon conventions
  5. Production systems: Add robustness appropriate to the context
  6. Reader expertise: Adjust depth based on stated knowledge level
  7. Unsafe code needed: Use unsafe when truly necessary, but explain thoroughly

Always explain why you're deviating from these principles.

Checklist for Tutorials

Before publishing a tutorial, verify:

  • Starts with clear goal and overview
  • Shows architecture/component diagram
  • States prerequisites and assumed Rust knowledge level
  • Shows complete, compilable code
  • Includes all use statements and Cargo.toml dependencies
  • Explains why, not just what (for new concepts)
  • Explains Rust-specific features (ownership, lifetimes, traits)
  • Uses popular crates appropriately
  • Follows Rust naming conventions
  • Uses idiomatic Rust patterns
  • Includes proper error handling with Result
  • Provides testing examples
  • Has troubleshooting section for common Rust errors
  • Code is properly modularized
  • No unnecessary complexity
  • Clear next steps provided
  • Uses top-down structure
  • Addresses common borrow checker issues

Checklist for Code

Before submitting code, verify:

  • Compiles without warnings (cargo build)
  • Passes cargo clippy without warnings
  • Formatted with cargo fmt
  • Uses popular crates for common tasks
  • Properly modularized (single responsibility)
  • No premature abstraction
  • Uses traits for abstraction when appropriate
  • Dependencies injected for testability
  • Includes tests (cargo test passes)
  • Has clear, focused functions/structs
  • Proper error handling with Result<T, E>
  • No .unwrap() in production code without justification
  • Uses appropriate ownership (&, &mut, owned)
  • Follows Rust naming conventions
  • Follows project patterns and structure
  • Documented why for non-obvious decisions
  • No clippy warnings
  • Idiomatic Rust code

Summary

Remember these core values:

  1. Top-down - Big picture first, details later
  2. Idiomatic Rust - Embrace ownership, traits, Result, iterators
  3. Proper componentization - Extract at 3 uses, not before
  4. No unnecessary complexity - Simple > clever
  5. Complete working examples - Compilable, not pseudo-code
  6. Use popular crates - Don't reinvent wheels
  7. Explain why - Reasoning > mechanics (especially for Rust-specific features)

Goal: Help developers be productive quickly while writing maintainable, idiomatic Rust code.

Motto: Clarity and simplicity trump cleverness. Let the compiler help you write better code.

For beginners: The borrow checker is your friend. Compiler errors teach you to write safer code.