oxiderr_derive/lib.rs
1//! [](./LICENSE)
2//! [](https://crates.io/crates/oxiderr-derive)
3//! [](https://docs.rs/oxiderr-derive)
4//! [](https://github.com/cdumay/oxiderr-derive)
5//!
6//! The `oxiderr-derive` crate provides procedural macros to simplify the creation of custom error types in Rust. By leveraging these macros,
7//! developers can efficiently define error structures that integrate seamlessly with the `oxiderr` error management ecosystem.
8//!
9//! # Overview
10//!
11//! Error handling in Rust often involves creating complex structs to represent various error kinds and implementing traits to provide context and
12//! conversions. The `oxiderr-derive` crate automates this process by offering macros that generate the necessary boilerplate code, allowing for
13//! more readable and maintainable error definitions.
14//!
15//! # Features
16//!
17//! * **Macros**: Automatically generate implementations for custom error types.
18//! * **Integration with oxiderr**: Designed to work cohesively with the `oxiderr` crate, ensuring consistent error handling patterns.
19//!
20//! # Usage
21//!
22//! To utilize `oxiderr-derive` in your project, follow these steps:
23//!
24//! 1. **Add Dependencies**: Include `oxiderr` with the feature `derive` in your `Cargo.toml`:
25//!
26//! ```toml
27//! [dependencies]
28//! oxiderr = { version = "0.1", features = ["derive"] }
29//! ```
30//!
31//! 2. **Define Error**: Use the provided derive macros to define your error and error kind structs:
32//!
33//! ```rust
34//! use oxiderr::{define_errors, define_kinds, AsError};
35//!
36//! define_kinds! {
37//! UnknownError = ("Err-00001", 500, "Unexpected error"),
38//! IoError = ("Err-00001", 400, "IO error")
39//! }
40//! define_errors! {
41//! Unexpected = UnknownError,
42//! FileRead = IoError,
43//! FileNotExists = IoError
44//! }
45//! ```
46//! In this example:
47//!
48//! * define_kinds create `oxiderr::ErrorKind` structs representing different categories of errors.
49//! * define_errors create `oxiderr::Error` struct that contains an ErrorKind and metadata.
50//!
51//! 3. **Implementing Error Handling**: With the above definitions, you can now handle errors in your application as follows:
52//!
53//! ```rust
54//! use std::fs::File;
55//! use std::io::Read;
56//!
57//! fn try_open_file(path: &str) -> oxiderr::Result<String> {
58//! let mut file = File::open(path).map_err(|err| FileNotExists::new().set_message(err.to_string()))?;
59//! let mut content = String::new();
60//! file.read_to_string(&mut content).map_err(|err| FileRead::new().set_message(err.to_string()))?;
61//! Ok(content)
62//! }
63//!
64//! fn main() {
65//! let path = "example.txt";
66//!
67//! match try_open_file(path) {
68//! Ok(content) => println!("File content:\n{}", content),
69//! Err(e) => eprintln!("{}", e),
70//! }
71//! }
72//! ```
73//! This will output:
74//!
75//! ```text
76//! [Err-00001] Client::IoError::FileNotExists (400) - No such file or directory (os error 2)
77//! ```
78extern crate proc_macro;
79
80use proc_macro::TokenStream;
81use quote::quote;
82use syn::parse::{Parse, ParseStream};
83use syn::punctuated::Punctuated;
84use syn::token::Comma;
85use syn::{parenthesized, parse_macro_input, Ident, LitInt, LitStr, Token, Type};
86
87struct ErrorKindArgs {
88 const_name: Ident,
89 _eq: Token![=],
90 _parens: syn::token::Paren,
91 message: LitStr,
92 _comma1: Token![,],
93 code: LitInt,
94 _comma2: Token![,],
95 description: LitStr,
96}
97
98impl Parse for ErrorKindArgs {
99 fn parse(input: ParseStream) -> syn::Result<Self> {
100 let const_name: Ident = input.parse()?;
101 let _eq: Token![=] = input.parse()?;
102
103 let content;
104 let _parens = parenthesized!(content in input);
105
106 let message: LitStr = content.parse()?;
107 let _comma1: Token![,] = content.parse()?;
108 let code: LitInt = content.parse()?;
109 let _comma2: Token![,] = content.parse()?;
110 let description: LitStr = content.parse()?;
111
112 Ok(ErrorKindArgs {
113 const_name,
114 _eq,
115 _parens,
116 message,
117 _comma1,
118 code,
119 _comma2,
120 description,
121 })
122 }
123}
124
125struct ErrorKindArgsList {
126 items: Punctuated<ErrorKindArgs, Comma>,
127}
128
129impl Parse for ErrorKindArgsList {
130 fn parse(input: ParseStream) -> syn::Result<Self> {
131 Ok(ErrorKindArgsList {
132 items: Punctuated::parse_terminated(input)?,
133 })
134 }
135}
136
137/// The `define_kinds` macro is a procedural macro that generates constants of type `oxiderr::ErrorKind`. This macro simplifies the definition
138/// of structured error kinds by allowing developers to declare them using a concise syntax. It takes a list of error definitions and expands
139/// them into properly structured `oxiderr::ErrorKind` constants.
140///
141/// # Usage Example
142///
143/// ## Macro Input
144///
145/// ```rust
146/// define_kinds! {
147/// FileNotFound = ("File not found", 404, "The requested file could not be located"),
148/// PermissionDenied = ("Permission denied", 403, "The user lacks the necessary permissions")
149/// }
150/// ```
151/// ## Macro Expansion (Generated Code)
152///
153/// ```rust
154/// #[allow(non_upper_case_globals)]
155/// pub const FileNotFound: oxiderr::ErrorKind = oxiderr::ErrorKind(
156/// "FileNotFound",
157/// "File not found",
158/// 404,
159/// "The requested file could not be located"
160/// );
161///
162/// #[allow(non_upper_case_globals)]
163/// pub const PermissionDenied: oxiderr::ErrorKind = oxiderr::ErrorKind(
164/// "PermissionDenied",
165/// "Permission denied",
166/// 403,
167/// "The user lacks the necessary permissions"
168/// );
169/// ```
170#[proc_macro]
171pub fn define_kinds(input: TokenStream) -> TokenStream {
172 let args_list = parse_macro_input!(input as ErrorKindArgsList);
173
174 let constants = args_list.items.iter().map(|args| {
175 let const_name = &args.const_name;
176 let message = &args.message;
177 let code = &args.code;
178 let description = &args.description;
179
180 quote! {
181 #[allow(non_upper_case_globals)]
182 pub const #const_name: oxiderr::ErrorKind = oxiderr::ErrorKind(stringify!(#const_name), #message, #code, #description);
183 }
184 });
185
186 TokenStream::from(quote! {
187 #(#constants)*
188 })
189}
190
191struct ErrorDefinition {
192 name: Ident,
193 kind: Type,
194}
195
196struct ErrorDefinitions {
197 definitions: Vec<ErrorDefinition>,
198}
199
200impl Parse for ErrorDefinitions {
201 fn parse(input: ParseStream) -> syn::Result<Self> {
202 let mut definitions = Vec::new();
203
204 while !input.is_empty() {
205 let name: Ident = input.parse()?;
206 input.parse::<Token![=]>()?;
207 let kind: Type = input.parse()?;
208 if input.peek(Token![,]) {
209 input.parse::<Token![,]>()?;
210 }
211 definitions.push(ErrorDefinition { name, kind });
212 }
213
214 Ok(ErrorDefinitions { definitions })
215 }
216}
217
218/// The `define_errors` macro is a procedural macro that generates structured error types implementing `oxiderr::AsError`. This macro simplifies
219/// error handling by defining error structures with relevant metadata, serialization, and error conversion logic.
220///
221/// Each generated struct:
222///
223/// * Implements `oxiderr::AsError` for interoperability with `oxiderr::ErrorKind`.
224/// * Provides methods for setting error messages and details.
225/// * Supports conversion from `oxiderr::Error`.
226
227/// # Usage Example
228///
229/// ## Macro Input
230///
231/// ```rust
232/// define_errors! {
233/// NotFoundError = FileNotFound,
234/// UnauthorizedError = PermissionDenied
235/// }
236/// ```
237/// ## Macro Expansion (Generated Code for NotFoundError)
238///
239/// ```rust
240/// #[derive(Debug, Clone)]
241/// pub struct NotFoundError {
242/// class: String,
243/// message: String,
244/// details: Option<std::collections::BTreeMap<String, serde_value::Value>>,
245/// }
246///
247/// impl NotFoundError {
248/// pub const kind: oxiderr::ErrorKind = FileNotFound;
249///
250/// pub fn new() -> Self {
251/// Self {
252/// class: format!("{}::{}::{}", Self::kind.side(), Self::kind.name(), "NotFoundError"),
253/// message: Self::kind.description().into(),
254/// details: None,
255/// }
256/// }
257///
258/// pub fn set_message(mut self, message: String) -> Self {
259/// self.message = message;
260/// self
261/// }
262///
263/// pub fn set_details(mut self, details: std::collections::BTreeMap<String, serde_value::Value>) -> Self {
264/// self.details = Some(details);
265/// self
266/// }
267///
268/// pub fn convert(error: oxiderr::Error) -> Self {
269/// let mut err_clone = error.clone();
270/// let mut details = error.details.unwrap_or_default();
271/// err_clone.details = None;
272/// details.insert("origin".to_string(), serde_value::to_value(err_clone).unwrap());
273///
274/// Self {
275/// class: format!("{}::{}::{}", Self::kind.side(), Self::kind.name(), "NotFoundError"),
276/// message: Self::kind.description().into(),
277/// details: Some(details),
278/// }
279/// }
280/// }
281///
282/// impl oxiderr::AsError for NotFoundError {
283/// fn kind() -> oxiderr::ErrorKind {
284/// Self::kind
285/// }
286/// fn class(&self) -> String {
287/// self.class.clone()
288/// }
289/// fn message(&self) -> String {
290/// self.message.clone()
291/// }
292/// fn details(&self) -> Option<std::collections::BTreeMap<String, serde_value::Value>> {
293/// self.details.clone()
294/// }
295/// }
296///
297/// impl std::error::Error for NotFoundError {}
298///
299/// impl std::fmt::Display for NotFoundError {
300/// fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
301/// write!(f, "[{}] {} ({}): {}", Self::kind.message_id(), "NotFoundError", Self::kind.code(), self.message())
302/// }
303/// }
304/// ```
305#[proc_macro]
306pub fn define_errors(input: TokenStream) -> TokenStream {
307 let definitions = parse_macro_input!(input as ErrorDefinitions);
308
309 let generated_structs = definitions.definitions.iter().map(|definition| {
310 let name = &definition.name;
311 let kind = &definition.kind;
312
313 quote! {
314 #[derive(Debug, Clone)]
315 pub struct #name {
316 class: String,
317 message: String,
318 details: Option<std::collections::BTreeMap<String, serde_value::Value>>,
319 }
320
321 impl #name {
322 pub const kind: oxiderr::ErrorKind = #kind;
323 pub fn new() -> Self {
324 Self {
325 class: format!("{}::{}::{}", Self::kind.side(), Self::kind.name(), stringify!(#name)),
326 message: Self::kind.description().into(),
327 details: None,
328 }
329 }
330 pub fn set_message(mut self, message: String) -> Self {
331 self.message = message;
332 self
333 }
334 pub fn set_details(mut self, details: std::collections::BTreeMap<String, serde_value::Value>) -> Self {
335 self.details = Some(details);
336 self
337 }
338 pub fn convert(error: oxiderr::Error) -> Self {
339 let mut err_clone = error.clone();
340 let mut details = error.details.unwrap_or_default();
341 err_clone.details = None;
342 details.insert("origin".to_string(), serde_value::to_value(err_clone).unwrap());
343 Self {
344 class: format!("{}::{}::{}", Self::kind.side(), Self::kind.name(), stringify!(#name)),
345 message: Self::kind.description().into(),
346 details: Some(details),
347 }
348 }
349 }
350 impl oxiderr::AsError for #name {
351 fn kind()-> oxiderr::ErrorKind {
352 Self::kind
353 }
354 fn class(&self) -> String {
355 self.class.clone()
356 }
357 fn message(&self) -> String {
358 self.message.clone()
359 }
360 fn details(&self) -> Option<std::collections::BTreeMap<String, serde_value::Value>> {
361 self.details.clone()
362 }
363 }
364
365 impl std::error::Error for #name {}
366
367 impl std::fmt::Display for #name {
368 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
369 write!(f, "[{}] {} ({}): {}", Self::kind.message_id(), stringify!(#name), Self::kind.code(), self.message())
370 }
371 }
372 }
373 });
374
375 TokenStream::from(quote! {
376 #(#generated_structs)*
377 })
378}