Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use snafu handle error #1

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
270 changes: 248 additions & 22 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[workspace]
members = ["./migration", "."]
members = ["./migration", ".", "macros", "common-error"]

[dependencies]
thiserror = "1"
clap = { version = "4.5.4", features = ["derive"] }

serde = { version = "1", features = ["derive"] }
Expand All @@ -18,7 +17,6 @@ serde_yaml = "0.9"
serde_variant = "0.1"

lazy_static = "1.4"
eyre = "0.6.12"
fs-err = "2.11"
tera = "1.19.1"

Expand Down Expand Up @@ -58,3 +56,6 @@ hmac = "0.12.1"
base64 = "0.22.1"
chrono-tz = "0.9.0"
octocrab = "0.38.0"
snafu = "0.8.3"
macros = { path = "macros" }
common-error = { path = "common-error" }
9 changes: 9 additions & 0 deletions common-error/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "common-error"
version = "0.1.0"
edition = "2021"

[dependencies]
strum = { version = "0.25", features = ["derive"] }
tonic = { version = "0.11", features = ["tls", "gzip", "zstd"] }
snafu = "0.8.3"
40 changes: 40 additions & 0 deletions common-error/src/ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use std::sync::Arc;

pub trait StackError: std::error::Error {
fn debug_fmt(&self, layer: usize, buf: &mut Vec<String>);

fn next(&self) -> Option<&dyn StackError>;

fn last(&self) -> &dyn StackError
where
Self: Sized,
{
let Some(mut result) = self.next() else {
return self;
};
while let Some(err) = result.next() {
result = err;
}
result
}
}

impl<T: ?Sized + StackError> StackError for Arc<T> {
fn debug_fmt(&self, layer: usize, buf: &mut Vec<String>) {
self.as_ref().debug_fmt(layer, buf)
}

fn next(&self) -> Option<&dyn StackError> {
self.as_ref().next()
}
}

impl<T: StackError> StackError for Box<T> {
fn debug_fmt(&self, layer: usize, buf: &mut Vec<String>) {
self.as_ref().debug_fmt(layer, buf)
}

fn next(&self) -> Option<&dyn StackError> {
self.as_ref().next()
}
}
1 change: 1 addition & 0 deletions common-error/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod ext;
20 changes: 20 additions & 0 deletions macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "macros"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0.85"
quote = "1.0.36"
syn = { version = "2.0", features = [
"derive",
"parsing",
"printing",
"clone-impls",
"proc-macro",
"extra-traits",
"full",
] }
7 changes: 7 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod stack_trace_debug;
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn stack_trace_debug(args: TokenStream, input: TokenStream) -> TokenStream {
stack_trace_debug::stack_trace_style_impl(args.into(), input.into()).into()
}
248 changes: 248 additions & 0 deletions macros/src/stack_trace_debug.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{parenthesized, spanned::Spanned, Attribute, Ident, ItemEnum, Variant};

pub fn stack_trace_style_impl(args: TokenStream, input: TokenStream) -> TokenStream {
let input_cloned: TokenStream = input.clone();
let error_enum_definition: ItemEnum = syn::parse2(input_cloned).unwrap();
let enum_name = error_enum_definition.ident;

let mut variants = vec![];

for error_variant in error_enum_definition.variants {
let variant = ErrorVariant::from_enum_variant(error_variant);
variants.push(variant);
}

let debug_fmt_fn = build_debug_fmt_impl(enum_name.clone(), variants.clone());
let next_fn = build_next_impl(enum_name.clone(), variants);
let debug_impl = build_debug_impl(enum_name.clone());

quote! {
#args
#input

impl ::common_error::ext::StackError for #enum_name {
#debug_fmt_fn
#next_fn
}

#debug_impl
}
}

fn build_debug_fmt_impl(enum_name: Ident, variants: Vec<ErrorVariant>) -> TokenStream {
let match_arms = variants
.iter()
.map(|v| v.to_debug_match_arm())
.collect::<Vec<_>>();

quote! {
fn debug_fmt(&self, layer: usize, buf: &mut Vec<String>) {
use #enum_name::*;
match self {
#(#match_arms)*
}
}
}
}

fn build_next_impl(enum_name: Ident, variants: Vec<ErrorVariant>) -> TokenStream {
let match_arms = variants
.iter()
.map(|v| v.to_next_match_arm())
.collect::<Vec<_>>();

quote! {
fn next(&self) -> Option<&dyn ::common_error::ext::StackError> {
use #enum_name::*;
match self {
#(#match_arms)*
}
}
}
}

/// Implement [std::fmt::Debug] via `debug_fmt`
fn build_debug_impl(enum_name: Ident) -> TokenStream {
quote! {
impl std::fmt::Debug for #enum_name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use ::common_error::ext::StackError;
let mut buf = vec![];
self.debug_fmt(0, &mut buf);
write!(f, "{}", buf.join("\n"))
}
}
}
}

#[derive(Clone, Debug)]
struct ErrorVariant {
name: Ident,
fields: Vec<Ident>,
has_location: bool,
has_source: bool,
has_external_cause: bool,
display: TokenStream,
span: Span,
cfg_attr: Option<Attribute>,
}

impl ErrorVariant {
/// Construct self from [Variant]
fn from_enum_variant(variant: Variant) -> Self {
let span = variant.span();
let mut has_location = false;
let mut has_source = false;
let mut has_external_cause = false;

for field in &variant.fields {
if let Some(ident) = &field.ident {
if ident == "location" {
has_location = true;
} else if ident == "source" {
has_source = true;
} else if ident == "error" {
has_external_cause = true;
}
}
}

let mut display = None;
let mut cfg_attr = None;
for attr in variant.attrs {
if attr.path().is_ident("snafu") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("display") {
let content;
parenthesized!(content in meta.input);
let display_ts: TokenStream = content.parse()?;
display = Some(display_ts);
Ok(())
} else {
Err(meta.error("unrecognized repr"))
}
})
.expect("Each error should contains a display attribute");
}

if attr.path().is_ident("cfg") {
cfg_attr = Some(attr);
}
}

let field_ident = variant
.fields
.iter()
.map(|f| f.ident.clone().unwrap_or_else(|| Ident::new("_", f.span())))
.collect();

Self {
name: variant.ident,
fields: field_ident,
has_location,
has_source,
has_external_cause,
display: display.unwrap(),
span,
cfg_attr,
}
}

/// Convert self into an match arm that will be used in [build_debug_impl].
///
/// The generated match arm will be like:
/// ```rust, ignore
/// ErrorKindWithSource { source, .. } => {
/// debug_fmt(source, layer + 1, buf);
/// },
/// ErrorKindWithoutSource { .. } => {
/// buf.push(format!("{layer}: {}, at {}", format!(#display), location)));
/// }
/// ```
///
/// The generated code assumes fn `debug_fmt`, var `layer`, var `buf` are in scope.
fn to_debug_match_arm(&self) -> TokenStream {
let name = &self.name;
let fields = &self.fields;
let display = &self.display;
let cfg = if let Some(cfg) = &self.cfg_attr {
quote_spanned!(cfg.span() => #cfg)
} else {
quote! {}
};

match (self.has_location, self.has_source, self.has_external_cause) {
(true, true, _) => quote_spanned! {
self.span => #cfg #[allow(unused_variables)] #name { #(#fields),*, } => {
buf.push(format!("{layer}: {}, at {}", format!(#display), location));
source.debug_fmt(layer + 1, buf);
},
},
(true, false, true) => quote_spanned! {
self.span => #cfg #[allow(unused_variables)] #name { #(#fields),* } => {
buf.push(format!("{layer}: {}, at {}", format!(#display), location));
buf.push(format!("{}: {:?}", layer + 1, error));
},
},
(true, false, false) => quote_spanned! {
self.span => #cfg #[allow(unused_variables)] #name { #(#fields),* } => {
buf.push(format!("{layer}: {}, at {}", format!(#display), location));
},
},
(false, true, _) => quote_spanned! {
self.span => #cfg #[allow(unused_variables)] #name { #(#fields),* } => {
buf.push(format!("{layer}: {}", format!(#display)));
source.debug_fmt(layer + 1, buf);
},
},
(false, false, true) => quote_spanned! {
self.span => #cfg #[allow(unused_variables)] #name { #(#fields),* } => {
buf.push(format!("{layer}: {}", format!(#display)));
buf.push(format!("{}: {:?}", layer + 1, error));
},
},
(false, false, false) => quote_spanned! {
self.span => #cfg #[allow(unused_variables)] #name { #(#fields),* } => {
buf.push(format!("{layer}: {}", format!(#display)));
},
},
}
}

/// Convert self into an match arm that will be used in [build_next_impl].
///
/// The generated match arm will be like:
/// ```rust, ignore
/// ErrorKindWithSource { source, .. } => {
/// Some(source)
/// },
/// ErrorKindWithoutSource { .. } => {
/// None
/// }
/// ```
fn to_next_match_arm(&self) -> TokenStream {
let name = &self.name;
let fields = &self.fields;
let cfg = if let Some(cfg) = &self.cfg_attr {
quote_spanned!(cfg.span() => #cfg)
} else {
quote! {}
};

if self.has_source {
quote_spanned! {
self.span => #cfg #[allow(unused_variables)] #name { #(#fields),* } => {
Some(source)
},
}
} else {
quote_spanned! {
self.span => #cfg #[allow(unused_variables)] #name { #(#fields),* } =>{
None
}
}
}
}
}
Loading
Loading