Skip to main content

resterror_derive/
lib.rs

1use darling::FromVariant;
2use std::{path::PathBuf, collections::HashMap};
3
4use syn::{parse_macro_input, DeriveInput};
5
6use proc_macro2::TokenTree;
7use proc_macro::TokenStream;
8
9
10#[cfg(feature = "json")]
11mod json;
12#[cfg(feature = "json")]
13use crate::json::get_json_error_messages;
14
15#[cfg(feature = "po")]
16use crate::po::get_po_error_messages;
17#[cfg(feature = "po")]
18mod po;
19
20/// 
21#[derive(FromVariant, Default)]
22#[darling(default, attributes(error))]
23struct Opts {
24    code: Option<u16>,
25    msg_id: Option<String>,
26    kind: Option<String>,
27    status: Option<String>,
28}
29
30
31#[cfg(any(feature = "po", feature = "json"))]
32fn get_dir_attr(attrs: &Vec<syn::Attribute>, attr_name: &str) -> Option<PathBuf> {
33    let mut directory_tokens = attrs.iter().find(|attr| attr.path.is_ident(attr_name)).expect("Couldn't get the attribute").tokens.clone().into_iter();
34    match directory_tokens.next() {
35        Some(TokenTree::Punct(punct)) if punct.as_char() == '=' => (),
36        _ => panic!("Expected leading '=' in {attr_name} attribute"),
37    }
38    let directory = match directory_tokens.next() {
39        Some(TokenTree::Literal(value)) => value.to_string(),
40        _ => panic!("Expected literal in {attr_name} attribute")
41    };
42    let directory = directory.trim_matches('"');
43    
44    // Check if the directory exists and contains at least one .po file.
45    let directory = std::path::PathBuf::from(directory);
46    if !directory.exists() {
47        panic!("The {attr_name} directory does not exist");
48    }
49    if !directory.is_dir() {
50        panic!("The path {attr_name} is not a directory");
51    }
52    
53    let mut files = std::fs::read_dir(&directory).expect("Couldn't read the directory");
54    if files.next().is_none() {
55        panic!("The path {attr_name} does not contain any files");
56    }
57
58    Some(directory)
59}
60
61/// This derive macro is used to convert an enum into an ApiError.  
62/// You can use it by adding the ```#[derive(AsApiError)]``` attribute to your enum.  
63/// You have to specify the error messages in a json file or in a po directory.  
64/// The path to the json file or the po directory is specified by adding the ```#[json_file = "path/to/messages.json"]``` or ```#[po_directory = "path/to/po"]``` attribute to the enum.  
65/// By default, the message id is the name of the variant in ```snake case```.  
66/// You can change the message id by adding the ```#[error(msg_id = "your_message_id")]``` attribute to the variant.  
67/// You can also add a custom code to the error by adding the ```#[error(code = 400)]``` attribute to the variant.  
68/// You can also add a status to the error by adding the ```#[error(status = "your_status")]``` attribute to the variant.  
69/// The following status are available and return the corresponding status code: 
70/// ``` rust
71/// match error_kind {
72///     "BadRequest" => 400,
73///     "Unauthorized" => 401,
74///     "Forbidden" => 403,
75///     "NotFound" => 404,
76///     "MethodNotAllowed" => 405,
77///     "Conflict" => 409,
78///     "Gone" => 410,
79///     "PayloadTooLarge" => 413,
80///     "UnsupportedMediaType" => 415,
81///     "UnprocessableEntity" => 422,
82///     "TooManyRequests" => 429,
83///     "InternalServerError" => 500,
84///     "NotImplemented" => 501,
85///     "BadGateway" => 502,
86///     "ServiceUnavailable" => 503,
87///     "GatewayTimeout" => 504,
88///     _ => unreachable!(),
89/// }
90/// ```
91#[cfg_attr(all(feature = "json", feature="po"), proc_macro_derive(AsApiError, attributes(error, msg_path)))]
92#[cfg_attr(all(feature = "json", not(feature = "po")), proc_macro_derive(AsApiError, attributes(json_file, error)))]
93#[cfg_attr(all(feature = "po", not(feature = "json")), proc_macro_derive(AsApiError, attributes(po_directory, error)))]
94#[cfg_attr(not(any(feature = "po", feature = "json")), proc_macro_derive(AsApiError, attributes(error)))]
95pub fn derive(input: TokenStream) -> TokenStream {
96    use convert_case::{Case, Casing};
97
98    // Parse the input tokens into a syntax tree
99    let ast = parse_macro_input!(input as DeriveInput); 
100    let ident_name = ast.ident;
101
102    // Get the path to the po file
103    #[cfg(all(feature = "po", not(feature = "json")))]
104    let po_directory = get_dir_attr(&ast.attrs, "po_directory").expect("No po_directory attribute found");
105    
106    // Get the path to the json file
107    #[cfg(all(feature = "json", not(feature = "po")))]
108    let json_file = get_dir_attr(&ast.attrs, "json_file").expect("No json_file attribute found");
109
110    #[cfg(all(feature = "json", feature = "po"))]
111    let messages_catalog = {
112        let path = get_dir_attr(&ast.attrs, "msg_path").expect("No path attribute found");
113        // Check if the path is a directory
114        if path.is_dir() {
115            get_po_error_messages(path)
116        } else {
117            get_json_error_messages(path)
118        }
119    };
120    
121    #[cfg(all(feature = "json", not(feature = "po")))]
122    let messages_catalog = get_json_error_messages(json_file);
123
124    #[cfg(all(feature = "po", not(feature = "json")))]
125    let messages_catalog = get_po_error_messages(po_directory);
126
127    #[cfg(not(any(feature = "json", feature = "po")))]
128    let messages_catalog: HashMap<String, HashMap<String, String>> = HashMap::new();
129
130    // Get the variants
131    let enum_data = match ast.data {
132        syn::Data::Enum(data) => data,
133        _ => panic!("ApiError can only be derived for enums"),
134    };
135    let variants = enum_data.variants;
136
137    // Generate the variant's code 
138    let variants = variants.iter().map(|v| {
139        let ident = &v.ident;
140        // Get the tuple if it exists
141        let tuple = match &v.fields {
142            syn::Fields::Unnamed(u) => Some(u),
143            _ => None,
144        };
145        let struc = if let syn::Fields::Named(n) = &v.fields {
146            Some(n)
147        } else {
148            None
149        };
150            
151        let opts = Opts::from_variant(&v).expect("Couldn't get the options for the variant");
152        let code = if let Some(code) = opts.code {
153            code
154        } else {
155            if let Some(ref error_kind) = opts.status {
156                match error_kind.as_str() {
157                    "BadRequest" => 400,
158                    "Unauthorized" => 401,
159                    "Forbidden" => 403,
160                    "NotFound" => 404,
161                    "MethodNotAllowed" => 405,
162                    "Conflict" => 409,
163                    "Gone" => 410,
164                    "PayloadTooLarge" => 413,
165                    "UnsupportedMediaType" => 415,
166                    "UnprocessableEntity" => 422,
167                    "TooManyRequests" => 429,
168                    "InternalServerError" => 500,
169                    "NotImplemented" => 501,
170                    "BadGateway" => 502,
171                    "ServiceUnavailable" => 503,
172                    "GatewayTimeout" => 504,
173                    _ => panic!("Invalid kind for variant {}: {}", ident, error_kind),
174                }
175            } else {
176                500
177            }
178        };
179
180        #[cfg(feature = "actix")]
181        {
182            use actix_web::http::StatusCode;
183            if let Err(e) = StatusCode::from_u16(code) {
184                panic!("Invalid status code for variant {}: {}", ident, e);
185            }
186        }
187        // Get the messages for the variant
188        let msg_id = opts.msg_id.unwrap_or_else(|| ident.to_string().to_case(Case::Snake));
189        let mut messages = String::new();
190        let mut list_vars = String::new();
191        
192        // Add the default messages for the variant in a hashmap
193        for (k, v) in messages_catalog.get(&msg_id).expect(&format!("Couldn't get the messages for the variant \"{msg_id}\"")) {
194            list_vars = String::new();
195            let mut v = v.to_string();
196            if let Some(tuple) = tuple {
197                // Get the variables names and their calls
198                let tup: (Vec<String>, Vec<String>)= tuple.unnamed.iter().enumerate().map(|(i, field)| {
199                    if field.ty == syn::parse_str("Translation").unwrap() {
200                        (format!("a{i}"), format!("a{i}.get(\"{k}\")"))
201                    } else {
202                        (format!("a{i}"), format!("a{i}"))
203                    }
204                }).unzip();
205                // Get the variables names
206                list_vars = tup.0.join(", ");
207                // Get the variables calls
208                let list_calls = tup.1.join(", ");
209                // Count the number of "{}" in the message and compare it to the number of variables in the tuple
210                let nb = v.matches("{}").count();
211                if nb != tuple.unnamed.len() {
212                    panic!("The number of variables in the message for the variant \"{msg_id}\" must be equal to the number of variables in the tuple");
213                }
214                messages.push_str(
215                    &format!("(String::from(\"{k}\"), format!(\"{v}\", {list_calls})),")
216                );
217            } else if let Some(struc) = struc {
218                let vars = v.split("{").skip(1).map(|s| s.split("}").next().unwrap().to_string()).collect::<Vec<String>>();
219                let vars = vars.as_slice();
220                // Replace all the variabels in the message
221                for var in vars {
222                    v = v.replace(&format!("{{{}}}", var.clone()), "{}").to_string();
223                }
224                list_vars = struc.named.iter().map(|f| f.ident.as_ref().unwrap().to_string()).collect::<Vec<String>>().join(", ");
225                messages.push_str(
226                    &format!("(String::from(\"{k}\"), format!(\"{v}\", {list_vars})),")
227                );
228            } else {
229                messages.push_str(
230                    &format!("(String::from(\"{k}\"), String::from(\"{v}\")),")
231                );
232            }
233        }
234        // Get the kind of the variant
235        let kind = opts.kind.unwrap_or_else(|| ident.to_string().to_case(Case::Snake));
236        // Add the tuple syntax if it exists
237        if list_vars.len() > 0 {
238            if struc.is_some() {
239                list_vars = format!("{{ {} }}", list_vars);
240            } else {
241                list_vars = format!("( {} )", list_vars);
242            }
243        }
244        format!("
245            {ident_name}::{ident} {list_vars} => {{
246                ApiError::new(
247                    {code}, 
248                    \"{kind}\",
249                    HashMap::from([{messages}]), 
250                )
251            }},
252        ")
253    });
254
255    // Implement the ApiError trait
256    let mut code = String::new();
257    code.push_str(&format!("impl AsApiError for {ident_name} {{\n"));
258    code.push_str(" fn as_api_error(&self) -> ApiError {\n");
259    code.push_str("     match &self {\n");
260    for v in variants {
261        code.push_str(&v.to_string());
262    }
263    code.push_str("\n    }\n");
264    code.push_str("   }\n");
265    code.push_str("}\n");
266
267    #[cfg(feature = "verbose")]
268    println!("code : {code}");
269
270    code.parse().expect("Couldn't parse the code")
271}
272
273