diff --git a/Cargo.lock b/Cargo.lock index 397a342301..441fc0ce23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -489,6 +489,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bitwarden-error" +version = "1.0.0" +dependencies = [ + "bitwarden-error-macro", +] + +[[package]] +name = "bitwarden-error-macro" +version = "1.0.0" +dependencies = [ + "bitwarden-error", + "quote", + "syn 2.0.79", +] + [[package]] name = "bitwarden-exporters" version = "1.0.0" diff --git a/crates/bitwarden-error-macro/Cargo.toml b/crates/bitwarden-error-macro/Cargo.toml new file mode 100644 index 0000000000..f42651e17b --- /dev/null +++ b/crates/bitwarden-error-macro/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bitwarden-error-macro" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +keywords.workspace = true + +[dependencies] +quote = "1.0.37" +syn = "2.0.79" + +[dev-dependencies] +bitwarden-error = { path = "../bitwarden-error" } + +[lib] +proc-macro = true + +[lints] +workspace = true diff --git a/crates/bitwarden-error-macro/src/lib.rs b/crates/bitwarden-error-macro/src/lib.rs new file mode 100644 index 0000000000..f04a4a7455 --- /dev/null +++ b/crates/bitwarden-error-macro/src/lib.rs @@ -0,0 +1,51 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Fields}; + +#[proc_macro_derive(FlatError)] +pub fn derive_flat_error(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + // Generate match arms without converting variant names to strings + let variant_matches: Vec<_> = if let Data::Enum(data_enum) = &input.data { + data_enum + .variants + .iter() + .map(|variant| { + let variant_ident = &variant.ident; + let message = match &variant.fields { + Fields::Unnamed(_) | Fields::Named(_) => { + format!("Error: {}", variant_ident) + } + Fields::Unit => { + format!("{}", variant_ident) + } + }; + quote! { + #name::#variant_ident { .. } => (stringify!(#variant_ident), #message), + } + }) + .collect() + } else { + panic!("FlatError can only be derived for enums"); + }; + + let expanded = quote! { + impl FlatError for #name { + fn get_variant(&self) -> &str { + match self { + #(#variant_matches)* + }.0 + } + + fn get_message(&self) -> &str { + match self { + #(#variant_matches)* + }.1 + } + } + }; + + TokenStream::from(expanded) +} diff --git a/crates/bitwarden-error-macro/tests/mod.rs b/crates/bitwarden-error-macro/tests/mod.rs new file mode 100644 index 0000000000..cd2c133981 --- /dev/null +++ b/crates/bitwarden-error-macro/tests/mod.rs @@ -0,0 +1,50 @@ +use bitwarden_error::prelude::*; + +#[test] +fn flattens_basic_enums() { + #[derive(FlatError)] + enum Errors { + Foo, + Bar, + Baz, + } + + let foo = Errors::Foo; + let bar = Errors::Bar; + let baz = Errors::Baz; + + assert_eq!(foo.get_variant(), "Foo"); + assert_eq!(bar.get_variant(), "Bar"); + assert_eq!(baz.get_variant(), "Baz"); + + assert_eq!(foo.get_message(), "Foo"); + assert_eq!(bar.get_message(), "Bar"); + assert_eq!(baz.get_message(), "Baz"); +} + +#[test] +fn flattens_enums_with_fields() { + #[derive(FlatError)] + enum Errors { + #[allow(dead_code)] + Foo(String), + #[allow(dead_code)] + Bar(u32), + Baz, + } + + let foo = Errors::Foo("hello".to_string()); + let bar = Errors::Bar(42); + let baz = Errors::Baz; + + assert_eq!(foo.get_variant(), "Foo"); + assert_eq!(bar.get_variant(), "Bar"); + assert_eq!(baz.get_variant(), "Baz"); + + // The message is always "Error: " + // TODO: Add support for getting the message from the fields + // or maybe just remove get_message and rely on ToString + assert_eq!(foo.get_message(), "Error: Foo"); + assert_eq!(bar.get_message(), "Error: Bar"); + assert_eq!(baz.get_message(), "Baz"); +} diff --git a/crates/bitwarden-error/Cargo.toml b/crates/bitwarden-error/Cargo.toml new file mode 100644 index 0000000000..78af969fa5 --- /dev/null +++ b/crates/bitwarden-error/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "bitwarden-error" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +keywords.workspace = true + +[dependencies] +bitwarden-error-macro = { path = "../bitwarden-error-macro" } + +[lints] +workspace = true diff --git a/crates/bitwarden-error/src/flat_error.rs b/crates/bitwarden-error/src/flat_error.rs new file mode 100644 index 0000000000..afd844a08c --- /dev/null +++ b/crates/bitwarden-error/src/flat_error.rs @@ -0,0 +1,4 @@ +pub trait FlatError { + fn get_variant(&self) -> &str; + fn get_message(&self) -> &str; +} diff --git a/crates/bitwarden-error/src/lib.rs b/crates/bitwarden-error/src/lib.rs new file mode 100644 index 0000000000..6a70954495 --- /dev/null +++ b/crates/bitwarden-error/src/lib.rs @@ -0,0 +1,8 @@ +mod flat_error; + +pub use flat_error::FlatError; + +pub mod prelude { + pub use crate::FlatError; + pub use bitwarden_error_macro::FlatError; +}