# Twyg Refactoring Plan: Rust Best Practices Alignment
**Date:** 2026-01-14
**Project:** twyg v0.5.0
**Total Lines of Code:** ~308 lines
**Purpose:** Align codebase with modern Rust best practices and idioms
---
## Part 1: Analysis and Findings
### Executive Summary
The twyg logging library is a small, focused crate (~308 LOC) that provides a simplified interface to `fern` for logging configuration. While functionally working, the codebase exhibits several anti-patterns and missed opportunities for idiomatic Rust. This analysis identifies **43 specific issues** across **10 major categories**, ranging from critical API design problems to minor stylistic improvements.
**Severity Breakdown:**
- 🔴 Critical (Breaking Changes): 15 issues
- 🟡 Important (Non-Breaking): 18 issues
- 🟢 Minor (Polish): 10 issues
---
### 1. Stringly-Typed APIs (Critical 🔴)
**Files Affected:** `level.rs`, `out.rs`, `opts.rs`, `logger.rs`
#### Issues Found:
**1.1 Log Levels as Strings (AP-53, AP-30, API-08)**
- **Location:** `src/level.rs:1-23`
- **Severity:** 🔴 Critical (Breaking)
- **Current Code:**
```rust
pub fn trace() -> Option<String> {
Some(String::from("trace"))
}
pub fn debug() -> Option<String> { ... }
```
- **Problem:**
- Using `Option<String>` for a finite set of log levels
- Runtime errors possible with typos (e.g., "deubg" vs "debug")
- Unnecessary allocations on every call
- Option always returns Some, making it misleading
- **Impact:** Type system can't catch log level errors at compile time
- **Best Practice Violated:** Anti-patterns AP-53, AP-30; API Design API-08
**1.2 Output Streams as Strings (AP-53, AP-30)**
- **Location:** `src/out.rs:1-10`
- **Severity:** 🔴 Critical (Breaking)
- **Current Code:**
```rust
pub const STDOUT: &str = "stdout";
pub const STDERR: &str = "stderr";
pub fn stdout() -> Option<String> {
Some(String::from(STDOUT))
}
```
- **Problem:**
- String-based output selection is error-prone
- Allocates unnecessarily
- Option wrapping adds no value (always Some)
- Magic string matching in logger.rs (line 40-42, 85)
- **Best Practice Violated:** Anti-patterns AP-53, AP-30
**1.3 Opts Struct Field Types (AP-30, API-08)**
- **Location:** `src/opts.rs:8-15`
- **Severity:** 🔴 Critical (Breaking)
- **Current Code:**
```rust
pub struct Opts {
pub coloured: bool,
pub file: Option<String>, pub level: Option<String>, pub report_caller: bool,
pub time_format: Option<String>,
}
```
- **Problem:** String types used where enums would provide type safety
---
### 2. Unnecessary Allocations and Cloning (Important 🟡)
**Files Affected:** `logger.rs`, `level.rs`, `out.rs`
#### Issues Found:
**2.1 Clone in Hot Path (AP-12, AP-18, AP-33, AP-65)**
- **Location:** `src/logger.rs:38,82`
- **Severity:** 🟡 Important
- **Current Code:**
```rust
dispatch = match self.opts.file.clone() { Some(opt) => match opt.as_str() {
},
};
fn stream(&self) -> Stream {
match self.opts.clone().file { }
}
```
- **Problem:**
- Cloning entire Opts struct unnecessarily
- Should borrow instead of clone
- **Best Practice Violated:** AP-12, AP-18, AP-33, AP-65
**2.2 String Allocations for Constants (AP-08, AP-17)**
- **Location:** `src/level.rs:1-23`, `src/out.rs:4-10`
- **Severity:** 🟡 Important
- **Current Code:**
```rust
pub fn trace() -> Option<String> {
Some(String::from("trace")) }
```
- **Problem:** Allocates String on every function call when &str would work
- **Impact:** Unnecessary heap allocations
**2.3 Repeated String Allocations (AP-08, AP-52)**
- **Location:** `src/logger.rs:138-149`
- **Severity:** 🟢 Minor
- **Current Code:**
```rust
fn get_opt_str(x: Option<&str>) -> String {
match x {
None => "??".to_string(),
Some(_) => x.unwrap().to_string(),
}
}
```
- **Problem:** Allocates String when formatting could be deferred
---
### 3. Unwrap and Panic Usage (Critical 🔴)
**Files Affected:** `logger.rs`
#### Issues Found:
**3.1 Unwrap in Library Code (AP-09, AP-58, AP-80, EH-04)**
- **Location:** `src/logger.rs:28,34,53,54,141`
- **Severity:** 🔴 Critical
- **Current Code:**
```rust
let mut dispatch = if self.opts.report_caller {
report_caller_logger(
self.format_ts(),
self.level_to_filter().unwrap(), self.stream(),
)
} else {
logger(
self.format_ts(),
self.level_to_filter().unwrap(), self.stream(),
)
};
let ts = match &self.opts.time_format {
None => opts::default_ts_format().unwrap(), Some(ts) => ts.to_string(),
};
Some(_) => x.unwrap().to_string(), ```
- **Problem:**
- Library code should never panic on user input
- `.unwrap()` provides no context when it fails
- Users can't handle errors gracefully
- **Impact:** Library will panic on invalid log levels instead of returning Result
- **Best Practice Violated:** AP-09, AP-58, AP-80, EH-04
**3.2 Missing Error Propagation (EH-04, ID-28)**
- **Location:** `src/logger.rs:24-46`
- **Severity:** 🔴 Critical
- **Current Code:** Functions use unwrap instead of returning Result
- **Problem:** Errors aren't propagated to caller
- **Expected:** `dispatch()` should return `Result<fern::Dispatch, Error>` (it does!) but internal functions should too
---
### 4. Option Anti-Patterns (Important 🟡)
**Files Affected:** `level.rs`, `out.rs`, `opts.rs`
#### Issues Found:
**4.1 Option<String> Always Returns Some (AP-30, ID-30)**
- **Location:** `src/level.rs:1-23`, `src/out.rs:4-10`
- **Severity:** 🟡 Important (Breaking to fix properly)
- **Current Code:**
```rust
pub fn trace() -> Option<String> {
Some(String::from("trace")) }
```
- **Problem:**
- Option suggests it might be None, but it never is
- Misleading API - forces users to handle None case that never happens
- Wrapping in Option adds no value
- **Best Practice Violated:** AP-30, AP-44, ID-30
**4.2 Unnecessary Option Wrapping in Helpers (AP-30)**
- **Location:** `src/opts.rs:33-43`
- **Severity:** 🟢 Minor
- **Current Code:**
```rust
pub fn default_file() -> Option<String> {
Some(out::STDOUT.to_string()) }
pub fn default_level() -> Option<String> {
Some(DEFAULT_LEVEL.to_string()) }
```
- **Problem:** Helper functions return Option but always return Some
---
### 5. API Design Issues (Critical 🔴)
**Files Affected:** `opts.rs`, `lib.rs`
#### Issues Found:
**5.1 Public Fields Without Validation (AP-06, AP-71)**
- **Location:** `src/opts.rs:8-15`
- **Severity:** 🔴 Critical (Breaking)
- **Current Code:**
```rust
pub struct Opts {
pub coloured: bool, pub file: Option<String>, pub level: Option<String>, pub report_caller: bool, pub time_format: Option<String>, }
```
- **Problem:**
- All fields are public - no encapsulation
- No validation of log levels
- No validation of time format strings
- Can't change internal representation later
- Can't add logging or validation when fields change
- **Impact:**
- Users can set invalid log levels: `Opts { level: Some("not-a-level".to_string()), .. }`
- Breaking change required to add validation later
- **Best Practice Violated:** AP-06, AP-71, API-06
**5.2 Builder Pattern Missing (API-10)**
- **Location:** `src/opts.rs`
- **Severity:** 🟡 Important
- **Current Code:** Direct struct initialization with public fields
- **Problem:**
- No builder pattern for complex configuration
- Users must know all fields and defaults
- **Best Practice Violated:** API-10, ID-09
**5.3 Accept Borrowed, Return Owned Not Followed (API-02)**
- **Location:** `src/lib.rs:50`
- **Severity:** 🟢 Minor
- **Current Code:**
```rust
pub fn setup(opts: Opts) -> Result<Logger, Error> { ... }
```
- **Problem:** Takes owned Opts but could accept reference
- **Note:** Actually acceptable here since we store opts in Logger
---
### 6. Error Handling Issues (Important 🟡)
**Files Affected:** `lib.rs`, `logger.rs`
#### Issues Found:
**6.1 Anyhow in Library API (EH-03, EH-07)**
- **Location:** `src/lib.rs:6,50`
- **Severity:** 🟡 Important (Best practice, not critical)
- **Current Code:**
```rust
use anyhow::{anyhow, Error, Result};
pub fn setup(opts: Opts) -> Result<Logger, Error> { ... }
```
- **Problem:**
- `anyhow::Error` in public library API
- Users can't match on specific error types
- Better practice: custom error enum with thiserror
- **Note:** Acceptable for applications but not ideal for libraries
- **Best Practice:** EH-03 (anyhow for apps only), EH-07 (custom errors for libraries)
**6.2 Error Messages Missing Context (EH-08, EH-09)**
- **Location:** `src/lib.rs:53,55`
- **Severity:** 🟢 Minor
- **Current Code:**
```rust
Err(e) => Err(anyhow!("couldn't set up Twyg logger ({:?}", e)),
Err(e) => Err(anyhow!("couldn't apply setup to Fern logger ({:?}", e)),
```
- **Problem:**
- Error messages don't provide actionable information
- Missing closing parenthesis in format string (syntax error!)
- Should use `{:?}` or `{}` consistently
- **Best Practice Violated:** EH-08
**6.3 Missing Error Documentation (EH-09)**
- **Location:** `src/lib.rs:12-49`
- **Severity:** 🟢 Minor
- **Current Code:** Doc comment exists but doesn't document errors
- **Problem:** No `# Errors` section explaining when function returns Err
---
### 7. Naming Convention Violations (Minor 🟢)
**Files Affected:** `logger.rs`
#### Issues Found:
**7.1 Unnecessary get_ Prefix (AP-76, ID-20)**
- **Location:** `src/logger.rs:138-149`
- **Severity:** 🟢 Minor
- **Current Code:**
```rust
fn get_opt_str(x: Option<&str>) -> String { ... }
fn get_opt_u32(x: Option<u32>) -> String { ... }
```
- **Problem:** `get_` prefix is unnecessary in Rust
- **Better Names:** `opt_str_or_placeholder()`, `opt_u32_or_placeholder()`
- **Best Practice Violated:** AP-76, ID-20
**7.2 Method Naming Inconsistency (ID-20)**
- **Location:** `src/logger.rs:59`
- **Severity:** 🟢 Minor
- **Current Code:**
```rust
pub fn level(&self) -> String { ... }
```
- **Problem:**
- Method is public but seems to be used internally
- Method name doesn't reflect what it returns (formatted timestamp with level?)
- Code looks like copy-paste error from `format_ts()` - uses timestamp formatting!
---
### 8. Testing and Quality Assurance (Critical 🔴)
**Files Affected:** Entire codebase
#### Issues Found:
**8.1 No Tests (Coverage Requirements)**
- **Location:** No test files exist
- **Severity:** 🔴 Critical
- **Current State:**
- No `tests/` directory
- No `#[cfg(test)]` modules in source files
- No integration tests
- No unit tests
- **Required:** 95%+ test coverage per CLAUDE-CODE-COVERAGE.md
- **Impact:**
- Can't verify behavior
- Can't safely refactor
- No regression detection
**8.2 Examples Not Comprehensive**
- **Location:** `examples/`
- **Severity:** 🟢 Minor
- **Current State:** 6 examples exist but don't cover error cases
- **Needed:** Examples showing error handling
---
### 9. Documentation Issues (Minor 🟢)
**Files Affected:** Multiple
#### Issues Found:
**9.1 Missing Module-Level Documentation (ID-19)**
- **Location:** All module files
- **Severity:** 🟢 Minor
- **Problem:** No module-level `//!` comments explaining purpose
**9.2 Incomplete Documentation (EH-09, ID-19)**
- **Location:** `src/lib.rs:12-49`
- **Severity:** 🟢 Minor
- **Current Code:** Good example exists but missing error documentation
- **Missing:**
- `# Errors` section
- `# Panics` section (though it shouldn't panic after refactor)
**9.3 Doc Example Could Be Better (ID-14)**
- **Location:** `src/lib.rs:26-45`
- **Severity:** 🟢 Minor
- **Problem:** Example uses panic but doesn't show Result handling
---
### 10. Code Organization and Structure (Minor 🟢)
**Files Affected:** `logger.rs`, `opts.rs`
#### Issues Found:
**10.1 Magic Numbers (AP-37)**
- **Location:** `src/opts.rs:5-6`
- **Severity:** 🟢 Minor
- **Current Code:**
```rust
const DEFAULT_LEVEL: &str = "error";
const DEFAULT_TS_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
```
- **Problem:** Constants are good, but should be public and documented
- **Better:** These should be part of the public API with doc comments
**10.2 Dead Code (logger.rs:59-65)**
- **Location:** `src/logger.rs:59-65`
- **Severity:** 🟡 Important
- **Current Code:**
```rust
pub fn level(&self) -> String {
let ts = match &self.opts.level {
None => opts::default_level().unwrap(),
Some(l) => l.to_string(),
};
Local::now().format(ts.as_str()).to_string() }
```
- **Problem:**
- Public method that doesn't make sense
- Appears to be copy-paste error from `format_ts()`
- Uses log level as time format string (bug!)
- Not called anywhere in the codebase
**10.3 Code Duplication (logger.rs:97-136)**
- **Location:** `src/logger.rs:97-136`
- **Severity:** 🟢 Minor
- **Problem:** `report_caller_logger` and `logger` functions are very similar
- **Better:** Could be refactored to reduce duplication
---
## Part 2: Phased Refactoring Plan
This plan is organized into phases that can be implemented incrementally. Each phase builds on the previous one and maintains backwards compatibility where possible.
---
### Phase 0: Preparation (Non-Breaking)
**Goal:** Set up infrastructure for safe refactoring
**Duration Estimate:** 1-2 hours
**Tasks:**
1. **Add Comprehensive Tests** 🔴
- Create test modules for each source file
- Achieve 95%+ code coverage
- Test all current behavior (even if buggy)
- This locks in current behavior before changes
**Files to Create:**
- `src/logger.rs` - add `#[cfg(test)] mod tests`
- `src/opts.rs` - add `#[cfg(test)] mod tests`
- `tests/integration_tests.rs`
**Success Criteria:**
- `cargo test` passes
- `cargo llvm-cov --html` shows 95%+ coverage
- All current behavior is tested
2. **Fix Obvious Bugs** 🔴
- Fix `logger.rs:59-65` (level method bug)
- Fix missing closing parenthesis in error messages (`lib.rs:53,55`)
**Verification:**
- Tests pass after fixes
- Clippy warnings reduced
3. **Add Linting Configuration** 🟢
- Create/update Makefile with `make format` and `make lint` targets
- Run `cargo clippy -- -D warnings`
- Run `cargo fmt`
**Success Criteria:**
- `make lint` passes
- `make format` produces no changes
**Deliverables:**
- Test suite with 95%+ coverage
- Bug fixes for obvious issues
- Clean linting
- Baseline for measuring improvements
**Migration Impact:** None (no API changes)
---
### Phase 1: Type Safety - Enums (Breaking 🔴)
**Goal:** Replace stringly-typed APIs with proper enums
**Duration Estimate:** 3-4 hours
**Tasks:**
1. **Create LogLevel Enum**
**New File:** Consider `src/level.rs` (replace existing)
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
Fatal,
}
impl Default for LogLevel {
fn default() -> Self {
Self::Error
}
}
impl Display for LogLevel { ... }
impl FromStr for LogLevel { ... }
impl From<LogLevel> for LevelFilter { ... }
```
**Benefits:**
- Compile-time verification
- No more string allocations
- Exhaustive matching
- Serde support for config files
2. **Create Output Enum**
**New File:** `src/output.rs` (replace `out.rs`)
```rust
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Output {
Stdout,
Stderr,
File(PathBuf),
}
impl Default for Output {
fn default() -> Self {
Self::Stdout
}
}
impl From<Output> for Stream { ... }
```
**Benefits:**
- Type-safe output selection
- No more string matching
- PathBuf for file paths (proper type)
3. **Update Opts Struct**
**File:** `src/opts.rs`
```rust
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Opts {
pub coloured: bool,
pub output: Output, pub level: LogLevel, pub report_caller: bool,
pub time_format: Option<String>, }
```
4. **Update Logger Implementation**
**File:** `src/logger.rs`
- Remove all `unwrap()` calls
- Use new enum types
- Remove string conversions
**Key Changes:**
```rust
fn level_to_filter(&self) -> LevelFilter {
self.opts.level.into() }
fn stream(&self) -> Stream {
(&self.opts.output).into() }
```
5. **Update Tests**
- Update all tests to use new enum types
- Ensure 95%+ coverage maintained
6. **Update Examples**
- Convert all examples to use new APIs
- Ensure they compile and run
**Breaking Changes:**
- `level::debug()` → `LogLevel::Debug`
- `out::STDOUT` → `Output::Stdout`
- `Opts { level: Some("debug".into()), ... }` → `Opts { level: LogLevel::Debug, ... }`
**Migration Guide:**
```rust
// Before
use twyg::{level, Opts};
let opts = Opts {
level: level::debug(),
file: Some("stdout".to_string()),
..Default::default()
};
// After
use twyg::{LogLevel, Output, Opts};
let opts = Opts {
level: LogLevel::Debug,
output: Output::Stdout,
..Default::default()
};
```
**Deliverables:**
- `LogLevel` enum with full trait implementations
- `Output` enum with full trait implementations
- Updated `Opts` struct
- Updated `Logger` implementation
- All tests passing
- Migration guide in CHANGELOG.md
**Migration Impact:** Breaking - requires major version bump (0.5.0 → 0.6.0 or 1.0.0)
---
### Phase 2: API Improvements - Builder and Validation (Breaking 🔴)
**Goal:** Add builder pattern and validation
**Duration Estimate:** 2-3 hours
**Tasks:**
1. **Make Opts Fields Private**
**File:** `src/opts.rs`
```rust
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Opts {
coloured: bool, output: Output, level: LogLevel, report_caller: bool, time_format: Option<String>, }
impl Opts {
pub fn coloured(&self) -> bool { self.coloured }
pub fn output(&self) -> &Output { &self.output }
pub fn level(&self) -> LogLevel { self.level }
pub fn report_caller(&self) -> bool { self.report_caller }
pub fn time_format(&self) -> Option<&str> {
self.time_format.as_deref()
}
}
```
2. **Add Builder Pattern**
**File:** `src/opts.rs`
```rust
pub struct OptsBuilder {
coloured: bool,
output: Output,
level: LogLevel,
report_caller: bool,
time_format: Option<String>,
}
impl OptsBuilder {
pub fn new() -> Self {
Self {
coloured: false,
output: Output::default(),
level: LogLevel::default(),
report_caller: false,
time_format: None,
}
}
pub fn coloured(mut self, coloured: bool) -> Self {
self.coloured = coloured;
self
}
pub fn output(mut self, output: Output) -> Self {
self.output = output;
self
}
pub fn level(mut self, level: LogLevel) -> Self {
self.level = level;
self
}
pub fn report_caller(mut self, report: bool) -> Self {
self.report_caller = report;
self
}
pub fn time_format(mut self, format: impl Into<String>) -> Self {
self.time_format = Some(format.into());
self
}
pub fn build(self) -> Result<Opts, ConfigError> {
if let Some(ref fmt) = self.time_format {
validate_time_format(fmt)?;
}
Ok(Opts {
coloured: self.coloured,
output: self.output,
level: self.level,
report_caller: self.report_caller,
time_format: self.time_format,
})
}
}
```
3. **Add Time Format Validation**
**File:** `src/opts.rs`
```rust
fn validate_time_format(format: &str) -> Result<(), ConfigError> {
Local::now().format(format).to_string();
Ok(())
}
```
4. **Create Custom Error Type**
**New File:** `src/error.rs`
```rust
use thiserror::Error;
#[derive(Debug, Error)]
pub enum TwygError {
#[error("invalid time format: {format}")]
InvalidTimeFormat { format: String },
#[error("failed to initialize logger: {0}")]
InitError(#[from] log::SetLoggerError),
#[error("failed to open log file: {0}")]
FileError(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, TwygError>;
```
5. **Update Public API**
**File:** `src/lib.rs`
```rust
pub use error::{TwygError, Result};
pub use opts::{Opts, OptsBuilder};
pub fn setup(opts: Opts) -> Result<Logger> {
}
```
6. **Update Tests and Examples**
- Test builder pattern
- Test validation
- Update all examples to use builder
**Breaking Changes:**
- `Opts` fields are now private
- `setup()` returns custom error type
- Direct struct initialization no longer works
**Migration Guide:**
```rust
// Before (Phase 1)
let opts = Opts {
level: LogLevel::Debug,
output: Output::Stdout,
coloured: true,
report_caller: true,
time_format: None,
};
// After (Phase 2)
use twyg::OptsBuilder;
let opts = OptsBuilder::new()
.level(LogLevel::Debug)
.output(Output::Stdout)
.coloured(true)
.report_caller(true)
.build()?;
```
**Deliverables:**
- Private Opts fields with getters
- Full builder pattern implementation
- Custom error type
- Time format validation
- Updated tests (95%+ coverage)
- Migration guide
**Migration Impact:** Breaking - same major version as Phase 1 (bundle together)
---
### Phase 3: Performance and Quality (Non-Breaking 🟢)
**Goal:** Optimize performance and code quality
**Duration Estimate:** 2-3 hours
**Tasks:**
1. **Remove Unnecessary Cloning**
**File:** `src/logger.rs`
```rust
dispatch = match self.opts.file.clone() { ...
};
dispatch = match &self.opts.output { Output::Stdout => dispatch.chain(std::io::stdout()),
Output::Stderr => dispatch.chain(std::io::stderr()),
Output::File(path) => dispatch.chain(fern::log_file(path)?),
};
```
2. **Optimize String Handling**
**File:** `src/logger.rs`
```rust
fn format_opt_str(x: Option<&str>) -> impl Display {
x.unwrap_or("??") }
```
3. **Reduce Code Duplication**
**File:** `src/logger.rs`
```rust
fn create_logger(
date: String,
filter: LevelFilter,
stream: Stream,
with_caller: bool,
) -> fern::Dispatch {
fern::Dispatch::new()
.format(move |out, message, record| {
if with_caller {
} else {
}
})
.level(filter)
}
```
4. **Improve Function Names**
**File:** `src/logger.rs`
```rust
fn get_opt_str(x: Option<&str>) -> String { ... }
fn get_opt_u32(x: Option<u32>) -> String { ... }
fn opt_str_or_placeholder(x: Option<&str>) -> &str { ... }
fn opt_u32_or_placeholder(x: Option<u32>) -> impl Display { ... }
```
5. **Add Module Documentation**
**All Files:** Add module-level `//!` comments
```rust
```
6. **Improve Error Messages**
**File:** `src/error.rs`
- Add more context to error messages
- Include suggestions for common mistakes
**Deliverables:**
- Removed all unnecessary clones
- Optimized string handling
- Reduced code duplication
- Better function names
- Module documentation
- Improved error messages
- Performance benchmarks showing improvements
**Migration Impact:** None (internal improvements only)
---
### Phase 4: Documentation and Examples (Non-Breaking 🟢)
**Goal:** Complete documentation and comprehensive examples
**Duration Estimate:** 2 hours
**Tasks:**
1. **Add Comprehensive API Documentation**
**All Public Items:**
- Document all error conditions (`# Errors`)
- Document all panic conditions (`# Panics`)
- Add usage examples
- Cross-reference related items
**Example:**
```rust
pub fn setup(opts: Opts) -> Result<Logger> { ... }
```
2. **Create Comprehensive Examples**
**Examples to Add:**
- `examples/quick-start.rs` - Minimal setup
- `examples/builder-pattern.rs` - Using builder
- `examples/error-handling.rs` - Proper error handling
- `examples/custom-format.rs` - Custom time formats
- `examples/file-output.rs` - Logging to file
- `examples/serde-config.rs` - Loading from config file
3. **Update README**
**File:** `README.md`
- Show new API (builder pattern)
- Migration guide from 0.5.x
- Feature comparison table
- Performance characteristics
4. **Create Migration Guide**
**New File:** `MIGRATION.md`
- Detailed guide for each breaking change
- Before/after code examples
- Deprecation timeline
- Common issues and solutions
5. **Add CHANGELOG.md**
**New File:** `CHANGELOG.md`
- Document all changes
- Breaking changes clearly marked
- Performance improvements noted
**Deliverables:**
- Complete API documentation
- 6+ comprehensive examples
- Updated README
- Migration guide
- CHANGELOG.md
- All rustdoc examples pass `cargo test --doc`
**Migration Impact:** None (documentation only)
---
### Phase 5: Advanced Features (Optional, Non-Breaking 🟢)
**Goal:** Add advanced features that enhance usability
**Duration Estimate:** 3-4 hours
**Tasks:**
1. **Add Preset Configurations**
**File:** `src/presets.rs`
```rust
impl OptsBuilder {
pub fn dev() -> Self {
Self::new()
.coloured(true)
.level(LogLevel::Debug)
.report_caller(true)
}
pub fn production() -> Self {
Self::new()
.coloured(false)
.level(LogLevel::Info)
.report_caller(false)
}
}
```
2. **Add Environment Variable Support**
**File:** `src/env.rs`
```rust
impl OptsBuilder {
pub fn from_env(mut self) -> Self {
if let Ok(level) = env::var("TWYG_LEVEL") {
if let Ok(l) = level.parse() {
self = self.level(l);
}
}
self
}
}
```
3. **Add Structured Logging Support**
**File:** `src/structured.rs`
```rust
pub enum Format {
Human, Json, Logfmt, }
impl OptsBuilder {
pub fn format(mut self, format: Format) -> Self {
self.format = Some(format);
self
}
}
```
4. **Add Dynamic Level Changing**
**File:** `src/logger.rs`
```rust
impl Logger {
pub fn set_level(&self, level: LogLevel) -> Result<()> {
}
}
```
5. **Add Custom Formatters**
**File:** `src/format.rs`
```rust
pub trait Formatter: Send + Sync {
fn format(
&self,
record: &log::Record,
timestamp: &str,
) -> String;
}
impl OptsBuilder {
pub fn custom_formatter(
mut self,
formatter: Box<dyn Formatter>,
) -> Self {
self.formatter = Some(formatter);
self
}
}
```
**Deliverables:**
- Preset configurations (dev/production)
- Environment variable support
- Structured logging options
- Dynamic level changing
- Custom formatter support
- Documentation for all features
- Examples for each feature
**Migration Impact:** None (new features are additive)
---
## Implementation Order Recommendation
### Quick Wins (Do First)
1. Phase 0 (Preparation) - Essential for safe refactoring
2. Fix bug in `logger.rs:59-65` - Critical bug
3. Add tests - Non-negotiable
### Breaking Changes (Bundle Together)
1. Phase 1 (Type Safety) - Most impactful improvement
2. Phase 2 (Builder Pattern) - Natural follow-on
3. Release as v1.0.0 with complete migration guide
### Polish (After Breaking Changes)
1. Phase 3 (Performance) - Internal improvements
2. Phase 4 (Documentation) - User-facing polish
3. Release as v1.1.0
### Optional Enhancements
1. Phase 5 (Advanced Features) - If desired
2. Release as v1.2.0
---
## Risk Assessment
### High Risk
- **Breaking changes** (Phases 1-2)
- Mitigation: Clear migration guide, deprecation warnings, examples
- Consider: Provide v0.5.x with deprecation warnings first
### Medium Risk
- **Test coverage gaps**
- Mitigation: Achieve 95%+ coverage before refactoring
- **Performance regressions**
- Mitigation: Benchmark before and after each phase
### Low Risk
- **Documentation improvements** (Phase 4)
- **New features** (Phase 5)
---
## Success Metrics
### Code Quality
- [ ] 95%+ test coverage
- [ ] Zero clippy warnings
- [ ] All tests pass
- [ ] cargo fmt produces no changes
### API Quality
- [ ] No unwrap() in library code
- [ ] No stringly-typed APIs
- [ ] All public types have Debug
- [ ] Custom error types with thiserror
- [ ] Builder pattern for complex config
### Documentation
- [ ] All public items documented
- [ ] # Errors sections for fallible functions
- [ ] 6+ comprehensive examples
- [ ] Migration guide complete
- [ ] CHANGELOG.md up to date
### Performance
- [ ] No unnecessary clones
- [ ] No unnecessary allocations
- [ ] Benchmarks show no regressions
---
## Timeline Estimate
- **Phase 0:** 1-2 hours
- **Phase 1:** 3-4 hours
- **Phase 2:** 2-3 hours
- **Phase 3:** 2-3 hours
- **Phase 4:** 2 hours
- **Phase 5 (Optional):** 3-4 hours
**Total (Required):** 10-14 hours
**Total (With Optional):** 13-18 hours
---
## Conclusion
The twyg crate is a well-scoped library with clear functionality. The identified issues, while numerous, are mostly straightforward to fix. The refactoring will transform the codebase from a functional but anti-pattern-heavy implementation to an idiomatic, type-safe, well-tested Rust library that serves as a good example of Rust best practices.
**Key Improvements:**
- **Type Safety:** Replace all stringly-typed APIs with enums (+safety, -bugs)
- **Error Handling:** Remove all unwrap(), add custom errors (+robustness)
- **API Design:** Builder pattern, private fields with validation (+usability)
- **Performance:** Remove unnecessary clones and allocations (+speed)
- **Quality:** 95%+ test coverage, comprehensive documentation (+confidence)
**Recommended Approach:**
Bundle Phases 1-2 into a single v1.0.0 release with a clear migration guide. Follow up with Phases 3-4 as v1.1.0 once the breaking changes have stabilized. Consider Phase 5 based on user feedback.