Initial commit
This commit is contained in:
338
commands/rust-error-add-type.md
Normal file
338
commands/rust-error-add-type.md
Normal file
@@ -0,0 +1,338 @@
|
||||
---
|
||||
description: Create a new custom error type with thiserror
|
||||
---
|
||||
|
||||
You are helping create a custom error type in Rust using the `thiserror` crate for robust error handling.
|
||||
|
||||
## Your Task
|
||||
|
||||
Generate a well-structured custom error type with appropriate variants and conversions.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Ask for Error Type Details**
|
||||
|
||||
Ask the user (if not provided):
|
||||
- Error type name (e.g., "ConfigError", "DatabaseError", "ValidationError")
|
||||
- What domain/module is this for?
|
||||
- What error variants are needed? (suggest common ones based on context)
|
||||
- Should it wrap any external error types? (std::io::Error, sqlx::Error, etc.)
|
||||
|
||||
2. **Determine Common Patterns**
|
||||
|
||||
Based on the error name, suggest appropriate variants:
|
||||
|
||||
**For *ValidationError**:
|
||||
- Required(String) - missing required field
|
||||
- Invalid { field, value } - invalid field value
|
||||
- OutOfRange { min, max } - value out of range
|
||||
|
||||
**For *DatabaseError / *RepositoryError**:
|
||||
- NotFound(String)
|
||||
- Connection (#[from] lib::Error)
|
||||
- Query(String)
|
||||
- Transaction
|
||||
|
||||
**For *NetworkError / *ApiError**:
|
||||
- Connection (#[from] reqwest::Error)
|
||||
- Timeout
|
||||
- InvalidResponse(String)
|
||||
- Unauthorized
|
||||
|
||||
**For *ConfigError**:
|
||||
- MissingField(String)
|
||||
- InvalidValue { field, value }
|
||||
- ParseError (#[from] toml::Error or serde_json::Error)
|
||||
|
||||
3. **Create Error Type File**
|
||||
|
||||
Generate the error type with thiserror:
|
||||
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
/// Error type for [domain/module]
|
||||
///
|
||||
/// This error type represents all possible errors that can occur
|
||||
/// when [description of what can go wrong].
|
||||
#[derive(Error, Debug)]
|
||||
pub enum [ErrorName] {
|
||||
/// [Description of when this error occurs]
|
||||
#[error("[User-friendly error message]")]
|
||||
[Variant1](String),
|
||||
|
||||
/// [Description]
|
||||
#[error("[Message with field]: {field}")]
|
||||
[Variant2] {
|
||||
field: String,
|
||||
},
|
||||
|
||||
/// Wraps [external error type]
|
||||
#[error("[Context message]")]
|
||||
[Variant3](#[from] [ExternalError]),
|
||||
|
||||
/// [Description]
|
||||
#[error("[Message with source error]")]
|
||||
[Variant4](#[source] [ExternalError]),
|
||||
}
|
||||
```
|
||||
|
||||
4. **Add Result Type Alias**
|
||||
|
||||
Create a Result alias for convenience:
|
||||
|
||||
```rust
|
||||
/// Result type alias for [module] operations
|
||||
pub type Result<T> = std::result::Result<T, [ErrorName]>;
|
||||
```
|
||||
|
||||
5. **Create Comprehensive Example**
|
||||
|
||||
Provide a complete, real-world example:
|
||||
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that can occur during user operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum UserError {
|
||||
/// User not found with the given identifier
|
||||
#[error("User not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
/// Invalid email format
|
||||
#[error("Invalid email: {0}")]
|
||||
InvalidEmail(String),
|
||||
|
||||
/// Database operation failed
|
||||
#[error("Database error")]
|
||||
Database(#[from] sqlx::Error),
|
||||
|
||||
/// User already exists
|
||||
#[error("User already exists: {email}")]
|
||||
AlreadyExists { email: String },
|
||||
|
||||
/// Validation failed
|
||||
#[error("Validation failed: {0}")]
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
/// Result type for user operations
|
||||
pub type Result<T> = std::result::Result<T, UserError>;
|
||||
|
||||
// Example usage
|
||||
pub async fn find_user(id: &str) -> Result<User> {
|
||||
let user = query_user(id).await?; // Auto-converts sqlx::Error
|
||||
validate_user(&user)?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
fn validate_user(user: &User) -> Result<()> {
|
||||
if user.email.is_empty() {
|
||||
return Err(UserError::InvalidEmail("Email cannot be empty".to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
6. **Add to Appropriate Module**
|
||||
|
||||
Determine where to place the error type:
|
||||
- If it's a domain error: `src/domain/errors.rs` or `src/domain/[module]/error.rs`
|
||||
- If it's an infrastructure error: `src/infrastructure/errors.rs`
|
||||
- If it's a service error: `src/services/[service]/error.rs`
|
||||
|
||||
Update the module's `mod.rs`:
|
||||
```rust
|
||||
mod error;
|
||||
pub use error::{[ErrorName], Result};
|
||||
```
|
||||
|
||||
7. **Update Cargo.toml**
|
||||
|
||||
Ensure thiserror is added:
|
||||
```toml
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
```
|
||||
|
||||
8. **Add Tests**
|
||||
|
||||
Create tests for error handling:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_error_display() {
|
||||
let error = UserError::NotFound("user123".to_string());
|
||||
assert_eq!(error.to_string(), "User not found: user123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_conversion() {
|
||||
fn returns_io_error() -> std::io::Result<()> {
|
||||
Err(std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"))
|
||||
}
|
||||
|
||||
fn wraps_error() -> Result<()> {
|
||||
returns_io_error().map_err(|e| UserError::Validation(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
assert!(wraps_error().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_matching() {
|
||||
let error = UserError::InvalidEmail("test".to_string());
|
||||
|
||||
match error {
|
||||
UserError::InvalidEmail(email) => assert_eq!(email, "test"),
|
||||
_ => panic!("Wrong error variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
9. **Provide Usage Guidance**
|
||||
|
||||
Show how to use the error type:
|
||||
|
||||
```
|
||||
✅ Error type '[ErrorName]' created successfully!
|
||||
|
||||
## Usage Examples:
|
||||
|
||||
### Returning Errors
|
||||
```rust
|
||||
fn do_work() -> Result<Output> {
|
||||
if condition {
|
||||
return Err([ErrorName]::SomeVariant("details".to_string()));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
```
|
||||
|
||||
### Error Propagation
|
||||
```rust
|
||||
fn process() -> Result<Data> {
|
||||
let result = risky_operation()?; // Auto-converts with #[from]
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
### Error Matching
|
||||
```rust
|
||||
match operation() {
|
||||
Ok(value) => println!("Success: {:?}", value),
|
||||
Err([ErrorName]::NotFound(id)) => eprintln!("Not found: {}", id),
|
||||
Err([ErrorName]::Validation(msg)) => eprintln!("Validation: {}", msg),
|
||||
Err(e) => eprintln!("Other error: {}", e),
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps:
|
||||
|
||||
1. Review and adjust error variants as needed
|
||||
2. Use this error type in your functions
|
||||
3. Add more variants as you discover new error cases
|
||||
4. Consider creating error conversion helpers if needed
|
||||
|
||||
## Testing:
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
```
|
||||
|
||||
## Error Message Guidelines
|
||||
|
||||
When creating error messages:
|
||||
|
||||
1. **Be Specific**: Include relevant context
|
||||
```rust
|
||||
#[error("Failed to load config from {path}")]
|
||||
ConfigLoad { path: String }
|
||||
```
|
||||
|
||||
2. **Be User-Friendly**: Write for humans
|
||||
```rust
|
||||
#[error("The email address '{0}' is not valid")]
|
||||
InvalidEmail(String)
|
||||
```
|
||||
|
||||
3. **Include Details**: Help with debugging
|
||||
```rust
|
||||
#[error("Database query failed: {query}")]
|
||||
QueryFailed { query: String }
|
||||
```
|
||||
|
||||
4. **Use Action Words**: Describe what went wrong
|
||||
```rust
|
||||
#[error("Failed to connect to database at {url}")]
|
||||
ConnectionFailed { url: String }
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Simple Error
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("Something went wrong: {0}")]
|
||||
Generic(String),
|
||||
}
|
||||
```
|
||||
|
||||
### Error with Fields
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("Invalid value for {field}: expected {expected}, got {actual}")]
|
||||
InvalidValue {
|
||||
field: String,
|
||||
expected: String,
|
||||
actual: String,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Wrapped External Error
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("IO operation failed")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Serialization failed")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
}
|
||||
```
|
||||
|
||||
### Error with Source
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("Operation failed")]
|
||||
Failed(#[source] Box<dyn std::error::Error>),
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Always derive `Error` and `Debug`
|
||||
- Use `#[from]` for automatic From impl
|
||||
- Use `#[source]` to preserve error chain
|
||||
- Keep error messages concise but informative
|
||||
- Consider adding Result type alias
|
||||
- Add documentation comments
|
||||
- Include tests for error cases
|
||||
|
||||
## After Completion
|
||||
|
||||
Ask the user:
|
||||
1. Do you want to add more error variants?
|
||||
2. Should we create conversion helpers?
|
||||
3. Do you want to integrate this with existing error types?
|
||||
387
commands/rust-error-refactor.md
Normal file
387
commands/rust-error-refactor.md
Normal file
@@ -0,0 +1,387 @@
|
||||
---
|
||||
description: Refactor code from panic-based to Result-based error handling
|
||||
---
|
||||
|
||||
You are helping refactor Rust code to use proper error handling with Result types instead of panic-based error handling.
|
||||
|
||||
## Your Task
|
||||
|
||||
Analyze code for panic-prone patterns and refactor to use Result-based error handling.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Scan for Panic-Prone Code**
|
||||
|
||||
Search the codebase for:
|
||||
- `.unwrap()` calls
|
||||
- `.expect()` calls
|
||||
- `panic!()` macros
|
||||
- `.unwrap_or_default()` where errors should be handled
|
||||
- Indexing that could panic (e.g., `vec[0]`)
|
||||
|
||||
Use grep to find these patterns:
|
||||
```
|
||||
- unwrap()
|
||||
- expect(
|
||||
- panic!(
|
||||
```
|
||||
|
||||
2. **Categorize Findings**
|
||||
|
||||
Group findings by severity:
|
||||
- **Critical**: Production code with unwrap/panic
|
||||
- **Warning**: expect() with poor messages
|
||||
- **Info**: Test code (acceptable to use unwrap)
|
||||
|
||||
Report to user:
|
||||
```
|
||||
Found panic-prone patterns:
|
||||
|
||||
Critical (5):
|
||||
- src/api/handler.rs:42 - .unwrap() on database query
|
||||
- src/config.rs:15 - .expect("Failed") on file read
|
||||
...
|
||||
|
||||
Info (2):
|
||||
- tests/integration.rs:10 - .unwrap() (OK in tests)
|
||||
...
|
||||
```
|
||||
|
||||
3. **Ask User for Scope**
|
||||
|
||||
Ask which files or functions to refactor:
|
||||
- All critical issues?
|
||||
- Specific file or module?
|
||||
- Specific function?
|
||||
|
||||
4. **Refactor Each Pattern**
|
||||
|
||||
For each panic-prone pattern, apply appropriate refactoring:
|
||||
|
||||
**Pattern 1: unwrap() on Option**
|
||||
|
||||
Before:
|
||||
```rust
|
||||
fn get_user(id: &str) -> User {
|
||||
let user = users.get(id).unwrap();
|
||||
user
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```rust
|
||||
fn get_user(id: &str) -> Result<User, UserError> {
|
||||
users.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| UserError::NotFound(id.to_string()))
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 2: unwrap() on Result**
|
||||
|
||||
Before:
|
||||
```rust
|
||||
fn load_config() -> Config {
|
||||
let content = std::fs::read_to_string("config.toml").unwrap();
|
||||
toml::from_str(&content).unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```rust
|
||||
fn load_config() -> Result<Config, ConfigError> {
|
||||
let content = std::fs::read_to_string("config.toml")
|
||||
.map_err(|e| ConfigError::FileRead(e))?;
|
||||
|
||||
toml::from_str(&content)
|
||||
.map_err(|e| ConfigError::Parse(e))
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 3: expect() with bad message**
|
||||
|
||||
Before:
|
||||
```rust
|
||||
let value = dangerous_op().expect("Failed");
|
||||
```
|
||||
|
||||
After:
|
||||
```rust
|
||||
let value = dangerous_op()
|
||||
.map_err(|e| MyError::OperationFailed(format!("Dangerous operation failed: {}", e)))?;
|
||||
```
|
||||
|
||||
**Pattern 4: panic! for validation**
|
||||
|
||||
Before:
|
||||
```rust
|
||||
fn create_user(email: String) -> User {
|
||||
if email.is_empty() {
|
||||
panic!("Email cannot be empty");
|
||||
}
|
||||
User { email }
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```rust
|
||||
fn create_user(email: String) -> Result<User, ValidationError> {
|
||||
if email.is_empty() {
|
||||
return Err(ValidationError::Required("email".to_string()));
|
||||
}
|
||||
Ok(User { email })
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 5: Vec indexing**
|
||||
|
||||
Before:
|
||||
```rust
|
||||
fn get_first(items: &Vec<Item>) -> Item {
|
||||
items[0].clone()
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```rust
|
||||
fn get_first(items: &Vec<Item>) -> Result<Item, ItemError> {
|
||||
items.first()
|
||||
.cloned()
|
||||
.ok_or(ItemError::Empty)
|
||||
}
|
||||
```
|
||||
|
||||
5. **Update Function Signatures**
|
||||
|
||||
When refactoring a function to return Result:
|
||||
- Change return type from `T` to `Result<T, ErrorType>`
|
||||
- Add error type if it doesn't exist
|
||||
- Update all return statements
|
||||
|
||||
Before:
|
||||
```rust
|
||||
fn process_data(input: &str) -> Data {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```rust
|
||||
fn process_data(input: &str) -> Result<Data, ProcessError> {
|
||||
// ...
|
||||
Ok(data)
|
||||
}
|
||||
```
|
||||
|
||||
6. **Update Call Sites**
|
||||
|
||||
Find all places where the refactored function is called and update them:
|
||||
|
||||
**If caller already returns Result**:
|
||||
```rust
|
||||
fn caller() -> Result<Output, Error> {
|
||||
let data = process_data(input)?; // Use ? operator
|
||||
Ok(output)
|
||||
}
|
||||
```
|
||||
|
||||
**If caller doesn't handle errors yet**:
|
||||
```rust
|
||||
// Option 1: Propagate error
|
||||
fn caller() -> Result<Output, Error> {
|
||||
let data = process_data(input)?;
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
// Option 2: Handle locally
|
||||
fn caller() -> Output {
|
||||
match process_data(input) {
|
||||
Ok(data) => process(data),
|
||||
Err(e) => {
|
||||
log::error!("Failed to process: {}", e);
|
||||
default_output()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
7. **Add Error Types if Needed**
|
||||
|
||||
If refactoring requires new error types, create them:
|
||||
|
||||
```rust
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ProcessError {
|
||||
#[error("Invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("IO error")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Parse error")]
|
||||
Parse(#[from] serde_json::Error),
|
||||
}
|
||||
```
|
||||
|
||||
8. **Update Tests**
|
||||
|
||||
Refactor tests to handle Result types:
|
||||
|
||||
Before:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_process() {
|
||||
let result = process_data("input");
|
||||
assert_eq!(result.value, 42);
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_process_success() {
|
||||
let result = process_data("input").unwrap(); // OK in tests
|
||||
assert_eq!(result.value, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_error() {
|
||||
let result = process_data("invalid");
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(ProcessError::InvalidInput(_)) => (),
|
||||
_ => panic!("Expected InvalidInput error"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
9. **Run Tests and Fix**
|
||||
|
||||
After refactoring:
|
||||
```bash
|
||||
cargo test
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
Fix any compilation errors or test failures.
|
||||
|
||||
10. **Provide Refactoring Summary**
|
||||
|
||||
Show what was changed:
|
||||
```
|
||||
✅ Refactored error handling
|
||||
|
||||
## Changes Made:
|
||||
|
||||
### Files Modified:
|
||||
- src/api/handler.rs
|
||||
- src/config.rs
|
||||
- src/database/query.rs
|
||||
|
||||
### Functions Refactored:
|
||||
- `load_config` - Now returns Result<Config, ConfigError>
|
||||
- `get_user` - Now returns Result<User, UserError>
|
||||
- `execute_query` - Now returns Result<Data, DatabaseError>
|
||||
|
||||
### New Error Types Created:
|
||||
- ConfigError in src/config.rs
|
||||
- UserError in src/domain/user.rs
|
||||
|
||||
### Patterns Replaced:
|
||||
- 8 unwrap() calls → ? operator with proper error handling
|
||||
- 3 expect() calls → descriptive error variants
|
||||
- 2 panic!() calls → Result returns
|
||||
|
||||
## Next Steps:
|
||||
|
||||
1. Run tests: `cargo test`
|
||||
2. Review error messages for clarity
|
||||
3. Consider adding error context where needed
|
||||
4. Update API documentation
|
||||
|
||||
## Before/After Example:
|
||||
|
||||
Before:
|
||||
```rust
|
||||
fn load_config() -> Config {
|
||||
let content = std::fs::read_to_string("config.toml").unwrap();
|
||||
toml::from_str(&content).unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```rust
|
||||
fn load_config() -> Result<Config, ConfigError> {
|
||||
let content = std::fs::read_to_string("config.toml")?;
|
||||
let config = toml::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## Refactoring Guidelines
|
||||
|
||||
1. **Don't refactor test code**: `unwrap()` is acceptable in tests
|
||||
2. **Preserve behavior**: Make sure logic stays the same
|
||||
3. **Update incrementally**: Refactor one function at a time
|
||||
4. **Test after each change**: Ensure nothing breaks
|
||||
5. **Add error context**: Make errors informative
|
||||
6. **Consider backwards compatibility**: Use deprecation if needed
|
||||
|
||||
## When to Keep unwrap/expect
|
||||
|
||||
Keep these patterns when:
|
||||
- In test code (`#[cfg(test)]`)
|
||||
- After explicitly checking with `if let Some` or `is_some()`
|
||||
- When panic is truly the desired behavior (e.g., invalid constants)
|
||||
- In example code or documentation
|
||||
|
||||
## Common Refactoring Patterns
|
||||
|
||||
### Option::unwrap → ok_or
|
||||
```rust
|
||||
// Before
|
||||
let value = map.get(key).unwrap();
|
||||
|
||||
// After
|
||||
let value = map.get(key)
|
||||
.ok_or(Error::NotFound(key.to_string()))?;
|
||||
```
|
||||
|
||||
### Result::unwrap → ?
|
||||
```rust
|
||||
// Before
|
||||
let data = parse_data(&input).unwrap();
|
||||
|
||||
// After
|
||||
let data = parse_data(&input)?;
|
||||
```
|
||||
|
||||
### panic! → return Err
|
||||
```rust
|
||||
// Before
|
||||
if !is_valid(&input) {
|
||||
panic!("Invalid input");
|
||||
}
|
||||
|
||||
// After
|
||||
if !is_valid(&input) {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Create comprehensive error types before refactoring
|
||||
- Update documentation to reflect new error returns
|
||||
- Consider API compatibility for public functions
|
||||
- Add migration guide if it's a library
|
||||
- Use `#[deprecated]` for gradual migration
|
||||
|
||||
## After Completion
|
||||
|
||||
Ask the user:
|
||||
1. Did all tests pass?
|
||||
2. Are there more files to refactor?
|
||||
3. Should we add more error context?
|
||||
4. Do you want to update error documentation?
|
||||
Reference in New Issue
Block a user