Skip to main content

format_attr/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![cfg_attr(docsrs, allow(unused_attributes))]
3#![doc = include_str!("../README.md")]
4
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::{parse::Parse, punctuated::Punctuated, DeriveInput, Expr, LitStr, Token};
8
9/// Attribute arguments for `#[fmt("...", arg1, arg2)]`
10struct FmtArgs {
11    format_str: LitStr,
12    _comma: Option<Token![,]>,
13    args: Punctuated<Expr, Token![,]>,
14}
15
16impl Parse for FmtArgs {
17    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
18        let format_str: LitStr = input.parse()?;
19        let _comma: Option<Token![,]> = input.parse()?;
20        let args: Punctuated<Expr, Token![,]> =
21            input.parse_terminated(Expr::parse, Token![,])?;
22
23        Ok(FmtArgs {
24            format_str,
25            _comma,
26            args,
27        })
28    }
29}
30
31/// Parse a specific attribute from the struct attributes by name.
32fn parse_specific_attr(attrs: &[syn::Attribute], name: &str) -> Option<FmtArgs> {
33    for attr in attrs {
34        if attr.path().is_ident(name) {
35            return attr.parse_args().ok();
36        }
37    }
38    None
39}
40
41/// Parse `fmt_display` attribute, fallback to `fmt`.
42fn parse_display_attr(attrs: &[syn::Attribute]) -> Option<FmtArgs> {
43    parse_specific_attr(attrs, "fmt_display")
44        .or_else(|| parse_specific_attr(attrs, "fmt"))
45}
46
47/// Parse `fmt_debug` attribute, fallback to `fmt`.
48fn parse_debug_attr(attrs: &[syn::Attribute]) -> Option<FmtArgs> {
49    parse_specific_attr(attrs, "fmt_debug")
50        .or_else(|| parse_specific_attr(attrs, "fmt"))
51}
52
53/// Generate the fmt implementation body.
54fn generate_fmt_body(fmt_args: &FmtArgs) -> proc_macro2::TokenStream {
55    let format_str = &fmt_args.format_str;
56    let args: Vec<_> = fmt_args.args.iter().collect();
57
58    quote! {
59        write!(f, #format_str #(, #args)*)
60    }
61}
62
63/// Derive macro for implementing `std::fmt::Display`.
64///
65/// # Attributes
66///
67/// - `#[fmt_display("...", args...)]` - Format string specifically for `Display` (highest priority)
68/// - `#[fmt("...", args...)]` - Shared format string for both `Display` and `Debug` (fallback)
69///
70/// # Example
71///
72/// ```
73/// use format_attr::DisplayAttr;
74///
75/// #[derive(DisplayAttr)]
76/// #[fmt("User: {}", self.name)]
77/// struct User {
78///     name: String,
79/// }
80/// ```
81#[proc_macro_derive(DisplayAttr, attributes(fmt, fmt_display))]
82pub fn derive_display(input: TokenStream) -> TokenStream {
83    let input = syn::parse_macro_input!(input as DeriveInput);
84
85    let name = &input.ident;
86    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
87
88    let fmt_args = match parse_display_attr(&input.attrs) {
89        Some(args) => args,
90        None => {
91            return syn::Error::new_spanned(
92                &input,
93                "DisplayAttr requires a #[fmt(...)] or #[fmt_display(...)] attribute",
94            )
95            .to_compile_error()
96            .into();
97        }
98    };
99
100    let fmt_body = generate_fmt_body(&fmt_args);
101
102    let expanded = quote! {
103        impl #impl_generics std::fmt::Display for #name #ty_generics #where_clause {
104            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105                #fmt_body
106            }
107        }
108    };
109
110    TokenStream::from(expanded)
111}
112
113/// Derive macro for implementing `std::fmt::Debug`.
114///
115/// # Attributes
116///
117/// - `#[fmt_debug("...", args...)]` - Format string specifically for `Debug` (highest priority)
118/// - `#[fmt("...", args...)]` - Shared format string for both `Display` and `Debug` (fallback)
119///
120/// # Example
121///
122/// ```
123/// use format_attr::DebugAttr;
124///
125/// #[derive(DebugAttr)]
126/// #[fmt_debug("User {{ name: {} }}", self.name)]
127/// struct User {
128///     name: String,
129/// }
130/// ```
131#[proc_macro_derive(DebugAttr, attributes(fmt, fmt_debug))]
132pub fn derive_debug(input: TokenStream) -> TokenStream {
133    let input = syn::parse_macro_input!(input as DeriveInput);
134
135    let name = &input.ident;
136    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
137
138    let fmt_args = match parse_debug_attr(&input.attrs) {
139        Some(args) => args,
140        None => {
141            return syn::Error::new_spanned(
142                &input,
143                "DebugAttr requires a #[fmt(...)] or #[fmt_debug(...)] attribute",
144            )
145            .to_compile_error()
146            .into();
147        }
148    };
149
150    let fmt_body = generate_fmt_body(&fmt_args);
151
152    let expanded = quote! {
153        impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
154            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155                #fmt_body
156            }
157        }
158    };
159
160    TokenStream::from(expanded)
161}
162
163/// Derive macro for implementing `std::fmt::Display` using the existing `Debug` implementation.
164///
165/// This macro requires that the type already implements `Debug`. It delegates the `Display`
166/// implementation to the `Debug` implementation, so both `{}` and `{:?}` will produce the same output.
167///
168/// # Example
169///
170/// ```
171/// use format_attr::DisplayAsDebug;
172///
173/// #[derive(Debug, DisplayAsDebug)]
174/// struct Value(i32);
175///
176/// let v = Value(42);
177/// assert_eq!(format!("{}", v), "Value(42)");
178/// assert_eq!(format!("{:?}", v), "Value(42)");
179/// ```
180#[proc_macro_derive(DisplayAsDebug)]
181pub fn derive_display_as_debug(input: TokenStream) -> TokenStream {
182    let input = syn::parse_macro_input!(input as DeriveInput);
183
184    let name = &input.ident;
185    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
186
187    let expanded = quote! {
188        impl #impl_generics std::fmt::Display for #name #ty_generics #where_clause {
189            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190                std::fmt::Debug::fmt(self, f)
191            }
192        }
193    };
194
195    TokenStream::from(expanded)
196}