Skip to content

Commit

Permalink
downstream error-stack (#13)
Browse files Browse the repository at this point in the history
Approved by: @gshuflin
============
This PR moves the `error-stack` codebase into a module of `bigerror`. This opens up a lot of feature work such as:

* blanket implementations on `Report<C>` types
* new methods onto `Report<C>` and `Frame` without trait imports
* simplify naming schemes; ex: `attach_printable_lazy` -> `attach_lazy`

NOTE: this is a major breaking change
  • Loading branch information
mkatychev authored Jul 8, 2024
1 parent 78e88cb commit 91c529e
Show file tree
Hide file tree
Showing 116 changed files with 10,824 additions and 36 deletions.
70 changes: 61 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,24 +1,76 @@
[package]
name = "bigerror"
version = "0.8.1"
version = "0.9.0"
edition = "2021"
description = "handle big errors ¯\\_(ツ)_/¯"
license = "MIT"

[features]
default = ["std"]
std = ["error-stack/std", "error-stack/anyhow"]
spantrace = ["error-stack/spantrace"]
eyre = ["error-stack/eyre"]
serde = ["error-stack/serde"]
hooks = ["error-stack/hooks"]
grpc=["tonic", "tonic-types"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
spantrace = ["dep:tracing-error", "std"]
std = ["anyhow?/std"]
eyre = ["dep:eyre", "std"]
serde = ["dep:serde"]
hooks = ['dep:spin']

grpc=["tonic", "tonic-types"]

[dependencies]
error-stack = "0.4.1"
thiserror = "1"
tracing = "0.1"
tonic = { version = "0.11", optional = true }
tonic-types = { version = "0.11", optional = true }

tracing-error = { version = "0.2", optional = true, default-features = false }
anyhow = { version = ">=1.0.73", default-features = false, optional = true }
eyre = { version = "0.6", default-features = false, optional = true }
serde = { version = "1", default-features = false, optional = true }
spin = { version = "0.9", default-features = false, optional = true, features = ['rwlock', 'once'] }

[dev-dependencies]
serde = { version = "1.0.200", features = ["derive"] }
serde_json = "1.0.116"
futures = { version = "0.3.30", default-features = false, features = ["executor"] }
trybuild = "1.0.93"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
insta = { version = "1.38.0", features = ['filters', 'ron'] }
regex = "1.10.4"
expect-test = "1.5.0"
ansi-to-html = "0.2.1"
once_cell = "1.19.0"
supports-color = "3.0.0"
supports-unicode = "3.0.0"
owo-colors = "4.0.0"
thiserror = "1.0.59"

[package.metadata.docs.rs]
all-features = true
cargo-args = ["-Z", "unstable-options", "-Z", "rustdoc-scrape-examples"]
targets = ["x86_64-unknown-linux-gnu"]

[[example]]
name = "demo"
required-features = ["std"]
doc-scrape-examples = true

[[example]]
name = "exit_code"
required-features = ["std"]
doc-scrape-examples = true

[[example]]
name = "parse_config"
required-features = ["std"]
doc-scrape-examples = true

[[example]]
name = "detect"
required-features = ['std']
doc-scrape-examples = true


[[test]]
name = "common"
test = false
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ CREATE CRATE IF NOT EXISTS
error BIGERROR NOT NULL,
);
```

* Note: `#[cfg(nightly)]` flags are not actively tested and are retained as part of merging `error-stack` into `bigerror`
112 changes: 112 additions & 0 deletions examples/demo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// This is the same example also used in the README.md. When updating this, don't forget updating
// the README.md as well. This is mainly used to test the code and generate the output shown.

use core::fmt;

use bigerror::{error_stack::Result, Context, Report, ResultExt};

#[derive(Debug)]
struct ParseExperimentError;

impl fmt::Display for ParseExperimentError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str("invalid experiment description")
}
}

impl Context for ParseExperimentError {}

// Reason: false-positive, try_fold is fail-fast, our implementation is fail-slow.
#[allow(clippy::manual_try_fold)]
fn parse_experiment(description: &str) -> Result<Vec<(u64, u64)>, ParseExperimentError> {
let values = description
.split(' ')
.map(|value| {
value
.parse::<u64>()
.attach_printable_lazy(|| format!("{value:?} could not be parsed as experiment"))
})
.map(|value| value.map(|ok| (ok, 2 * ok)))
.fold(Ok(vec![]), |accum, value| match (accum, value) {
(Ok(mut accum), Ok(value)) => {
accum.push(value);

Ok(accum)
}
(Ok(_), Err(err)) => Err(err),
(Err(accum), Ok(_)) => Err(accum),
(Err(mut accum), Err(err)) => {
accum.extend_one(err);

Err(accum)
}
})
.change_context(ParseExperimentError)?;

Ok(values)
}

#[derive(Debug)]
struct ExperimentError;

impl fmt::Display for ExperimentError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str("experiment error: could not run experiment")
}
}

impl Context for ExperimentError {}

#[allow(clippy::manual_try_fold)]
fn start_experiments(
experiment_ids: &[usize],
experiment_descriptions: &[&str],
) -> Result<Vec<u64>, ExperimentError> {
let experiments = experiment_ids
.iter()
.map(|exp_id| {
let description = experiment_descriptions.get(*exp_id).ok_or_else(|| {
Report::new(ExperimentError)
.attach_printable(format!("experiment {exp_id} has no valid description"))
})?;

let experiments = parse_experiment(description)
.attach_printable(format!("experiment {exp_id} could not be parsed"))
.change_context(ExperimentError)?;

let experiments = experiments
.into_iter()
.map(|(a, b)| move || a * b)
.collect::<Vec<_>>();

Ok(experiments)
})
.fold(
Ok(vec![]),
|accum: Result<_, ExperimentError>, value| match (accum, value) {
(Ok(mut accum), Ok(value)) => {
accum.extend(value);

Ok(accum)
}
(Ok(_), Err(err)) => Err(err),
(Err(accum), Ok(_)) => Err(accum),
(Err(mut accum), Err(err)) => {
accum.extend_one(err);

Err(accum)
}
},
)
.attach_printable("unable to set up experiments")?;

Ok(experiments.iter().map(|experiment| experiment()).collect())
}

fn main() -> Result<(), ExperimentError> {
let experiment_ids = &[0, 2, 3];
let experiment_descriptions = &["10", "20", "3o 4a"];
start_experiments(experiment_ids, experiment_descriptions)?;

Ok(())
}
69 changes: 69 additions & 0 deletions examples/detect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#![allow(clippy::print_stdout, clippy::use_debug)]
// This example shows how you can use the `supports-color` and `supports-unicode` crates to
// automatically enable or disable the color mode and set the appropriate charset.
// Your default terminal should automatically show colored unicode output, to emulate a terminal
// that does not support color set your `$TERM` env-variable **temporarily** to "dumb", or set the
// env-variable `NO_COLOR=1`. To emulate no-unicode support set your `$TERM` variable
// **temporarily** to `linux`.

use core::fmt::{Display, Formatter};
use std::path::Path;

use bigerror::{
error_stack::Result,
fmt::{Charset, ColorMode},
Report,
};

type Config = String;

#[derive(Debug)]
struct ParseConfigError;

impl Display for ParseConfigError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.write_str("unable to parse config")
}
}

impl std::error::Error for ParseConfigError {}

fn parse_config(path: impl AsRef<Path>) -> Result<Config, ParseConfigError> {
_ = path.as_ref();

/*
usually you would actually do something here, we just error out, for a more complete example
check out the other examples
*/

Err(Report::new(ParseConfigError).attach_printable("unable to read configuration"))
}

fn main() {
// error-stack only uses ANSI codes for colors
let supports_color = supports_color::on_cached(supports_color::Stream::Stdout)
.map_or(false, |level| level.has_basic);

let color_mode = if supports_color {
ColorMode::Color
} else {
ColorMode::None
};

let supports_unicode = supports_unicode::on(supports_unicode::Stream::Stdout);

let charset = if supports_unicode {
Charset::Utf8
} else {
Charset::Ascii
};

Report::set_color_mode(color_mode);
Report::set_charset(charset);

if let Err(err) = parse_config("config.json") {
// if you would use `eprintln!` instead, you should check support on `Stream::Stderr`
// instead.
println!("{err:?}");
}
}
24 changes: 24 additions & 0 deletions examples/exit_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! Example of using `attach` to set a custom exit code. Requires nightly and std feature.
use std::process::{ExitCode, Termination};

use bigerror::{Context, Report};

#[derive(Debug)]
struct CustomError;

impl Context for CustomError {}

impl core::fmt::Display for CustomError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("Custom Error")
}
}

fn main() -> ExitCode {
let report = Report::new(CustomError)
.attach(ExitCode::from(100))
.attach_printable("this error has an exit code of 100!");

report.report()
}
55 changes: 55 additions & 0 deletions examples/parse_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#![allow(clippy::print_stderr, unreachable_pub, clippy::use_debug)]
// This is the same example also used in `lib.rs`. When updating this, don't forget updating the doc
// example as well. This example is mainly used to generate the output shown in the documentation.

use core::fmt;
use std::{fs, path::Path};

use bigerror::{Context, Report, ResultExt};

pub type Config = String;

#[derive(Debug)]
struct ParseConfigError;

impl ParseConfigError {
#[must_use]
pub const fn new() -> Self {
Self
}
}

impl fmt::Display for ParseConfigError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str("Could not parse configuration file")
}
}

impl Context for ParseConfigError {}

struct Suggestion(&'static str);

fn parse_config(path: impl AsRef<Path>) -> Result<Config, Report<ParseConfigError>> {
let path = path.as_ref();

let content = fs::read_to_string(path)
.change_context(ParseConfigError::new())
.attach(Suggestion("use a file you can read next time!"))
.attach_printable_lazy(|| format!("could not read file {path:?}"))?;

Ok(content)
}

fn main() {
if let Err(report) = parse_config("config.json") {
eprintln!("{report:?}");
#[cfg(nightly)]
for suggestion in report.request_ref::<Suggestion>() {
eprintln!("Suggestion: {}", suggestion.0);
}
#[cfg(not(nightly))]
while let Some(suggestion) = report.downcast_ref::<Suggestion>() {
eprintln!("Suggestion: {}", suggestion.0);
}
}
}
2 changes: 1 addition & 1 deletion src/attachment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::{any, fmt, time::Duration};

use tracing::error;

pub use error_stack::{self, Context, Report, ResultExt};
pub use crate::{Context, Report, ResultExt};
pub use thiserror;

use crate::reportable;
Expand Down
8 changes: 4 additions & 4 deletions src/context.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::{any, path::Path, time::Duration};

use error_stack::Context;

use crate::{
attachment::{self, FromTo, Unsupported},
ty, AttachExt, Report, Reportable,
attachment::{
FromTo, Unsupported, {self},
},
ty, AttachExt, Context, Report, Reportable,
};

use crate::{attachment::DisplayDuration, reportable, Field};
Expand Down
Loading

0 comments on commit 91c529e

Please sign in to comment.