Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:25:47 +08:00
commit 3694299e2d
9 changed files with 2710 additions and 0 deletions

View 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?

View 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?