Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8af04f7
feat: first implementation
indietyp Oct 30, 2022
c956c1b
fix: imports
indietyp Oct 30, 2022
ee0071a
feat: test serialize logic
indietyp Oct 30, 2022
1097abe
feat: test multiple sources
indietyp Oct 30, 2022
974614d
fix: lint
indietyp Nov 1, 2022
7f4c5b0
chore: update snapshots
indietyp Nov 1, 2022
83bff7f
ci: explicit `no-default` update-snapshot task
indietyp Nov 1, 2022
61f4112
fix: `insta` does not like `miri`, therefore disabled
indietyp Nov 1, 2022
eeaedbb
feat: apply suggestions from talks
indietyp Nov 9, 2022
ba6a7bb
Merge remote-tracking branch 'origin/main' into bm/error-stack/serde
indietyp Nov 9, 2022
b633047
fix: replace `()` with `None` for opaque attachments
indietyp Nov 9, 2022
667479b
fix: merge errors
indietyp Nov 9, 2022
cb0c7b7
chore: update snapshots
indietyp Nov 9, 2022
9240139
feat: implement suggestions
indietyp Nov 9, 2022
2d3b42f
docs: adjust sources
indietyp Nov 9, 2022
f4c6722
chore: update snapshots
indietyp Nov 9, 2022
20cc01b
fix: clippy
indietyp Nov 9, 2022
cd3f8ed
Apply suggestions from code review
indietyp Nov 18, 2022
0147679
chore: rename `ser` to `serde`
indietyp Nov 18, 2022
0414ac3
docs: clarify why context
indietyp Nov 18, 2022
43279ba
chore: `SerializeAttachments` -> `SerializeAttachmentList`
indietyp Nov 18, 2022
4c67564
docs: attribute correct type
indietyp Nov 18, 2022
763a7af
chore: move `create_report`
indietyp Nov 18, 2022
8fc92ae
Merge branch 'main' into bm/error-stack/serde
TimDiekmann Nov 22, 2022
c170a06
Merge branch 'main' into bm/error-stack/serde
indietyp Nov 22, 2022
4606aa9
Update packages/libs/error-stack/src/serde.rs
indietyp Nov 22, 2022
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
4 changes: 3 additions & 1 deletion packages/libs/error-stack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ tracing-error = { version = "0.2", optional = true, default_features = false }
anyhow = { version = "1", default-features = false, optional = true }
eyre = { version = "0.6", default-features = false, optional = true }
owo-colors = { version = "3", default-features = false, optional = true, features = ['supports-colors'] }
serde = { version = "1.0.147", default-features = false, optional = true }

[dev-dependencies]
serde = { version = "1.0.137", features = ["derive"] }
Expand All @@ -29,7 +30,7 @@ futures = { version = "0.3.21", default-features = false, features = ["executor"
trybuild = "1.0.63"
tracing = "0.1.35"
tracing-subscriber = "0.3.14"
insta = { version = "1.18.0", features = ['filters'] }
insta = { version = "1.18.0", features = ['filters', 'ron'] }
regex = "1.6.0"
expect-test = "1.4.0"
ansi-to-html = "0.1.0"
Expand All @@ -45,6 +46,7 @@ pretty-print = ["dep:owo-colors"]
spantrace = ["dep:tracing-error", "std"]
std = ["anyhow?/std"]
eyre = ["dep:eyre", "std"]
serde = ["dep:serde"]

[package.metadata.docs.rs]
all-features = true
Expand Down
14 changes: 12 additions & 2 deletions packages/libs/error-stack/Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,23 @@ run_task = { name = ['update-snapshots-task'] }

[tasks.update-snapshots-task]
private = true
run_task = { name = ['update-snapshots-task-lib', 'update-snapshots-task-doc'] }
run_task = { name = ['update-snapshots-task-lib-fmt', 'update-snapshots-task-lib-ser', 'update-snapshots-task-lib-no-default', 'update-snapshots-task-doc'] }

[tasks.update-snapshots-task-lib]
[tasks.update-snapshots-task-lib-fmt]
extend = "cargo"
args = ["hack", "@@split(CARGO_INSTA_TEST_HACK_FLAGS, )", "nextest", "run", "--cargo-profile", "${CARGO_MAKE_CARGO_PROFILE}", "@@split(CARGO_TEST_FLAGS, )", "@@split(CARGO_INSTA_TEST_FLAGS, )", "${@}"]
env = { "INSTA_FORCE_PASS" = "1", "RUST_BACKTRACE" = "1", "INSTA_UPDATE" = "new" }

[tasks.update-snapshots-task-lib-ser]
extend = "cargo"
args = ["nextest", "run", "--cargo-profile", "${CARGO_MAKE_CARGO_PROFILE}", "@@split(CARGO_TEST_FLAGS, )", "--features", "std,spantrace,serde"]
env = { "INSTA_FORCE_PASS" = "1", "RUST_BACKTRACE" = "1", "INSTA_UPDATE" = "new" }

[tasks.update-snapshots-task-lib-no-default]
extend = "cargo"
args = ["nextest", "run", "--cargo-profile", "${CARGO_MAKE_CARGO_PROFILE}", "@@split(CARGO_TEST_FLAGS, )", '--no-default-features']
env = { "INSTA_FORCE_PASS" = "1", "RUST_BACKTRACE" = "1", "INSTA_UPDATE" = "new" }

[tasks.update-snapshots-task-doc]
# only run on nightly, as backtraces are otherwise not included
condition = { channels = ["nightly"] }
Expand Down
2 changes: 2 additions & 0 deletions packages/libs/error-stack/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,8 @@ pub mod fmt;
mod fmt;
#[cfg(feature = "std")]
mod hook;
#[cfg(feature = "serde")]
mod serde;

#[cfg(feature = "std")]
#[allow(deprecated, unreachable_pub)]
Expand Down
153 changes: 153 additions & 0 deletions packages/libs/error-stack/src/serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Implementation of general [`Report`] serialization.
//!
//! The value can be of any type, currently only printable attachments and context are supported, in
//! the near future any values will be supported through the use of hooks.
//!
//! The serialized [`Report`] is a list of all current sources with the following output:
//!
//! ```json
//! {
//! "context": "context display output",
//! "attachments": ["all", "attachments", "leading", "up", "to", "this", "context"],
//! "sources": [] // recursive render using `frame.sources()`
//! }
//! ```

use alloc::{format, vec, vec::Vec};

use serde::{ser::SerializeMap, Serialize, Serializer};

use crate::{AttachmentKind, Context, Frame, FrameKind, Report};

struct SerializeAttachment<'a>(&'a Frame);

impl<'a> Serialize for SerializeAttachment<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let Self(frame) = self;

#[allow(clippy::match_same_arms)]
match frame.kind() {
FrameKind::Context(_) => {
// TODO: for now `Context` is unsupported, upcoming PR will fix via hooks
// `SerializeAttachmentList` ensures that no context is ever serialized
todo!()
}
FrameKind::Attachment(AttachmentKind::Opaque(_)) => {
// TODO: for now opaque attachments are unsupported, upcoming PR will fix that
// `SerializeAttachmentList` ensures that no such attachment is added
todo!()
}
FrameKind::Attachment(AttachmentKind::Printable(attachment)) => {
format!("{attachment}").serialize(serializer)
}
}
}
}

struct SerializeAttachmentList<'a, 'b>(&'a [&'b Frame]);

impl<'a, 'b> Serialize for SerializeAttachmentList<'a, 'b> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_seq(
self.0
.iter()
.copied()
.filter(|attachment| {
// for now opaque attachments are ignored
!matches!(
attachment.kind(),
FrameKind::Attachment(AttachmentKind::Opaque(_))
)
})
.map(SerializeAttachment),
)
}
}

struct SerializeContext<'a> {
attachments: Vec<&'a Frame>,
context: &'a dyn Context,
sources: &'a [Frame],
}

impl<'a> Serialize for SerializeContext<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let Self {
context,
attachments,
sources,
} = self;

let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("context", &format!("{context}"))?;
map.serialize_entry("attachments", &SerializeAttachmentList(&attachments[..]))?;
map.serialize_entry("sources", &SerializeSources(sources))?;

map.end()
}
}

struct SerializeSources<'a>(&'a [Frame]);

impl<'a> Serialize for SerializeSources<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_seq(self.0.iter().flat_map(|source| find_next(&[], source)))
}
}

// find the next applicable context and return the serializer
fn find_next<'a>(head: &[&'a Frame], mut current: &'a Frame) -> Vec<SerializeContext<'a>> {
let mut attachments = vec![];
attachments.extend(head);

loop {
if let FrameKind::Context(context) = current.kind() {
// found the context, return all attachments (reversed)
attachments.reverse();

return vec![SerializeContext {
attachments,
context,
sources: current.sources(),
}];
} else if current.sources().len() > 1 {
// current is an attachment, add to attachments and recursively probe
attachments.push(current);

return current
.sources()
.iter()
.flat_map(|source| find_next(&attachments, source))
.collect();
} else if current.sources().len() == 1 {
attachments.push(current);

current = &current.sources()[0];
} else {
// there are no more frames, therefore we need to abandon
// this is theoretically impossible (the bottom is always a context), but not enforced
return vec![];
}
}
}

impl<C: Context> Serialize for Report<C> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
SerializeSources(self.current_frames()).serialize(serializer)
}
}
10 changes: 10 additions & 0 deletions packages/libs/error-stack/tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ impl fmt::Display for PrintableB {
}
}

#[derive(Debug, PartialEq, Eq)]
pub struct PrintableC(pub u32);

impl fmt::Display for PrintableC {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str("printable C: ")?;
fmt::Display::fmt(&self.0, fmt)
}
}

pub fn create_error() -> Result<(), RootError> {
Err(create_report())
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions packages/libs/error-stack/tests/test_serialize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! Note: span_trace, backtrace and such are not special cased, therefore all tests run with all
//! tests enabled.
#![cfg(all(feature = "std", feature = "spantrace", feature = "serde"))]
// can be considered safe, because we only check the output, which in itself does not use **any**
// unsafe code.
#![cfg(not(miri))]
#![cfg_attr(all(nightly, feature = "std"), feature(error_generic_member_access))]
#![cfg_attr(nightly, feature(provide_any))]

use insta::assert_ron_snapshot;

use crate::common::{create_report, ContextA, PrintableA, PrintableC};

mod common;

fn prepare() -> impl Drop {
std::env::set_var("RUST_LIB_BACKTRACE", "0");

let settings = insta::Settings::clone_current();

settings.bind_to_scope()
}

#[test]
fn attachment() {
let _guard = prepare();

let report = create_report().attach_printable(PrintableA(2));

assert_ron_snapshot!(report);
}

#[test]
fn context() {
let _guard = prepare();

let report = create_report()
.attach_printable(PrintableA(2))
.change_context(ContextA(2));

assert_ron_snapshot!(report);
}

#[test]
fn multiple_sources() {
let _guard = prepare();

let mut a = create_report().attach_printable(PrintableC(1));
let b = create_report().attach_printable(PrintableC(2));

a.extend_one(b);

let a = a
.attach_printable(PrintableC(3))
.change_context(ContextA(2))
.attach_printable(PrintableC(4));

assert_ron_snapshot!(a);
}

#[test]
fn multiple_sources_at_root() {
let _guard = prepare();

let mut a = create_report().attach_printable(PrintableC(1));
let b = create_report().attach_printable(PrintableC(2));

a.extend_one(b);

assert_ron_snapshot!(a);
}