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#[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 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#[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 let ast = parse_macro_input!(input as DeriveInput);
100 let ident_name = ast.ident;
101
102 #[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 #[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 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 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 let variants = variants.iter().map(|v| {
139 let ident = &v.ident;
140 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 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 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 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 list_vars = tup.0.join(", ");
207 let list_calls = tup.1.join(", ");
209 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 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 let kind = opts.kind.unwrap_or_else(|| ident.to_string().to_case(Case::Snake));
236 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 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