1extern crate proc_macro;
2use proc_macro::TokenStream;
3use proc_macro2::{Span, TokenStream as TokenStream2};
4use quote::quote;
5use syn::ext::IdentExt;
6use syn::parse::{ParseStream, Parser};
7use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Lit, Token};
8
9#[proc_macro_derive(
77 ModError,
78 attributes(
79 error_prefix,
80 error_display,
81 error_kind,
82 error_caption,
83 error_retryable,
84 error_http_status,
85 error_exit_code,
86 error_fatal
87 )
88)]
89pub fn derive_mod_error(input: TokenStream) -> TokenStream {
90 let input = parse_macro_input!(input as DeriveInput);
92
93 TokenStream::from(expand(&input))
95}
96
97fn expand(input: &DeriveInput) -> TokenStream2 {
103 let result = match &input.data {
104 Data::Enum(_) => {
105 get_error_prefix(&input.attrs).and_then(|prefix| implement_for_enum(input, &prefix))
106 }
107 Data::Struct(_) => {
108 get_error_prefix(&input.attrs).map(|prefix| implement_for_struct(input, &prefix))
109 }
110 Data::Union(data) => Err(syn::Error::new_spanned(
111 data.union_token,
112 "ModError cannot be derived for unions; use an enum or a struct",
113 )),
114 };
115 result.unwrap_or_else(|error| error.to_compile_error())
116}
117
118fn rewrite_format(fmt: &str, fields: &[Option<String>]) -> Option<(String, Vec<usize>)> {
133 fn slot(args: &mut Vec<usize>, field: usize) -> usize {
134 match args.iter().position(|&f| f == field) {
135 Some(index) => index,
136 None => {
137 args.push(field);
138 args.len() - 1
139 }
140 }
141 }
142 fn named_field(fields: &[Option<String>], name: &str) -> Option<usize> {
143 let name = name.strip_prefix("r#").unwrap_or(name);
144 fields.iter().position(|f| f.as_deref() == Some(name))
145 }
146
147 let mut out = String::with_capacity(fmt.len() + 8);
148 let mut args: Vec<usize> = Vec::new();
149 let mut next_implicit = 0usize;
150 let mut chars = fmt.chars().peekable();
151
152 while let Some(c) = chars.next() {
153 match c {
154 '{' if chars.peek() == Some(&'{') => {
155 chars.next();
156 out.push_str("{{");
157 }
158 '{' => {
159 let mut inner = String::new();
160 loop {
161 match chars.next() {
162 Some('}') => break,
163 Some('{') | None => return None,
164 Some(ch) => inner.push(ch),
165 }
166 }
167 let (arg, spec) = match inner.find(':') {
168 Some(i) => (&inner[..i], Some(&inner[i + 1..])),
169 None => (inner.as_str(), None),
170 };
171
172 out.push('{');
173 if arg.is_empty() {
174 if next_implicit >= fields.len() {
175 return None;
176 }
177 out.push_str(&slot(&mut args, next_implicit).to_string());
178 next_implicit += 1;
179 } else if arg.bytes().all(|b| b.is_ascii_digit()) {
180 let index = arg.parse::<usize>().ok()?;
181 if index >= fields.len() {
182 return None;
183 }
184 out.push_str(&slot(&mut args, index).to_string());
185 } else if is_plain_ident(arg.strip_prefix("r#").unwrap_or(arg)) {
186 match named_field(fields, arg) {
187 Some(index) => out.push_str(&slot(&mut args, index).to_string()),
188 None => out.push_str(arg),
189 }
190 } else {
191 return None;
192 }
193
194 if let Some(spec) = spec {
195 if spec.contains('*') {
196 return None;
197 }
198 out.push(':');
199 let mut word = String::new();
201 for ch in spec.chars() {
202 if ch == '_' || ch.is_alphanumeric() {
203 word.push(ch);
204 continue;
205 }
206 if ch == '$' && !word.is_empty() {
207 let field = if word.bytes().all(|b| b.is_ascii_digit()) {
208 let index = word.parse::<usize>().ok()?;
209 if index >= fields.len() {
210 return None;
211 }
212 Some(index)
213 } else {
214 named_field(fields, &word)
215 };
216 match field {
217 Some(index) => out.push_str(&slot(&mut args, index).to_string()),
218 None => out.push_str(&word),
219 }
220 } else {
221 out.push_str(&word);
222 }
223 word.clear();
224 out.push(ch);
225 }
226 out.push_str(&word);
227 }
228 out.push('}');
229 }
230 '}' if chars.peek() == Some(&'}') => {
231 chars.next();
232 out.push_str("}}");
233 }
234 '}' => return None,
235 other => out.push(other),
236 }
237 }
238
239 Some((out, args))
240}
241
242fn is_plain_ident(s: &str) -> bool {
243 let mut chars = s.chars();
244 match chars.next() {
245 Some(first) if first == '_' || first.is_alphabetic() => {
246 chars.all(|c| c == '_' || c.is_alphanumeric())
247 }
248 _ => false,
249 }
250}
251
252enum AttrValue {
254 Flag,
256 Lit(Option<Token![-]>, Lit),
259}
260
261fn parse_attr_value(input: ParseStream) -> syn::Result<AttrValue> {
262 fn literal(input: ParseStream) -> syn::Result<AttrValue> {
263 let minus: Option<Token![-]> = input.parse()?;
264 let lit: Lit = input.parse()?;
265 Ok(AttrValue::Lit(minus, lit))
266 }
267
268 if input.is_empty() {
269 return Ok(AttrValue::Flag);
270 }
271 let value = if input.peek(syn::token::Paren) {
272 let content;
273 syn::parenthesized!(content in input);
274 let value = literal(&content)?;
275 if !content.is_empty() {
276 return Err(content.error("expected a single literal value"));
277 }
278 value
279 } else if input.peek(Token![=]) {
280 input.parse::<Token![=]>()?;
281 literal(input)?
282 } else {
283 return Err(input.error("expected `(value)` or `= value`"));
284 };
285 if !input.is_empty() {
286 return Err(input.error("unexpected tokens after the attribute value"));
287 }
288 Ok(value)
289}
290
291fn attr_value(attr: &syn::Attribute) -> syn::Result<AttrValue> {
292 parse_attr_value.parse2(attr.tokens.clone())
293}
294
295fn attr_name(attr: &syn::Attribute) -> String {
296 attr.path
297 .get_ident()
298 .map(ToString::to_string)
299 .unwrap_or_default()
300}
301
302fn parse_string_attribute(attr: &syn::Attribute) -> syn::Result<String> {
303 let message = || format!("`{}` expects a string literal", attr_name(attr));
304 match attr_value(attr)? {
305 AttrValue::Lit(None, Lit::Str(lit)) => Ok(lit.value()),
306 AttrValue::Lit(Some(minus), _) => Err(syn::Error::new_spanned(minus, message())),
307 AttrValue::Lit(None, lit) => Err(syn::Error::new_spanned(lit, message())),
308 AttrValue::Flag => Err(syn::Error::new_spanned(
309 attr,
310 format!("{}: `#[{}(\"...\")]`", message(), attr_name(attr)),
311 )),
312 }
313}
314
315fn parse_flag_attribute(attr: &syn::Attribute) -> syn::Result<bool> {
316 let message = || format!("`{}` expects `true` or `false`", attr_name(attr));
317 match attr_value(attr)? {
318 AttrValue::Flag => Ok(true),
319 AttrValue::Lit(None, Lit::Bool(lit)) => Ok(lit.value),
320 AttrValue::Lit(Some(minus), _) => Err(syn::Error::new_spanned(minus, message())),
321 AttrValue::Lit(None, lit) => Err(syn::Error::new_spanned(lit, message())),
322 }
323}
324
325fn parse_int_attribute<T>(attr: &syn::Attribute) -> syn::Result<T>
326where
327 T: TryFrom<i128>,
328{
329 let name = attr_name(attr);
330 let range_error = |tokens: TokenStream2| {
331 syn::Error::new_spanned(
332 tokens,
333 format!(
334 "`{name}` expects an integer in {}",
335 std::any::type_name::<T>()
336 ),
337 )
338 };
339 match attr_value(attr)? {
340 AttrValue::Lit(minus, Lit::Int(lit)) => {
341 let tokens = quote!(#minus #lit);
342 let magnitude: i128 = lit
343 .base10_parse()
344 .map_err(|_| range_error(tokens.clone()))?;
345 let value = if minus.is_some() {
346 -magnitude
347 } else {
348 magnitude
349 };
350 T::try_from(value).map_err(|_| range_error(tokens))
351 }
352 AttrValue::Lit(_, lit) => Err(syn::Error::new_spanned(
353 lit,
354 format!("`{name}` expects an integer literal"),
355 )),
356 AttrValue::Flag => Err(syn::Error::new_spanned(
357 attr,
358 format!("`{name}` expects an integer: `#[{name}(...)]`"),
359 )),
360 }
361}
362
363fn push_error(errors: &mut Option<syn::Error>, error: syn::Error) {
366 match errors {
367 Some(existing) => existing.combine(error),
368 None => *errors = Some(error),
369 }
370}
371
372fn get_error_prefix(attrs: &[syn::Attribute]) -> syn::Result<String> {
374 let mut prefix = String::new();
375 for attr in attrs {
376 if attr.path.is_ident("error_prefix") {
377 prefix = parse_string_attribute(attr)?;
378 }
379 }
380 Ok(prefix)
381}
382
383struct VariantAttrs {
385 display: String,
386 kind: String,
387 caption: String,
388 retryable: bool,
389 fatal: bool,
390 status_code: u16,
391 exit_code: i32,
392}
393
394fn variant_attrs(variant: &syn::Variant, error_prefix: &str) -> syn::Result<VariantAttrs> {
395 let name = variant.ident.to_string();
396 let mut attrs = VariantAttrs {
397 display: name.clone(),
398 kind: name,
399 caption: format!("{}: Error", error_prefix),
400 retryable: false,
401 fatal: false,
402 status_code: 500,
403 exit_code: 1,
404 };
405 let mut errors = None;
406 for attr in &variant.attrs {
407 let result = if attr.path.is_ident("error_display") {
408 parse_string_attribute(attr).map(|v| attrs.display = v)
409 } else if attr.path.is_ident("error_kind") {
410 parse_string_attribute(attr).map(|v| attrs.kind = v)
411 } else if attr.path.is_ident("error_caption") {
412 parse_string_attribute(attr).map(|v| attrs.caption = v)
413 } else if attr.path.is_ident("error_retryable") {
414 parse_flag_attribute(attr).map(|v| attrs.retryable = v)
415 } else if attr.path.is_ident("error_fatal") {
416 parse_flag_attribute(attr).map(|v| attrs.fatal = v)
417 } else if attr.path.is_ident("error_http_status") {
418 parse_int_attribute(attr).map(|v| attrs.status_code = v)
419 } else if attr.path.is_ident("error_exit_code") {
420 parse_int_attribute(attr).map(|v| attrs.exit_code = v)
421 } else {
422 Ok(())
423 };
424 if let Err(error) = result {
425 push_error(&mut errors, error);
426 }
427 }
428 match errors {
429 Some(error) => Err(error),
430 None => Ok(attrs),
431 }
432}
433
434fn display_arm(variant: &syn::Variant, display: &str, writer: &Ident) -> TokenStream2 {
437 let variant_name = &variant.ident;
438 let binding = |index: usize| Ident::new(&format!("__ef_field_{index}"), Span::mixed_site());
441
442 match &variant.fields {
443 Fields::Named(fields) => {
444 let idents: Vec<&Ident> = fields
445 .named
446 .iter()
447 .map(|f| f.ident.as_ref().expect("named field"))
448 .collect();
449 let names: Vec<Option<String>> = idents
450 .iter()
451 .map(|ident| Some(ident.unraw().to_string()))
452 .collect();
453 match rewrite_format(display, &names) {
454 Some((format_str, args)) => {
455 let pattern = args.iter().map(|&i| {
456 let field = idents[i];
457 let bind = binding(i);
458 quote! { #field: #bind }
459 });
460 let values = args.iter().map(|&i| binding(i));
461 quote! {
462 Self::#variant_name { #(#pattern,)* .. } =>
463 ::core::write!(#writer, #format_str #(, #values)*)
464 }
465 }
466 None => {
467 let binds: Vec<Ident> = (0..idents.len()).map(binding).collect();
468 quote! {
469 Self::#variant_name { #(#idents: #binds),* } =>
470 ::core::write!(#writer, #display, #(#idents = #binds),*)
471 }
472 }
473 }
474 }
475 Fields::Unnamed(fields) => {
476 let count = fields.unnamed.len();
477 let names: Vec<Option<String>> = vec![None; count];
478 match rewrite_format(display, &names) {
479 Some((format_str, args)) => {
480 let pattern = (0..count).map(|i| {
481 if args.contains(&i) {
482 let bind = binding(i);
483 quote! { #bind }
484 } else {
485 quote! { _ }
486 }
487 });
488 let values = args.iter().map(|&i| binding(i));
489 quote! {
490 Self::#variant_name(#(#pattern),*) =>
491 ::core::write!(#writer, #format_str #(, #values)*)
492 }
493 }
494 None => {
495 let binds: Vec<Ident> = (0..count).map(binding).collect();
496 quote! {
497 Self::#variant_name(#(#binds),*) =>
498 ::core::write!(#writer, #display #(, #binds)*)
499 }
500 }
501 }
502 }
503 Fields::Unit => {
504 let format_str = rewrite_format(display, &[])
505 .map(|(format_str, _)| format_str)
506 .unwrap_or_else(|| display.to_string());
507 quote! {
508 Self::#variant_name => ::core::write!(#writer, #format_str)
509 }
510 }
511 }
512}
513
514fn implement_for_enum(input: &DeriveInput, error_prefix: &str) -> syn::Result<TokenStream2> {
516 let name = &input.ident;
517 let data_enum = match &input.data {
518 Data::Enum(data) => data,
519 _ => unreachable!("implement_for_enum is only called for enums"),
520 };
521
522 let writer = Ident::new("__ef_writer", Span::mixed_site());
523 let formatter = Ident::new("__ef_formatter", Span::mixed_site());
524 let write_fn = Ident::new("__ef_write", Span::mixed_site());
525 let buffer = Ident::new("__ef_buffer", Span::mixed_site());
526
527 let mut kind_match_arms = Vec::new();
529 let mut caption_match_arms = Vec::new();
530 let mut display_match_arms = Vec::new();
531 let mut retryable_match_arms = Vec::new();
532 let mut fatal_match_arms = Vec::new();
533 let mut status_code_match_arms = Vec::new();
534 let mut exit_code_match_arms = Vec::new();
535 let mut errors = None;
536
537 for variant in &data_enum.variants {
538 let variant_name = &variant.ident;
539 let attrs = match variant_attrs(variant, error_prefix) {
540 Ok(attrs) => attrs,
541 Err(error) => {
542 push_error(&mut errors, error);
543 continue;
544 }
545 };
546 let VariantAttrs {
547 display,
548 kind,
549 caption,
550 retryable,
551 fatal,
552 status_code,
553 exit_code,
554 } = attrs;
555
556 let pattern = match &variant.fields {
557 Fields::Named(_) => quote! { Self::#variant_name { .. } },
558 Fields::Unnamed(_) => quote! { Self::#variant_name(..) },
559 Fields::Unit => quote! { Self::#variant_name },
560 };
561
562 kind_match_arms.push(quote! { #pattern => #kind });
563 caption_match_arms.push(quote! { #pattern => #caption });
564 retryable_match_arms.push(quote! { #pattern => #retryable });
565 fatal_match_arms.push(quote! { #pattern => #fatal });
566 status_code_match_arms.push(quote! { #pattern => #status_code });
567 exit_code_match_arms.push(quote! { #pattern => #exit_code });
568 display_match_arms.push(display_arm(variant, &display, &writer));
569 }
570
571 if let Some(error) = errors {
572 return Err(error);
573 }
574
575 Ok(quote! {
577 impl ::core::fmt::Display for #name {
578 fn fmt(&self, #formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
579 let #write_fn = |#writer: &mut dyn ::core::fmt::Write| -> ::core::fmt::Result {
580 match self {
581 #(#display_match_arms,)*
582 }
583 };
584 if #formatter.width().is_none() && #formatter.precision().is_none() {
585 #write_fn(#formatter)
586 } else {
587 let mut #buffer = ::std::string::String::new();
590 #write_fn(&mut #buffer)?;
591 #formatter.pad(&#buffer)
592 }
593 }
594 }
595
596 impl ::error_forge::error::ForgeError for #name {
597 fn kind(&self) -> &'static str {
598 match self {
599 #(#kind_match_arms,)*
600 }
601 }
602
603 fn caption(&self) -> &'static str {
604 match self {
605 #(#caption_match_arms,)*
606 }
607 }
608
609 fn is_retryable(&self) -> bool {
610 match self {
611 #(#retryable_match_arms,)*
612 }
613 }
614
615 fn is_fatal(&self) -> bool {
616 match self {
617 #(#fatal_match_arms,)*
618 }
619 }
620
621 fn status_code(&self) -> u16 {
622 match self {
623 #(#status_code_match_arms,)*
624 }
625 }
626
627 fn exit_code(&self) -> i32 {
628 match self {
629 #(#exit_code_match_arms,)*
630 }
631 }
632 }
633
634 impl ::std::error::Error for #name {
635 fn source(&self) -> ::core::option::Option<&(dyn ::std::error::Error + 'static)> {
636 ::core::option::Option::None
637 }
638 }
639 })
640}
641
642fn implement_for_struct(input: &DeriveInput, error_prefix: &str) -> TokenStream2 {
644 let name = &input.ident;
645 let name_str = name.to_string();
646 let formatter = Ident::new("__ef_formatter", Span::mixed_site());
647
648 quote! {
649 impl ::core::fmt::Display for #name {
650 fn fmt(&self, #formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
651 #formatter.pad(::core::concat!(#error_prefix, ": Error"))
652 }
653 }
654
655 impl ::error_forge::error::ForgeError for #name {
656 fn kind(&self) -> &'static str {
657 #name_str
658 }
659
660 fn caption(&self) -> &'static str {
661 ::core::concat!(#error_prefix, ": Error")
662 }
663 }
664
665 impl ::std::error::Error for #name {
666 fn source(&self) -> ::core::option::Option<&(dyn ::std::error::Error + 'static)> {
667 ::core::option::Option::None
668 }
669 }
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use super::{expand, rewrite_format};
676
677 fn named(list: &[&str]) -> Vec<Option<String>> {
678 list.iter().map(|s| Some(s.to_string())).collect()
679 }
680
681 fn tuple(count: usize) -> Vec<Option<String>> {
682 vec![None; count]
683 }
684
685 fn expand_str(input: syn::DeriveInput) -> String {
686 expand(&input).to_string()
687 }
688
689 #[test]
690 fn union_produces_compile_error_instead_of_panicking() {
691 let output = expand_str(syn::parse_quote! {
692 union Bits { a: u32, b: f32 }
693 });
694 assert!(output.contains("compile_error"), "{output}");
695 assert!(output.contains("cannot be derived for unions"), "{output}");
696 }
697
698 #[test]
699 fn rewrite_maps_positional_placeholders_to_arguments() {
700 assert_eq!(
701 rewrite_format("{} then {} and {0:?}", &tuple(3)),
702 Some(("{0} then {1} and {0:?}".to_string(), vec![0, 1]))
703 );
704 assert_eq!(
705 rewrite_format("only {2}", &tuple(3)),
706 Some(("only {0}".to_string(), vec![2]))
707 );
708 assert_eq!(
709 rewrite_format("NoFields", &tuple(3)),
710 Some(("NoFields".to_string(), Vec::new()))
711 );
712 }
713
714 #[test]
715 fn rewrite_keeps_escapes_and_non_field_names() {
716 let fields = named(&["host", "port"]);
717 assert_eq!(
718 rewrite_format("{{literal}} {port:>5} {CONST}", &fields),
719 Some(("{{literal}} {0:>5} {CONST}".to_string(), vec![1]))
720 );
721 assert_eq!(
724 rewrite_format("{}:{}", &fields),
725 Some(("{0}:{1}".to_string(), vec![0, 1]))
726 );
727 }
728
729 #[test]
730 fn rewrite_maps_raw_identifiers_and_count_arguments() {
731 let fields = named(&["type", "width"]);
732 assert_eq!(
733 rewrite_format("{type} {r#type} {type:>width$} {:.1$}", &fields),
734 Some(("{0} {0} {0:>1$} {0:.1$}".to_string(), vec![0, 1]))
735 );
736 assert_eq!(
737 rewrite_format("{type:>W$}", &fields),
738 Some(("{0:>W$}".to_string(), vec![0]))
739 );
740 }
741
742 #[test]
743 fn rewrite_declines_unsupported_strings() {
744 let fields = named(&["a", "b"]);
745 assert_eq!(rewrite_format("{:.*}", &fields), None);
746 assert_eq!(rewrite_format("{5}", &fields), None);
747 assert_eq!(rewrite_format("{} {} {}", &fields), None);
748 assert_eq!(rewrite_format("unclosed {", &fields), None);
749 assert_eq!(rewrite_format("stray }", &fields), None);
750 }
751
752 #[test]
753 fn malformed_attribute_values_are_compile_errors() {
754 let cases: [(syn::DeriveInput, &str); 6] = [
755 (
756 syn::parse_quote! { enum E { #[error_http_status("404")] A } },
757 "`error_http_status` expects an integer literal",
758 ),
759 (
760 syn::parse_quote! { enum E { #[error_exit_code(99999999999)] A } },
761 "`error_exit_code` expects an integer in i32",
762 ),
763 (
764 syn::parse_quote! { enum E { #[error_http_status(-1)] A } },
765 "`error_http_status` expects an integer in u16",
766 ),
767 (
768 syn::parse_quote! { enum E { #[error_retryable("yes")] A } },
769 "`error_retryable` expects `true` or `false`",
770 ),
771 (
772 syn::parse_quote! { enum E { #[error_display(42)] A } },
773 "`error_display` expects a string literal",
774 ),
775 (
776 syn::parse_quote! { #[error_prefix] enum E { A } },
777 "`error_prefix` expects a string literal",
778 ),
779 ];
780 for (input, message) in cases {
781 let output = expand_str(input);
782 assert!(output.contains("compile_error"), "{output}");
783 assert!(output.contains(message), "{message} not in {output}");
784 }
785 }
786
787 #[test]
788 fn explicit_flag_values_are_honoured() {
789 let output = expand_str(syn::parse_quote! {
790 enum E {
791 #[error_retryable(false)]
792 #[error_fatal = false]
793 A,
794 #[error_retryable = true]
795 #[error_fatal(true)]
796 B,
797 #[error_exit_code(-2)]
798 C,
799 }
800 });
801 assert!(!output.contains("compile_error"), "{output}");
802 let retryable = output
803 .split("fn is_retryable")
804 .nth(1)
805 .and_then(|s| s.split("fn is_fatal").next())
806 .unwrap();
807 assert!(retryable.contains("Self :: A => false"), "{retryable}");
808 assert!(retryable.contains("Self :: B => true"), "{retryable}");
809 let exit = output.split("fn exit_code").nth(1).unwrap();
810 assert!(
811 exit.contains("Self :: C => - 2i32") || exit.contains("Self :: C => -2i32"),
812 "{exit}"
813 );
814 }
815}