derive-error-kind 0.1.0

Proc macro for deriving error kinds
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# derive-error-kind

[![Crates.io](https://img.shields.io/crates/v/derive-error-kind.svg)](https://crates.io/crates/derive-error-kind)
[![Documentation](https://docs.rs/derive-error-kind/badge.svg)](https://docs.rs/derive-error-kind)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A Rust procedural macro for implementing the ErrorKind pattern that simplifies error classification and handling in complex applications.

## Motivation

The ErrorKind pattern is a common technique in Rust for separating:
- The **kind** of an error (represented by a simple enum)
- The **details** of the error (contained in the error structure)

This allows developers to handle errors more granularly without losing context.

Rust's standard library uses this pattern in `std::io::ErrorKind`, and many other libraries have adopted it due to its flexibility. However, manually implementing this pattern can be repetitive and error-prone, especially in applications with multiple nested error types.

This crate solves this problem by providing a derive macro that automates the implementation of the ErrorKind pattern.

## Overview

The `ErrorKind` macro allows you to associate error types with a specific kind from an enum. This creates a clean and consistent way to categorize errors in your application, enabling more precise error handling.

Key features:
- Automatically implements a `.kind()` method that returns a categorized error type
- Supports nested error types via the `transparent` attribute
- Works with unit variants, named fields, and tuple variants
- Enables transparent error propagation through error hierarchies

## Installation

Add this to your `Cargo.toml`:

```toml
[dependencies]
derive-error-kind = "0.1.0"
```

## Basic Usage

First, define an enum for your error kinds:

```rust
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ErrorKind {
    NotFound,
    InvalidInput,
    InternalError,
}
```

Then, use the `ErrorKind` derive macro on your error enums:

```rust
use derive_error_kind::ErrorKind;

#[derive(Debug, ErrorKind)]
#[error_kind(ErrorKind)]
pub enum MyError {
    #[error_kind(ErrorKind, NotFound)]
    ResourceNotFound,

    #[error_kind(ErrorKind, InvalidInput)]
    BadRequest { details: String },

    #[error_kind(ErrorKind, InternalError)]
    ServerError(String),
}

// Now you can use the .kind() method
let error = MyError::ResourceNotFound;
assert_eq!(error.kind(), ErrorKind::NotFound);
```

## Advanced Examples

### Nested Error Types

You can create hierarchical error structures with the `transparent` attribute:

```rust
use derive_error_kind::ErrorKind;

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ErrorKind {
    Database,
    Cache,
    Network,
    Configuration,
}

#[derive(Debug, ErrorKind)]
#[error_kind(ErrorKind)]
pub enum DatabaseError {
    #[error_kind(ErrorKind, Database)]
    Connection,

    #[error_kind(ErrorKind, Database)]
    Query(String),
}

#[derive(Debug, ErrorKind)]
#[error_kind(ErrorKind)]
pub enum CacheError {
    #[error_kind(ErrorKind, Cache)]
    Expired,

    #[error_kind(ErrorKind, Cache)]
    Missing,
}

#[derive(Debug, ErrorKind)]
#[error_kind(ErrorKind)]
pub enum AppError {
    #[error_kind(transparent)]
    Db(DatabaseError),

    #[error_kind(transparent)]
    Cache(CacheError),

    #[error_kind(ErrorKind, Network)]
    Connection,

    #[error_kind(ErrorKind, Configuration)]
    InvalidConfig { field: String, message: String },
}

// The transparent attribute allows the kind to bubble up
let db_error = AppError::Db(DatabaseError::Connection);
assert_eq!(db_error.kind(), ErrorKind::Database);

let cache_error = AppError::Cache(CacheError::Missing);
assert_eq!(cache_error.kind(), ErrorKind::Cache);
```

### Integrating with `thiserror`

The `ErrorKind` derive macro works well with other popular error handling crates:

```rust
use derive_error_kind::ErrorKind;
use thiserror::Error;

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ErrorKind {
    NotFound,
    Unauthorized,
    Internal,
}

#[derive(Debug, Error, ErrorKind)]
#[error_kind(ErrorKind)]
pub enum ApiError {
    #[error("Resource not found: {0}")]
    #[error_kind(ErrorKind, NotFound)]
    NotFound(String),

    #[error("Unauthorized access")]
    #[error_kind(ErrorKind, Unauthorized)]
    Unauthorized,

    #[error("Internal server error: {0}")]
    #[error_kind(ErrorKind, Internal)]
    Internal(String),
}

// Use in error handling
fn process_api_result(result: Result<(), ApiError>) {
    if let Err(err) = result {
        match err.kind() {
            ErrorKind::NotFound => {
                // Handle not found errors
                println!("Resource not found: {}", err);
            },
            ErrorKind::Unauthorized => {
                // Handle authorization errors
                println!("Please log in first");
            },
            ErrorKind::Internal => {
                // Log internal errors
                eprintln!("Internal error: {}", err);
            },
        }
    }
}
```

### Web Application Example

Here's a more complete example for a web application with multiple error domains:

```rust
use derive_error_kind::ErrorKind;
use thiserror::Error;
use std::fmt;

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ApiErrorKind {
    Authentication,
    Authorization,
    NotFound,
    BadRequest,
    ServerError,
}

// Implement Display for HTTP status code mapping
impl fmt::Display for ApiErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Authentication => write!(f, "Authentication Failed"),
            Self::Authorization => write!(f, "Not Authorized"),
            Self::NotFound => write!(f, "Resource Not Found"),
            Self::BadRequest => write!(f, "Bad Request"),
            Self::ServerError => write!(f, "Internal Server Error"),
        }
    }
}

// Implement status code conversion
impl ApiErrorKind {
    pub fn status_code(&self) -> u16 {
        match self {
            Self::Authentication => 401,
            Self::Authorization => 403,
            Self::NotFound => 404,
            Self::BadRequest => 400,
            Self::ServerError => 500,
        }
    }
}

// Database errors
#[derive(Debug, Error, ErrorKind)]
#[error_kind(ApiErrorKind)]
pub enum DbError {
    #[error("Database connection failed: {0}")]
    #[error_kind(ApiErrorKind, ServerError)]
    Connection(String),

    #[error("Query execution failed: {0}")]
    #[error_kind(ApiErrorKind, ServerError)]
    Query(String),

    #[error("Entity not found: {0}")]
    #[error_kind(ApiErrorKind, NotFound)]
    NotFound(String),
}

// Auth errors
#[derive(Debug, Error, ErrorKind)]
#[error_kind(ApiErrorKind)]
pub enum AuthError {
    #[error("Invalid credentials")]
    #[error_kind(ApiErrorKind, Authentication)]
    InvalidCredentials,

    #[error("Token expired")]
    #[error_kind(ApiErrorKind, Authentication)]
    TokenExpired,

    #[error("Insufficient permissions for {0}")]
    #[error_kind(ApiErrorKind, Authorization)]
    InsufficientPermissions(String),
}

// Application errors that can wrap domain-specific errors
#[derive(Debug, Error, ErrorKind)]
#[error_kind(ApiErrorKind)]
pub enum AppError {
    #[error(transparent)]
    #[error_kind(transparent)]
    Database(#[from] DbError),

    #[error(transparent)]
    #[error_kind(transparent)]
    Auth(#[from] AuthError),

    #[error("Invalid input: {0}")]
    #[error_kind(ApiErrorKind, BadRequest)]
    InvalidInput(String),

    #[error("Unexpected error: {0}")]
    #[error_kind(ApiErrorKind, ServerError)]
    Unexpected(String),
}

// Example API response
#[derive(Debug, serde::Serialize)]
pub struct ApiResponse<T> {
    success: bool,
    data: Option<T>,
    error: Option<ErrorResponse>,
}

#[derive(Debug, serde::Serialize)]
pub struct ErrorResponse {
    code: u16,
    message: String,
}

// Use in a web handler (example with actix-web)
fn handle_error(err: AppError) -> HttpResponse {
    let status_code = err.kind().status_code();

    let response = ApiResponse {
        success: false,
        data: None,
        error: Some(ErrorResponse {
            code: status_code,
            message: err.to_string(),
        }),
    };

    HttpResponse::build(StatusCode::from_u16(status_code).unwrap())
        .json(response)
}

// Usage example
async fn get_user(user_id: String) -> Result<User, AppError> {
    let user = db::find_user(&user_id).await
        .map_err(AppError::Database)?;

    if !user.is_active {
        return Err(AppError::Auth(AuthError::InsufficientPermissions("inactive user".to_string())));
    }

    Ok(user)
}
```

## Benefits

- **Simplified Error Handling**: Map complex errors to a simple enum for clean error handling
- **Better Error Classification**: Categorize errors consistently across your application
- **Cleaner APIs**: Hide implementation details behind error kinds
- **Integration with Error Handling Libraries**: Works well with `thiserror`, `anyhow`, and other error handling crates

## Attribute Reference

- `#[error_kind(KindEnum)]`: Top-level attribute that specifies which enum to use for error kinds
- `#[error_kind(KindEnum, Variant)]`: Variant-level attribute that specifies which variant of the kind enum to return
- `#[error_kind(transparent)]`: Variant-level attribute for nested errors, indicating that the inner error's kind should be used

## Requirements

- The macro can only be applied to enums
- Each variant must have an `error_kind` attribute
- The kind enum must be in scope and accessible

### Microservices Example

Here's an example showing how `derive-error-kind` can be used in a microservices architecture:

```rust
use derive_error_kind::ErrorKind;
use thiserror::Error;
use std::fmt;

// Global error kinds that are consistent across all services
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum GlobalErrorKind {
    // Infrastructure errors
    DatabaseError,
    CacheError,
    NetworkError,

    // Business logic errors
    ValidationError,
    NotFoundError,
    ConflictError,

    // Security errors
    AuthenticationError,
    AuthorizationError,

    // General errors
    ConfigurationError,
    InternalError,
}

// User service errors
#[derive(Debug, Error, ErrorKind)]
#[error_kind(GlobalErrorKind)]
pub enum UserServiceError {
    #[error("Failed to connect to users database: {0}")]
    #[error_kind(GlobalErrorKind, DatabaseError)]
    Database(String),

    #[error("User not found: {0}")]
    #[error_kind(GlobalErrorKind, NotFoundError)]
    NotFound(String),

    #[error("Email already exists: {0}")]
    #[error_kind(GlobalErrorKind, ConflictError)]
    DuplicateEmail(String),
}

// Inventory service errors
#[derive(Debug, Error, ErrorKind)]
#[error_kind(GlobalErrorKind)]
pub enum InventoryServiceError {
    #[error("Failed to connect to inventory database: {0}")]
    #[error_kind(GlobalErrorKind, DatabaseError)]
    Database(String),

    #[error("Product not found: {0}")]
    #[error_kind(GlobalErrorKind, NotFoundError)]
    ProductNotFound(String),

    #[error("Insufficient stock for product: {0}")]
    #[error_kind(GlobalErrorKind, ConflictError)]
    InsufficientStock(String),
}

// Order service errors
#[derive(Debug, Error, ErrorKind)]
#[error_kind(GlobalErrorKind)]
pub enum OrderServiceError {
    #[error("Database error: {0}")]
    #[error_kind(GlobalErrorKind, DatabaseError)]
    Database(String),

    #[error(transparent)]
    #[error_kind(transparent)]
    User(#[from] UserServiceError),

    #[error(transparent)]
    #[error_kind(transparent)]
    Inventory(#[from] InventoryServiceError),

    #[error("Order validation failed: {0}")]
    #[error_kind(GlobalErrorKind, ValidationError)]
    Validation(String),
}

// API Gateway error handling
fn handle_service_error<E: std::error::Error + 'static>(err: E) -> HttpResponse {
    // Use downcast to check if we have an error with a kind() method
    if let Some(user_err) = err.downcast_ref::<UserServiceError>() {
        match user_err.kind() {
            GlobalErrorKind::NotFoundError => return HttpResponse::NotFound().finish(),
            GlobalErrorKind::ConflictError => return HttpResponse::Conflict().finish(),
            _ => { /* continue with other error types */ }
        }
    }

    if let Some(order_err) = err.downcast_ref::<OrderServiceError>() {
        // Here, transparent errors from other services are automatically
        // mapped to the correct GlobalErrorKind
        match order_err.kind() {
            GlobalErrorKind::ValidationError => return HttpResponse::BadRequest().finish(),
            GlobalErrorKind::NotFoundError => return HttpResponse::NotFound().finish(),
            GlobalErrorKind::DatabaseError => {
                log::error!("Database error: {}", order_err);
                return HttpResponse::InternalServerError().finish();
            },
            _ => { /* continue with general error handling */ }
        }
    }

    // Default error response
    HttpResponse::InternalServerError().finish()
}
```


## Best Practices

1. **Keep error categories (ErrorKind) simple and stable**
   - They should change less frequently than your detailed error types

2. **Use the same error category throughout your application**
   - Makes consistent error handling at the API layer easier

3. **Combine with `thiserror` for detailed error messages**
   - `derive-error-kind` handles categorization while `thiserror` handles messages

4. **Use `transparent` for nested errors**
   - Allows the correct error category to propagate automatically

## Acknowledgements

- This project was inspired by the [enum-kinds]https://crates.io/crates/enum-kinds crate

## License

Licensed under MIT license.