1#![warn(missing_docs)]
2
3use proc_macro::TokenStream;
9use quote::quote;
10use syn::*;
11
12#[proc_macro_attribute]
36pub fn command(_args: TokenStream, input: TokenStream) -> TokenStream {
37 let input = parse_macro_input!(input as DeriveInput);
38
39 match input.data {
40 Data::Struct(ref data) => handle_struct(&input, data),
41 Data::Enum(ref data) => handle_enum(&input, data),
42 _ => {
43 let error: proc_macro2::TokenStream = parse_quote! {
44 compile_error!("#[Command] can only be used on structs and enums");
45 };
46 error.into()
47 }
48 }
49}
50
51#[proc_macro_attribute]
68pub fn error(_args: TokenStream, input: TokenStream) -> TokenStream {
69 let input = parse_macro_input!(input as DeriveInput);
70
71 match input.data {
72 Data::Struct(ref data) => handle_error_struct(&input, data),
73 Data::Enum(ref data) => handle_error_enum(&input, data),
74 _ => {
75 let error: proc_macro2::TokenStream = parse_quote! {
76 compile_error!("#[Error] can only be used on structs and enums");
77 };
78 error.into()
79 }
80 }
81}
82
83fn handle_struct(input: &DeriveInput, data: &DataStruct) -> TokenStream {
85 let name = &input.ident;
86 let generics = &input.generics;
87 let where_clause = &generics.where_clause;
88
89 let expanded = match &data.fields {
90 Fields::Named(fields) => {
91 let field_parsers = fields.named.iter().map(|field| {
92 let ident = field.ident.as_ref().unwrap();
93 let ty = &field.ty;
94 quote! {
95 #ident: <#ty as itools::Parse>::parse(args),
96 }
97 });
98 quote! {
99 #input
100
101 impl itools::Parse for #name #generics #where_clause {
102 fn parse(args: &mut Vec<String>) -> Self {
103 Self {
104 #(#field_parsers)*
105 }
106 }
107 }
108
109 impl #name #generics #where_clause {
110 pub fn builder() -> itools::Builder {
112 itools::Builder::new()
113 }
114 }
115 }
116 }
117 Fields::Unnamed(fields) => {
118 let field_parsers = fields.unnamed.iter().map(|field| {
119 let ty = &field.ty;
120 quote! {
121 <#ty as itools::Parse>::parse(args),
122 }
123 });
124 quote! {
125 #input
126
127 impl itools::Parse for #name #generics #where_clause {
128 fn parse(args: &mut Vec<String>) -> Self {
129 Self(
130 #(#field_parsers)*
131 )
132 }
133 }
134
135 impl #name #generics #where_clause {
136 pub fn builder() -> itools::Builder {
138 itools::Builder::new()
139 }
140 }
141 }
142 }
143 Fields::Unit => quote! {
144 #input
145
146 impl itools::Parse for #name #generics #where_clause {
147 fn parse(args: &mut Vec<String>) -> Self {
148 Self
149 }
150 }
151
152 impl #name #generics #where_clause {
153 pub fn builder() -> itools::Builder {
155 itools::Builder::new()
156 }
157 }
158 },
159 };
160
161 expanded.into()
162}
163
164fn handle_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream {
166 let name = &input.ident;
167 let generics = &input.generics;
168 let where_clause = &generics.where_clause;
169
170 let variants = data.variants.iter().map(|variant| {
171 let variant_ident = &variant.ident;
172 let variant_str = {
174 let s = variant_ident.to_string();
175 let mut result = String::new();
176 for (i, c) in s.chars().enumerate() {
177 if i > 0 && c.is_uppercase() {
178 result.push('-');
179 }
180 result.push(c.to_lowercase().next().unwrap());
181 }
182 result
183 };
184
185 match &variant.fields {
186 Fields::Named(fields) => {
187 let field_parsers = fields.named.iter().map(|field| {
188 let ident = field.ident.as_ref().unwrap();
189 let ty = &field.ty;
190 quote! {
191 #ident: <#ty as itools::Parse>::parse(args),
192 }
193 });
194 quote! {
195 #variant_str => {
196 Self::#variant_ident {
197 #(#field_parsers)*
198 }
199 }
200 }
201 }
202 Fields::Unnamed(fields) => {
203 let field_parsers = fields.unnamed.iter().map(|field| {
204 let ty = &field.ty;
205 quote! {
206 <#ty as itools::Parse>::parse(args),
207 }
208 });
209 quote! {
210 #variant_str => {
211 Self::#variant_ident(
212 #(#field_parsers)*
213 )
214 }
215 }
216 }
217 Fields::Unit => {
218 quote! {
219 #variant_str => Self::#variant_ident,
220 }
221 }
222 }
223 });
224
225 let expanded = quote! {
226 #input
227
228 impl itools::Parse for #name #generics #where_clause {
229 fn parse(args: &mut Vec<String>) -> Self {
230 if args.is_empty() {
231 panic!("Expected command");
232 }
233 let cmd = args.remove(0);
234 match cmd.as_str() {
235 #(#variants)*
236 _ => panic!("Unknown command: {}", cmd),
237 }
238 }
239 }
240
241 impl #name #generics #where_clause {
242 pub fn builder() -> itools::Builder {
244 itools::Builder::new()
245 }
246 }
247 };
248
249 expanded.into()
250}
251
252fn handle_error_struct(input: &DeriveInput, _data: &DataStruct) -> TokenStream {
254 let name = &input.ident;
255 let generics = &input.generics;
256 let where_clause = &generics.where_clause;
257
258 let error_message = get_error_message(&input.attrs);
259
260 let expanded = quote! {
261 #input
262
263 impl std::fmt::Debug for #name #generics #where_clause {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 std::fmt::Debug::fmt(self, f)
266 }
267 }
268
269 impl std::fmt::Display for #name #generics #where_clause {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 write!(f, "{}", self.localize())
272 }
273 }
274
275 impl std::error::Error for #name #generics #where_clause {}
276
277 impl itools_core::LocalizableError for #name #generics #where_clause {
278 fn localize(&self) -> String {
279 itools::t(#error_message)
280 }
281 }
282 };
283
284 expanded.into()
285}
286
287fn handle_error_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream {
289 let name = &input.ident;
290 let generics = &input.generics;
291 let where_clause = &generics.where_clause;
292
293 let variants = data.variants.iter().map(|variant| {
294 let variant_ident = &variant.ident;
295 let error_message = get_error_message(&variant.attrs);
296
297 match &variant.fields {
298 Fields::Named(fields) => {
299 let field_names = fields.named.iter().map(|field| {
300 let ident = field.ident.as_ref().unwrap();
301 quote! { #ident }
302 });
303 quote! {
304 Self::#variant_ident { #(#field_names),* } => itools::t(#error_message)
305 }
306 }
307 Fields::Unnamed(fields) => {
308 let field_names = (0..fields.unnamed.len()).map(|i| {
309 let i = syn::Index::from(i);
310 quote! { self.#i }
311 });
312 quote! {
313 Self::#variant_ident(#(#field_names),*) => itools::t(#error_message)
314 }
315 }
316 Fields::Unit => {
317 quote! {
318 Self::#variant_ident => itools::t(#error_message)
319 }
320 }
321 }
322 });
323
324 let expanded = quote! {
325 #input
326
327 impl std::fmt::Debug for #name #generics #where_clause {
328 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329 std::fmt::Debug::fmt(self, f)
330 }
331 }
332
333 impl std::fmt::Display for #name #generics #where_clause {
334 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335 write!(f, "{}", self.localize())
336 }
337 }
338
339 impl std::error::Error for #name #generics #where_clause {}
340
341 impl itools_core::LocalizableError for #name #generics #where_clause {
342 fn localize(&self) -> String {
343 match self {
344 #(#variants)*
345 }
346 }
347 }
348 };
349
350 expanded.into()
351}
352
353fn get_error_message(attrs: &[Attribute]) -> proc_macro2::TokenStream {
355 for attr in attrs {
356 if attr.path().is_ident("error") {
357 if let Ok(lit_str) = attr.parse_args::<syn::LitStr>() {
359 return quote! { #lit_str };
360 }
361 }
362 }
363 quote! { "unknown_error" }
364}
365
366#[proc_macro]
381pub fn t(input: TokenStream) -> TokenStream {
382 if let Ok(lit_str) = syn::parse::<syn::LitStr>(input.clone()) {
384 let key = lit_str.value();
385
386 if key.is_empty() {
388 let error: proc_macro2::TokenStream = parse_quote! {
389 compile_error!("t! macro expects a non-empty string literal as translation key");
390 };
391 return error.into();
392 }
393
394 let expanded = quote! {
395 itools_localization::t(#lit_str)
396 };
397
398 return expanded.into();
399 }
400
401 if let Ok(expr_tuple) = syn::parse::<syn::ExprTuple>(input) {
403 if expr_tuple.elems.len() != 2 {
404 let error: proc_macro2::TokenStream = parse_quote! {
405 compile_error!("t! macro expects either one argument (key) or two arguments (key, args)");
406 };
407 return error.into();
408 }
409
410 let key = &expr_tuple.elems[0];
411 let args = &expr_tuple.elems[1];
412
413 let expanded = quote! {
414 itools_localization::t_with_args(#key, #args)
415 };
416
417 return expanded.into();
418 }
419
420 let error: proc_macro2::TokenStream = parse_quote! {
422 compile_error!("t! macro expects either a string literal or a tuple of (string literal, args)");
423 };
424 error.into()
425}