mod gen_display;
mod gen_error;
mod gen_module;
mod gen_selectors;
mod generics;
pub mod model;
pub mod parse;
mod size;
use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, quote};
use syn::DeriveInput;
pub use self::gen_display::{gen_enum_display, gen_struct_display};
pub use self::gen_error::{gen_enum_error, gen_struct_error};
pub use self::gen_module::wrap_in_module;
pub use self::gen_selectors::{gen_enum_selectors, gen_struct_selector};
pub use self::model::{ResolvedEnum, ResolvedStruct};
pub use self::parse::{EnumContainerAttrs, StructAttrs};
use self::size::{
gen_default_size_cap_enum, gen_default_size_cap_struct, gen_enum_size_assertion,
gen_size_assertion, manifest_size_cap, reject_size_with_generics, wrap_size_assertion_in_const,
};
pub fn expand(input: TokenStream2) -> syn::Result<TokenStream2> {
let input: DeriveInput = syn::parse2(input)?;
match &input.data {
syn::Data::Enum(_) => {
let attrs = EnumContainerAttrs::from_attrs(&input.attrs)?;
expand_enum(&input, &attrs, false)
}
syn::Data::Struct(_) => {
let attrs = StructAttrs::from_attrs(&input.attrs)?;
expand_struct(&input, &attrs)
}
syn::Data::Union(_) => Err(syn::Error::new_spanned(
input,
"#[derive(Oopsie)] cannot be applied to unions",
)),
}
}
fn gen_site_registration(oopsie_path: &syn::Path) -> TokenStream2 {
quote! {
#oopsie_path::__register_generated_site!();
}
}
pub fn expand_enum(
input: &DeriveInput,
attrs: &EnumContainerAttrs,
tracing_active: bool,
) -> syn::Result<TokenStream2> {
reject_size_with_generics(input, attrs.size.as_ref())?;
let resolved = ResolvedEnum::resolve(input, attrs)?;
if !tracing_active {
resolved.reject_inert_variant_traced()?;
}
let path = attrs.oopsie_path();
let selectors = gen_enum_selectors(&resolved, &path)?;
let display = gen_enum_display(&resolved);
let error = gen_enum_error(&resolved, &path)?;
let effective_module = attrs.effective_module(true);
let module_vis = attrs
.visibility()
.cloned()
.or_else(crate::utils::manifest_vis_default)
.unwrap_or_else(|| input.vis.clone());
let wrapped_selectors =
wrap_in_module(&effective_module, &input.ident, &module_vis, &selectors);
let (cap_info, cap_err) = manifest_size_cap();
let assertion = if let Some(c) = attrs.size.as_ref() {
gen_enum_size_assertion(&path, &input.ident, &resolved.variants, c)
} else if let Some((cap, section)) = cap_info.filter(|_| input.generics.params.is_empty()) {
gen_default_size_cap_enum(&path, &input.ident, &resolved.variants, cap, section)
} else {
quote! {}
};
let size_assert = assertion.wrap(wrap_size_assertion_in_const);
let keyword_docs = crate::keyword_docs::gen_keyword_docs(input, &path);
let site_registration = gen_site_registration(&path);
let naming_err = crate::utils::manifest_naming().1;
Ok(quote! {
#wrapped_selectors
#display
#error
#size_assert
#cap_err
#naming_err
#keyword_docs
#site_registration
})
}
pub fn expand_struct(input: &DeriveInput, attrs: &StructAttrs) -> syn::Result<TokenStream2> {
reject_size_with_generics(input, attrs.container.size.as_ref())?;
let resolved = ResolvedStruct::resolve(input, attrs)?;
let path = attrs.container.oopsie_path();
let selector = gen_struct_selector(&resolved, &path)?;
let display = gen_struct_display(&resolved);
let error = gen_struct_error(&resolved, &path)?;
let (cap_info, cap_err) = manifest_size_cap();
let assertion = if let Some(c) = attrs.container.size.as_ref() {
gen_size_assertion(&path, &input.ident, c)
} else if let Some((cap, section)) = cap_info.filter(|_| input.generics.params.is_empty()) {
gen_default_size_cap_struct(&path, &input.ident, cap, section)
} else {
quote! {}
};
let size_assert = assertion.wrap(wrap_size_assertion_in_const);
let effective_module = attrs.container.effective_module(false);
let module_vis = attrs
.visibility()
.cloned()
.or_else(crate::utils::manifest_vis_default)
.unwrap_or_else(|| input.vis.clone());
let wrapped_selector = if attrs.transparent {
selector
} else {
wrap_in_module(
&effective_module,
&input.ident,
&module_vis,
std::slice::from_ref(&selector),
)
};
let keyword_docs = crate::keyword_docs::gen_keyword_docs(input, &path);
let site_registration = gen_site_registration(&path);
let naming_err = crate::utils::manifest_naming().1;
Ok(quote! {
#wrapped_selector
#display
#error
#size_assert
#cap_err
#naming_err
#keyword_docs
#site_registration
})
}
pub trait TokenStreamExt {
fn wrap(&self, apply: impl FnOnce(&Self) -> TokenStream2) -> TokenStream2;
}
impl<T: ToTokens> TokenStreamExt for T {
fn wrap(&self, apply: impl FnOnce(&Self) -> TokenStream2) -> TokenStream2 {
apply(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
#[test]
fn test_derive_struct_minimal() {
let input = quote! {
pub struct MyError {
message: String,
}
};
let output = expand(input).unwrap().to_string();
insta::assert_snapshot!(output);
}
#[test]
fn test_derive_struct_with_display() {
let input = quote! {
#[oopsie("Test error: {message}")]
pub struct MyError {
message: String,
}
};
let output = expand(input).unwrap().to_string();
insta::assert_snapshot!(output);
}
#[test]
fn test_derive_struct_simple() {
let input = quote! {
#[oopsie(vis(pub(crate)))]
#[oopsie(suffix)]
#[oopsie(path = "crate")]
#[oopsie("Test error: {message}")]
pub struct MyError {
message: String,
}
};
let output = expand(input).unwrap().to_string();
insta::assert_snapshot!(output);
}
#[cfg(not(feature = "unstable-error-generic-member-access"))]
#[test]
fn test_derive_struct_with_provide() {
let input = quote! {
#[oopsie(vis(pub(crate)))]
#[oopsie(suffix)]
#[oopsie(path = "crate")]
#[oopsie("Test error: {message}")]
#[oopsie(provide(ref, crate::Backtrace => __oopsie_backtrace.as_ref()))]
pub struct MyError {
message: String,
#[oopsie(capture)]
__oopsie_backtrace: ::std::boxed::Box<crate::Backtrace>,
}
};
let output = expand(input).unwrap().to_string();
insta::assert_snapshot!(output);
}
#[cfg(not(feature = "unstable-error-generic-member-access"))]
#[test]
fn test_derive_generic_enum() {
let input = quote! {
#[oopsie(module(false))]
pub enum GenericError<T, E>
where
T: ::core::fmt::Debug,
E: ::core::error::Error,
{
#[oopsie("payload was {payload:?}")]
Wrap { payload: T },
#[oopsie(display("inner failed"), transparent)]
Inner { source: E },
}
};
let output = expand(input).unwrap().to_string();
insta::assert_snapshot!(output);
}
#[test]
fn module_vis_mirrors_error_type_vis() {
let out = expand(quote! {
pub(crate) enum InternalError { #[oopsie("x")] X { f: String } }
})
.unwrap()
.to_string();
assert!(out.contains("pub (crate) mod internal_oopsies"), "{out}");
let out = expand(quote! {
enum PrivError { #[oopsie("x")] X { f: String } }
})
.unwrap()
.to_string();
assert!(
out.contains("mod priv_oopsies") && !out.contains("pub mod priv_oopsies"),
"{out}"
);
}
#[test]
fn generated_public_items_are_documented() {
let out = expand(quote! {
pub enum AppError { #[oopsie("x")] Connect { host: String } }
})
.unwrap()
.to_string();
assert!(
out.contains("Auto-generated context selectors for `AppError`"),
"{out}"
);
assert!(
out.contains("Context selector for `AppError::Connect`"),
"{out}"
);
assert!(out.contains("Value for the `host` field"), "{out}");
}
#[test]
fn selector_collision_after_error_stripping_errors() {
let err = expand(quote! {
pub enum AppError {
#[oopsie("read failed")] Read,
#[oopsie("read failed (io)")] ReadError,
}
})
.unwrap_err();
assert!(
err.to_string().contains("both generate a selector named"),
"{err}"
);
}
#[test]
fn pub_enum_selector_defaults_to_pub() {
let out = expand(quote! {
#[oopsie(module(false))]
pub enum PubError { #[oopsie("x")] X { f: String } }
})
.unwrap()
.to_string();
assert!(out.contains("pub struct X"), "{out}");
}
#[cfg(not(feature = "unstable-error-generic-member-access"))]
#[test]
fn test_derive_enum_with_transparent() {
let input = quote! {
#[oopsie(module(transparent_wrapper_oopsies))]
#[oopsie(vis(pub(crate)))]
#[oopsie(path = "crate")]
pub enum TransparentWrapper {
#[oopsie(display("Inner error happened"), transparent)]
Inner { source: InnerError },
}
};
let output = expand(input).unwrap().to_string();
insta::assert_snapshot!(output);
}
}