mirror of
https://codeberg.org/Toasterson/ips.git
synced 2026-04-10 21:30:41 +00:00
322 lines
9 KiB
Markdown
322 lines
9 KiB
Markdown
|
|
# Error Handling Guidelines for IPS Rust Code
|
||
|
|
|
||
|
|
This document outlines best practices for error handling in the IPS Rust codebase. It covers how to use the `miette` and `thiserror` crates for robust error handling and reporting, and the `tracing` crate for configurable debug output.
|
||
|
|
|
||
|
|
## Core Principles
|
||
|
|
|
||
|
|
The core idea is to combine:
|
||
|
|
- `thiserror` for creating custom error types with clear error messages
|
||
|
|
- `miette` for rich, user-friendly error reporting with diagnostic information
|
||
|
|
- `tracing` for structured, configurable debug output
|
||
|
|
|
||
|
|
## Project Setup
|
||
|
|
|
||
|
|
### Dependencies
|
||
|
|
|
||
|
|
Add the necessary dependencies to your crate's `Cargo.toml`:
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[dependencies]
|
||
|
|
miette = { version = "7.6.0", features = ["fancy"] }
|
||
|
|
thiserror = "1.0.50"
|
||
|
|
tracing = "0.1.37"
|
||
|
|
tracing-subscriber = "0.3.17"
|
||
|
|
```
|
||
|
|
|
||
|
|
**Note**: The "fancy" feature for miette enables colorful, detailed error reports. This feature should only be enabled in the top-level crate of your project (application crates) to avoid unnecessary dependencies in library crates.
|
||
|
|
|
||
|
|
For library crates like `libips`, use:
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[dependencies]
|
||
|
|
miette = "7.6.0"
|
||
|
|
thiserror = "1.0.50"
|
||
|
|
tracing = "0.1.37"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Defining Custom Error Types
|
||
|
|
|
||
|
|
Define your error types as enums using `thiserror`. This allows you to create specific error variants for different failure scenarios. Then, use miette's `Diagnostic` derive macro to add rich diagnostic information to your errors.
|
||
|
|
|
||
|
|
### Example
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use miette::{Diagnostic, NamedSource, SourceSpan};
|
||
|
|
use thiserror::Error;
|
||
|
|
|
||
|
|
#[derive(Error, Debug, Diagnostic)]
|
||
|
|
#[error("A validation error occurred")]
|
||
|
|
#[diagnostic(
|
||
|
|
code(ips::validation_error),
|
||
|
|
help("Please check the input data and try again.")
|
||
|
|
)]
|
||
|
|
pub enum ValidationError {
|
||
|
|
#[error("Invalid package name: {0}")]
|
||
|
|
#[diagnostic(
|
||
|
|
code(ips::validation_error::invalid_name),
|
||
|
|
help("Package names must follow the IPS naming conventions.")
|
||
|
|
)]
|
||
|
|
InvalidName(String),
|
||
|
|
|
||
|
|
#[error("Invalid version format")]
|
||
|
|
#[diagnostic(
|
||
|
|
code(ips::validation_error::invalid_version),
|
||
|
|
help("Version must be in the format: major.minor.patch")
|
||
|
|
)]
|
||
|
|
InvalidVersion {
|
||
|
|
#[source_code]
|
||
|
|
src: NamedSource,
|
||
|
|
|
||
|
|
#[label("the invalid version")]
|
||
|
|
span: SourceSpan,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
In this example:
|
||
|
|
|
||
|
|
- `#[derive(Error, Debug, Diagnostic)]` automatically implements the necessary traits from thiserror and miette.
|
||
|
|
- `#[error("...")]` provides the main error message.
|
||
|
|
- `#[diagnostic(...)]` adds diagnostic information like a unique error code and a help message.
|
||
|
|
- For more specific errors, like `InvalidVersion`, you can provide `source_code` and a `label` with a `SourceSpan` to highlight the exact location of the error.
|
||
|
|
|
||
|
|
### Error Codes
|
||
|
|
|
||
|
|
Use a consistent naming scheme for error codes:
|
||
|
|
|
||
|
|
- Top-level errors: `crate_name::error_category`
|
||
|
|
- Specific errors: `crate_name::error_category::specific_error`
|
||
|
|
|
||
|
|
For example:
|
||
|
|
- `ips::validation_error`
|
||
|
|
- `ips::validation_error::invalid_name`
|
||
|
|
|
||
|
|
## Returning Errors
|
||
|
|
|
||
|
|
### In Library Code
|
||
|
|
|
||
|
|
In library code (like `libips`), always return your specific error types. This makes your library's API clear and easy to use.
|
||
|
|
|
||
|
|
```rust
|
||
|
|
pub fn validate_package(name: &str, version: &str) -> Result<(), ValidationError> {
|
||
|
|
if !is_valid_package_name(name) {
|
||
|
|
return Err(ValidationError::InvalidName(name.to_string()));
|
||
|
|
}
|
||
|
|
|
||
|
|
if !is_valid_version(version) {
|
||
|
|
let source = NamedSource::new("input.txt", version.to_string());
|
||
|
|
let span = (0, version.len()).into();
|
||
|
|
return Err(ValidationError::InvalidVersion { src: source, span });
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### In Application Code
|
||
|
|
|
||
|
|
In your application's main function and other top-level code, you can use `miette::Result` for convenience.
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use miette::{IntoDiagnostic, Result};
|
||
|
|
|
||
|
|
fn main() -> Result<()> {
|
||
|
|
// Initialize tracing
|
||
|
|
setup_tracing();
|
||
|
|
|
||
|
|
// Your application logic here
|
||
|
|
let config = std::fs::read_to_string("config.json").into_diagnostic()?;
|
||
|
|
|
||
|
|
// Process the config
|
||
|
|
process_config(&config).map_err(|e| e.into())?;
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Converting Between Error Types
|
||
|
|
|
||
|
|
To convert from one error type to another, you can use the `From` trait or the `map_err` method on `Result`.
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// Using From trait
|
||
|
|
impl From<std::io::Error> for MyError {
|
||
|
|
fn from(err: std::io::Error) -> Self {
|
||
|
|
MyError::IoError(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Using map_err
|
||
|
|
fn read_config() -> Result<Config, MyError> {
|
||
|
|
std::fs::read_to_string("config.json")
|
||
|
|
.map_err(|e| MyError::IoError(e))?
|
||
|
|
.parse()
|
||
|
|
.map_err(|e| MyError::ParseError(e))
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Using Miette's Diagnostic Features
|
||
|
|
|
||
|
|
Miette provides several features to enhance error reporting:
|
||
|
|
|
||
|
|
### Source Code Highlighting
|
||
|
|
|
||
|
|
You can include the source code that caused the error and highlight the specific part that's problematic:
|
||
|
|
|
||
|
|
```rust
|
||
|
|
#[error("Invalid syntax in configuration file")]
|
||
|
|
struct InvalidSyntax {
|
||
|
|
#[source_code]
|
||
|
|
src: NamedSource,
|
||
|
|
|
||
|
|
#[label("this syntax is invalid")]
|
||
|
|
span: SourceSpan,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
To use this in your code:
|
||
|
|
|
||
|
|
```rust
|
||
|
|
let source = NamedSource::new("config.json", config_content.to_string());
|
||
|
|
let span = (error_start, error_length).into();
|
||
|
|
return Err(ConfigError::InvalidSyntax { src: source, span });
|
||
|
|
```
|
||
|
|
|
||
|
|
### Related Information
|
||
|
|
|
||
|
|
You can add related information to provide context for the error:
|
||
|
|
|
||
|
|
```rust
|
||
|
|
#[error("Failed to process package")]
|
||
|
|
#[diagnostic(
|
||
|
|
code(ips::package_error),
|
||
|
|
help("Check the package manifest for errors.")
|
||
|
|
)]
|
||
|
|
ProcessError {
|
||
|
|
#[related]
|
||
|
|
related: Vec<ValidationError>,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Custom Help Messages
|
||
|
|
|
||
|
|
Provide helpful messages to guide users on how to fix the error:
|
||
|
|
|
||
|
|
```rust
|
||
|
|
#[error("Invalid configuration")]
|
||
|
|
#[diagnostic(
|
||
|
|
code(ips::config_error),
|
||
|
|
help("The configuration file must be valid JSON. Check the syntax and try again.")
|
||
|
|
)]
|
||
|
|
InvalidConfig,
|
||
|
|
```
|
||
|
|
|
||
|
|
## Configurable Debug Output with Tracing
|
||
|
|
|
||
|
|
The `tracing` crate provides a powerful framework for structured, event-based logging. You can use it to add debug output to your library that can be enabled or disabled at runtime.
|
||
|
|
|
||
|
|
### Setting Up Tracing
|
||
|
|
|
||
|
|
In your application's main function, initialize the tracing-subscriber:
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use tracing_subscriber::{EnvFilter, FmtSubscriber};
|
||
|
|
|
||
|
|
fn setup_tracing() {
|
||
|
|
let subscriber = FmtSubscriber::builder()
|
||
|
|
.with_env_filter(EnvFilter::from_default_env())
|
||
|
|
.finish();
|
||
|
|
|
||
|
|
tracing::subscriber::set_global_default(subscriber)
|
||
|
|
.expect("setting default subscriber failed");
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Instrumenting Your Code
|
||
|
|
|
||
|
|
Use the tracing macros (`trace!`, `debug!`, `info!`, `warn!`, `error!`) to add log statements to your code. You can also use the `#[tracing::instrument]` attribute to automatically log the entry and exit of a function, along with its arguments.
|
||
|
|
|
||
|
|
```rust
|
||
|
|
#[tracing::instrument]
|
||
|
|
pub fn process_package(package: &Package) -> Result<(), ProcessError> {
|
||
|
|
tracing::debug!("Processing package: {}", package.name);
|
||
|
|
|
||
|
|
// Your logic here
|
||
|
|
|
||
|
|
tracing::info!("Package processed successfully");
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Controlling Log Levels
|
||
|
|
|
||
|
|
You can control the log level using the `RUST_LOG` environment variable. For example, to enable debug output, you would run your application like this:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
RUST_LOG=debug cargo run
|
||
|
|
```
|
||
|
|
|
||
|
|
Common log levels, from most to least verbose:
|
||
|
|
- `trace`: Very detailed information, typically only useful for debugging specific issues
|
||
|
|
- `debug`: Useful information for debugging
|
||
|
|
- `info`: General information about the application's operation
|
||
|
|
- `warn`: Potentially problematic situations that don't prevent the application from working
|
||
|
|
- `error`: Error conditions that prevent some functionality from working
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
1. **Be Specific**: Create specific error variants for different failure scenarios.
|
||
|
|
2. **Be Helpful**: Include helpful error messages and diagnostic information.
|
||
|
|
3. **Be Consistent**: Use a consistent naming scheme for error codes.
|
||
|
|
4. **Be Transparent**: Use `#[error(transparent)]` for wrapping errors from dependencies.
|
||
|
|
5. **Be Traceable**: Use tracing to log important events and debug information.
|
||
|
|
|
||
|
|
## Example: Converting Existing Code
|
||
|
|
|
||
|
|
Here's an example of how to convert an existing error type to use miette:
|
||
|
|
|
||
|
|
### Before
|
||
|
|
|
||
|
|
```rust
|
||
|
|
#[derive(Debug, Error)]
|
||
|
|
pub enum FmriError {
|
||
|
|
#[error("invalid FMRI format")]
|
||
|
|
InvalidFormat,
|
||
|
|
#[error("invalid version format")]
|
||
|
|
InvalidVersionFormat,
|
||
|
|
#[error("invalid release format")]
|
||
|
|
InvalidReleaseFormat,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### After
|
||
|
|
|
||
|
|
```rust
|
||
|
|
#[derive(Debug, Error, Diagnostic)]
|
||
|
|
#[diagnostic(code(ips::fmri_error))]
|
||
|
|
pub enum FmriError {
|
||
|
|
#[error("invalid FMRI format")]
|
||
|
|
#[diagnostic(
|
||
|
|
help("FMRI must be in the format: pkg://publisher/package@version")
|
||
|
|
)]
|
||
|
|
InvalidFormat,
|
||
|
|
|
||
|
|
#[error("invalid version format")]
|
||
|
|
#[diagnostic(
|
||
|
|
help("Version must be in the format: major.minor.patch")
|
||
|
|
)]
|
||
|
|
InvalidVersionFormat,
|
||
|
|
|
||
|
|
#[error("invalid release format")]
|
||
|
|
#[diagnostic(
|
||
|
|
help("Release must be a dot-separated list of numbers")
|
||
|
|
)]
|
||
|
|
InvalidReleaseFormat,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Further Reading
|
||
|
|
|
||
|
|
- [miette documentation](https://docs.rs/miette/7.6.0/miette/)
|
||
|
|
- [thiserror documentation](https://docs.rs/thiserror/1.0.50/thiserror/)
|
||
|
|
- [tracing documentation](https://docs.rs/tracing/0.1.37/tracing/)
|