error_doc_impl/
lib.rs

1use proc_macro2::TokenStream;
2use quote::quote;
3
4/// This macro derives `thiserror::Error` and [`Debug`] traits, and automatically generates missing documents for error variants from error messages.
5///
6/// # Example
7///
8/// ```
9/// #[error_doc::errors]
10/// pub enum SomeError {
11///     #[error("failed to open config file")]
12///     OpenFile(#[from] std::io::Error),
13///     #[error(transparent)]
14///     #[doc = "Database error"]
15///     Database(#[from] sqlx::Error),
16///     #[error("unexpected value: `{0}`")]
17///     #[doc = "Unexpected value is provided"]
18///     UnexpectedValue(u16),
19///     #[error("some other error")]
20///     Other,
21/// }
22/// ```
23///
24/// generates
25///
26/// ```
27/// #[derive(thiserror::Error, Debug)]
28/// pub enum SomeError {
29///     #[error("failed to open config file")]
30///     #[doc = "Failed to open config file"]
31///     OpenFile(#[from] std::io::Error),
32///     #[error(transparent)]
33///     #[doc = "Database error"]
34///     Database(#[from] sqlx::Error),
35///     #[error("unexpected value: `{0}`")]
36///     #[doc = "Unexpected value is provided"]
37///     UnexpectedValue(u16),
38///     #[error("some other error")]
39///     #[doc = "Some other error"]
40///     Other,
41/// }
42/// ```
43#[proc_macro_attribute]
44pub fn errors(
45    _attr: proc_macro::TokenStream,
46    item: proc_macro::TokenStream,
47) -> proc_macro::TokenStream {
48    let item: TokenStream = match error_doc_impl(item.into()) {
49        Ok(token_stream) => token_stream.into(),
50        Err(error) => return error.to_compile_error().into(),
51    };
52
53    quote! {
54        #[derive(::core::fmt::Debug, ::error_doc::thiserror::Error)]
55        #item
56    }
57    .into()
58}
59
60/// This macro automatically generates missing documents for error variants from error messages.
61///
62/// # Example
63///
64/// ```rust
65/// #[error_doc::error_doc]
66/// #[derive(thiserror::Error, Debug)]
67/// pub enum SomeError {
68///     #[error("failed to open config file")]
69///     OpenFile(#[from] std::io::Error),
70///     #[error(transparent)]
71///     #[doc = "Database error"]
72///     Database(#[from] sqlx::Error),
73///     #[error("unexpected value: `{0}`")]
74///     #[doc = "Unexpected value is provided"]
75///     UnexpectedValue(u16),
76///     #[error("some other error")]
77///     Other,
78/// }
79/// ```
80///
81/// generates
82///
83/// ```rust
84/// #[error_doc::error_doc]
85/// #[derive(thiserror::Error, Debug)]
86/// pub enum SomeError {
87///     #[error("failed to open config file")]
88///     #[doc = "Failed to open config file"]
89///     OpenFile(#[from] std::io::Error),
90///     #[error(transparent)]
91///     #[doc = "Database error"]
92///     Database(#[from] sqlx::Error),
93///     #[error("unexpected value: `{0}`")]
94///     #[doc = "Unexpected value is provided"]
95///     UnexpectedValue(u16),
96///     #[error("some other error")]
97///     #[doc = "Some other error"]
98///     Other,
99/// }
100/// ```
101#[proc_macro_attribute]
102pub fn error_doc(
103    _attr: proc_macro::TokenStream,
104    item: proc_macro::TokenStream,
105) -> proc_macro::TokenStream {
106    match error_doc_impl(item.into()) {
107        Ok(token_stream) => token_stream.into(),
108        Err(error) => error.to_compile_error().into(),
109    }
110}
111
112fn error_doc_impl(item: TokenStream) -> syn::Result<TokenStream> {
113    let mut item: syn::ItemEnum = syn::parse2(item)?;
114
115    item.variants = item
116        .variants
117        .into_iter()
118        .map(|mut variant| {
119            // check if doc attribute is alredy there
120            if variant.attrs.iter().any(|attr| attr.path().is_ident("doc")) {
121                return variant;
122            }
123
124            let msg = variant.attrs.iter().find_map(|attr| {
125                if !attr.path().is_ident("error") {
126                    return None;
127                }
128                let lit: syn::LitStr = attr.parse_args().ok()?;
129                let mut msg = lit.value();
130
131                // capitalize the first letter
132                if let Some(first_letter) = msg.get_mut(..1) {
133                    first_letter.make_ascii_uppercase();
134                }
135
136                Some(msg)
137            });
138
139            // add #[doc] attribute
140            if let Some(msg) = msg {
141                variant.attrs.push(syn::parse_quote! { #[doc = #msg] });
142            }
143
144            variant
145        })
146        .collect();
147
148    Ok(quote! { #item })
149}