Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `WithContext<C, E, F>` — tag any error with a context value (path, attempt number, etc.). `Colon` is the default strategy; `PathColon` handles
`Path`/`PathBuf` directly without a newtype. `WithPath<C, E>` aliases the path case. The wrapper's `Error::source` skips its inner error so chain walkers don't print it twice.
- `Suggest` trait for per-variant "Did you mean…" hints, and the `Suggestion` `Format` strategy that renders them. `error.suggestion()` prints the top-level hint via `Display`. Default `Suggest::fmt` writes nothing, so types only implement it for variants that have a hint.
- `DisplayPath` wrapper in the `path_display` module — drops `&Path`/`PathBuf` into any `Display` context.
- `T` generic on `MainResult<E, F, T = ()>` so `main` can return `ExitCode` or any `Termination` type.
- `Add<L, R>` — type-level combinator that runs two `Format` strategies in sequence. Compose with the new `separator` module (`NewLine`, `Space`, `Empty`, `Colon`, `ColonSpace`) to build strategies like `Add<Add<OneLine, NewLine>, Suggestion>` without writing a new impl. Bounds compose automatically — using `Suggestion` in the chain still requires `E: Suggest`.
- `WithSep<L, Sep, R>` alias — sugar for `Add<Add<L, Sep>, R>` so the separator reads in the middle. Plus pre-baked variants in `separator`: `WithSpace<L, R>`, `WithNewLine<L, R>`, `WithColonSpace<L, R>`.
- `with_context::ContextField`, `ErrorField`, and `ContextPath` (std-only) — `Format` extractor strategies that read fields of `WithContext`. Compose with separators to build custom pair strategies, e.g. `WithSep<ContextField, Space, ErrorField>` for a space-delimited pair.

### Changed

- **Breaking:** `Format` is now `Format<E: ?Sized>` (the `E: Error` bound on the trait is gone). Strategies that walk the source chain still need `E: Error` and declare it themselves. The motivation: composing strategies through non-error types (e.g. `WithContext`) required dropping the trait-level `Error` bound. Existing impls keep working — the bound just moves from the trait to each impl that needs it.
- **Breaking:** `with_context::Colon` and `with_context::PathColon` are now `type` aliases over `Add` rather than dedicated structs: `Colon = Add<Add<ContextField, ColonSpace>, ErrorField>`. The `WithContext::<_, _, Colon>` / `WithContextColon` / `WithPath` surface is unchanged; only code that named the strategies as `struct Colon;` (e.g. an explicit `impl ContextFormat for Colon`) is affected.
- **Breaking:** `WithContext`'s `Display`, `Error`, and `with_format` bounds switched from `F: ContextFormat<C, E>` to `F: Format<WithContext<C, E, F>>`.

### Removed

- **Breaking:** `FormatOneLine` type alias. Use `Formatted<E, OneLine>`.
- **Breaking:** `with_context::ContextFormat<C, E>` trait. Custom pair strategies now `impl<C, E, F> Format<WithContext<C, E, F>> for MyFmt` and pull fields off `&WithContext` directly. The pre-composed `Colon` / `PathColon` aliases cover the previous defaults.

## [0.1.0] - 2025-01-01

### Added
Expand Down
99 changes: 90 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,17 @@ fn create_with_retry(
You can nest the two: wrap a `WithContext<usize, io::Error>` inside a `WithPath<&Path, WithContext<usize, io::Error>>` and the chain prints `<path>: <attempt>: <io error>`.
The [`with_context`](https://github.com/maxwase/errortools/blob/master/examples/with_context.rs) example shows that through `MainResult` end-to-end.

Need a different look? Implement `ContextFormat<C, E>` (re-exported as
`errortools::with_context::ContextFormat`) on a unit type and plug it in
with `WithContext<C, E, MyFmt>`. The bounds are yours: `Colon` asks for
`Display`, `PathColon` asks for `AsRef<Path>`, you ask for whatever you
need.
Need a different look? `WithContext` formats through any `F: Format<WithContext<C, E, F>>`,
so there are two ways to customize it:

1. **Compose** with the built-in field extractors and separators:
`type SpacePair = WithSpace<ContextField, ErrorField>;` swaps `": "`
for a single space. Same recipe for any delimiter you can write as a
`Format` tag.
2. **Write a one-shot impl** when the layout is unusual:
`impl<C: Display, E: Display, F> Format<WithContext<C, E, F>> for MyFmt { ... }`.
You declare your own bounds — `Colon` asks for `Display`, `PathColon` asks
for `AsRef<Path>`, you ask for whatever you need.

## But why?

Expand Down Expand Up @@ -164,23 +170,98 @@ if let Err(e) = do_thing() {

## Custom formats

Implement the `Format` trait on a unit type:
Implement the `Format<E>` trait on a unit type. `E` is generic so your strategy can require extra bounds on the error type (e.g. `Suggest` for the suggestion strategy):

```rust,ignore
use core::{error::Error, fmt};
use errortools::{Format, FormatError, chain};
use itertools::Itertools;

struct Arrow;
impl Format for Arrow {
fn fmt(error: &dyn Error, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", chain(error).format(" -> "))
impl<E: Error + ?Sized> Format<E> for Arrow {
fn fmt(error: &E, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", chain(&error).format(" -> "))
}
}

println!("{}", my_error.formatted::<Arrow>()); // outer -> middle -> inner
```

## Combining strategies

`Add<L, R>` glues two `Format` strategies together. Both run against the same value, left then right. There's no built-in separator, drop a separator strategy (`NewLine`, `Space`, `Colon`, `ColonSpace`, `Empty`) in between, or reach for the three-arg `WithSep<L, Sep, R>` alias when you'd otherwise nest:

```rust,ignore
use errortools::{Formatted, OneLine, Suggestion, separator::{NewLine, WithSep}};

// Same as Add<Add<OneLine, NewLine>, Suggestion>. Renders:
// "<one-line chain>\n<top-level suggestion>"
type Brief = WithSep<OneLine, NewLine, Suggestion>;

eprintln!("{}", Formatted::<_, Brief>::new(err));
```

For the common separators there are zero-think aliases — `WithSpace<L, R>`,
`WithNewLine<L, R>`, `WithColonSpace<L, R>` — all in `errortools::separator`:

```rust,ignore
use errortools::{Formatted, OneLine, Suggestion, separator::WithNewLine};

type Brief = WithNewLine<OneLine, Suggestion>;
eprintln!("{}", Formatted::<_, Brief>::new(err));
```

Bounds compose: `Add<OneLine, Suggestion>` only implements `Format<E>` when
`E: Error + Suggest`, because `Suggestion`'s impl carries that bound.

The same combinator powers the `WithContext` default — `Colon` is just a type
alias for `WithColonSpace<ContextField, ErrorField>`, where
`ContextField`/`ErrorField` are extractor strategies that read the pair's
fields. To get a different delimiter, swap one piece:

```rust,ignore
use errortools::{WithContext, separator::WithSpace, with_context::{ContextField, ErrorField}};

type SpacePair = WithSpace<ContextField, ErrorField>;
let w = WithContext::<_, _, SpacePair>::new("step", "boom");
assert_eq!(w.to_string(), "step boom");
```

## Suggestions

For "Did you mean…" hints, implement `Suggest` on your error type and call
`error.suggestion()`:

```rust,ignore
use core::fmt;
use errortools::{FormatError, Suggest};

#[derive(Debug, thiserror::Error)]
enum Error {
#[error("Config file missing")]
NoConfig,
#[error("Network down")]
Network,
}

impl Suggest for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoConfig => f.write_str("Did you copy config.example.toml to config.toml?"),
Self::Network => Ok(()),
}
}
}

eprintln!("{}\n{}", Error::NoConfig.one_line(), Error::NoConfig.suggestion());
// Config file missing
// Did you copy config.example.toml to config.toml?
```

Only the top-level error's hint is printed, the source chain isn't walked. This decision is intentional: The underlying hint may be irrelevant in the context of the top-level error, and printing it may just add noise.

The idea is that every error that is supposed to have a suggestion should implement `Suggest` and then later the top-level error's suggestion may concatenate the inner hint if it's relevant with nesting matching the error chain.

## How it works

`MainResult<E, F>` is a type alias:
Expand Down
6 changes: 3 additions & 3 deletions examples/custom_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ use itertools::Itertools;

struct Arrow;

impl Format for Arrow {
fn fmt(error: &dyn Error, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", chain(error).format(" -> "))
impl<E: Error + ?Sized> Format<E> for Arrow {
fn fmt(error: &E, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", chain(&error).format(" -> "))
}
}

Expand Down
Loading