oxiderr_derive/
lib.rs

1//! [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue)](./LICENSE)
2//! [![oxiderr-derive on crates.io](https://img.shields.io/crates/v/oxiderr-derive)](https://crates.io/crates/oxiderr-derive)
3//! [![oxiderr-derive on docs.rs](https://docs.rs/oxiderr-derive/badge.svg)](https://docs.rs/oxiderr-derive)
4//! [![Source Code Repository](https://img.shields.io/badge/Code-On%20GitHub-blue?logo=GitHub)](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}