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}