Skip to main content

error_forge_derive/
lib.rs

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/// Derive macro for ModError
10///
11/// This macro automatically implements the ForgeError trait and common
12/// error handling functionality for a struct or enum, allowing for
13/// "lazy mode" error creation with minimal boilerplate.
14///
15/// # Example
16///
17/// When the macro is used in your application where error-forge is a dependency:
18///
19/// ```ignore
20/// use error_forge::ModError;
21///
22/// #[derive(Debug, ModError)]
23/// #[error_prefix("Database")]
24/// pub enum DbError {
25///     #[error_display("Connection to {0} failed")]
26///     ConnectionFailed(String),
27///
28///     #[error_display("Query execution failed: {reason}")]
29///     QueryFailed { reason: String },
30///
31///     #[error_display("Transaction error")]
32///     #[error_http_status(400)]
33///     TransactionError,
34/// }
35/// ```
36///
37/// `#[error_display("...")]` is optional; without it a variant displays
38/// as its name. The string is a `format!` string and may reference any
39/// subset of the variant's fields: by name for struct-like variants
40/// (a raw identifier such as `r#type` is written `{type}`), by position
41/// (`{0}`, `{}`) for tuple variants. Fields it does not mention are
42/// ignored, `{{` and `}}` produce literal braces, and a width or
43/// precision given where the error is formatted (`{:>20}`) pads or
44/// truncates the whole message.
45///
46/// # Attributes
47///
48/// On the enum (or struct):
49///
50/// - `#[error_prefix("...")]`: caption prefix, giving `"<prefix>: Error"`.
51///
52/// On each enum variant:
53///
54/// - `#[error_display("...")]`, `#[error_kind("...")]`,
55///   `#[error_caption("...")]`: string literals.
56/// - `#[error_retryable]`, `#[error_fatal]`: flags. A bare flag means
57///   `true`; an explicit value (`#[error_retryable(false)]` or
58///   `#[error_retryable = false]`) is honoured.
59/// - `#[error_http_status(404)]`: an integer that fits in `u16`.
60/// - `#[error_exit_code(2)]`: an integer that fits in `i32`.
61///
62/// Each attribute accepts both the list form (`#[name(value)]`) and the
63/// name-value form (`#[name = value]`). A value of the wrong type or out
64/// of range is a compile error.
65///
66/// # Structs
67///
68/// On a struct only `#[error_prefix]` is read. The struct displays as
69/// `"<prefix>: Error"`, its `kind()` is the struct name, and the other
70/// metadata methods use the `ForgeError` defaults. The variant
71/// attributes listed above (`#[error_display]`, `#[error_retryable]`,
72/// and so on) are currently ignored when placed on a struct.
73///
74/// Note: This is a procedural macro that is re-exported by the `error-forge` crate.
75/// When using in your application, import it from the main crate with `use error_forge::ModError;`.
76#[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    // Parse the input
91    let input = parse_macro_input!(input as DeriveInput);
92
93    // Return the generated implementation
94    TokenStream::from(expand(&input))
95}
96
97/// Expand `#[derive(ModError)]` for a parsed item.
98///
99/// Unsupported input (a union) and malformed attributes produce a
100/// `compile_error!` pointing at the offending tokens instead of
101/// panicking inside the compiler or falling back to defaults.
102fn 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
118/// Rewrite a display format so it only needs the fields it mentions.
119///
120/// `fields` lists the variant's fields in declaration order: their
121/// names (without any `r#` prefix) for a struct-like variant, `None` for
122/// a tuple variant. Every reference to a field, whether positional
123/// (`{}`, `{0}`), by name (`{host}`, `{r#type}`) or as a width or
124/// precision argument (`{:width$}`), is rewritten to an explicit
125/// position in the argument list the caller passes. Names that are not
126/// fields (`{CONST}`) are left for `format_args!` to capture.
127///
128/// Returns the rewritten string and the referenced field indices in
129/// argument order, or `None` for strings this does not handle (`.*`
130/// precision, malformed braces, out-of-range indices); the caller then
131/// passes every field and lets `format_args!` report any error.
132fn 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                    // Map `name$` / `N$` width and precision arguments.
200                    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
252/// The value written after an attribute's path.
253enum AttrValue {
254    /// `#[name]`
255    Flag,
256    /// `#[name(value)]` or `#[name = value]`, with an optional leading
257    /// minus sign for negative integers.
258    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
363/// Merge `error` into the running error so every bad attribute is
364/// reported, not only the first.
365fn 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
372// Extract error_prefix attribute value
373fn 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
383/// Metadata read from one variant's attributes.
384struct 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
434/// Build the `Display` match arm for one variant. The arm writes into
435/// `writer`, a `&mut dyn core::fmt::Write`.
436fn display_arm(variant: &syn::Variant, display: &str, writer: &Ident) -> TokenStream2 {
437    let variant_name = &variant.ident;
438    // Bindings use mixed-site hygiene so a display string cannot capture
439    // them by accident and they cannot collide with user items.
440    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
514// Implement ModError for an enum
515fn 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    // Generate match arms for each variant
528    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    // Generate implementation
576    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                    // Width and precision apply to the whole message, so
588                    // it has to be rendered before it can be padded.
589                    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
642// Implement ModError for a struct
643fn 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        // Positional references in a struct-like variant map to the
722        // fields in declaration order, as format! did before.
723        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}