format-attr 0.2.1

a custom derive to implement Debug/Display easy
Documentation
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, allow(unused_attributes))]
#![doc = include_str!("../README.md")]

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::Parse, punctuated::Punctuated, DeriveInput, Expr, LitStr, Token};

/// Attribute arguments for `#[fmt("...", arg1, arg2)]`
struct FmtArgs {
    format_str: LitStr,
    _comma: Option<Token![,]>,
    args: Punctuated<Expr, Token![,]>,
}

impl Parse for FmtArgs {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let format_str: LitStr = input.parse()?;
        let _comma: Option<Token![,]> = input.parse()?;
        let args: Punctuated<Expr, Token![,]> =
            input.parse_terminated(Expr::parse, Token![,])?;

        Ok(FmtArgs {
            format_str,
            _comma,
            args,
        })
    }
}

/// Parse a specific attribute from the struct attributes by name.
fn parse_specific_attr(attrs: &[syn::Attribute], name: &str) -> Option<FmtArgs> {
    for attr in attrs {
        if attr.path().is_ident(name) {
            return attr.parse_args().ok();
        }
    }
    None
}

/// Parse `fmt_display` attribute, fallback to `fmt`.
fn parse_display_attr(attrs: &[syn::Attribute]) -> Option<FmtArgs> {
    parse_specific_attr(attrs, "fmt_display")
        .or_else(|| parse_specific_attr(attrs, "fmt"))
}

/// Parse `fmt_debug` attribute, fallback to `fmt`.
fn parse_debug_attr(attrs: &[syn::Attribute]) -> Option<FmtArgs> {
    parse_specific_attr(attrs, "fmt_debug")
        .or_else(|| parse_specific_attr(attrs, "fmt"))
}

/// Check if an expression is a simple field identifier (not `self.x` or method calls).
/// Returns true if the expr is a simple identifier like `field_name`.
fn is_simple_field(expr: &Expr) -> bool {
    matches!(expr, Expr::Path(expr_path) if expr_path.path.get_ident().is_some())
}

/// Regex pattern to match `{field_name}` where field_name is a valid identifier.
/// Matches: {name}, {name:?}, {name:#?}, {name:width}, {name:>10}
/// Does NOT match: {}, {:?}, {0}, {0:?} (positional/numeric indices)
fn extract_field_from_format(spec: &str) -> Option<&str> {
    // Find the content inside braces
    let content = spec.strip_prefix('{')?.strip_suffix('}')?;

    // Split on ':' to separate field name from format spec
    let field_part = content.split(':').next()?;

    // Check if it's a valid identifier (starts with letter/underscore, contains only alphanumeric/underscore)
    if field_part.is_empty() {
        return None;
    }

    let first_char = field_part.chars().next()?;
    if !first_char.is_ascii_alphabetic() && first_char != '_' {
        return None;
    }

    if !field_part.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
        return None;
    }

    Some(field_part)
}

/// Transform format string with `{field}` syntax to standard format string.
/// Returns the transformed format string and a list of field identifiers to use as arguments.
fn transform_format_string(input: &str) -> (String, Vec<String>) {
    let mut result = String::with_capacity(input.len());
    let mut fields = Vec::new();
    let mut chars = input.chars().peekable();

    while let Some(ch) = chars.next() {
        if ch == '{' {
            // Check if it's an escaped brace {{ or }}
            if chars.peek() == Some(&'{') {
                chars.next(); // consume second {
                result.push_str("{{");
                continue;
            }

            // Collect the content until }
            let mut brace_content = String::new();
            let mut found_closing = false;

            for inner_ch in chars.by_ref() {
                if inner_ch == '}' {
                    found_closing = true;
                    break;
                }
                brace_content.push(inner_ch);
            }

            if !found_closing {
                // Unclosed brace, treat as literal
                result.push('{');
                result.push_str(&brace_content);
                continue;
            }

            // Check if this is a named field reference
            if let Some(field_name) = extract_field_from_format(&format!("{{{}}}", brace_content)) {
                // It's a field name - replace with {} and add field to list
                fields.push(field_name.to_string());

                // Preserve the format spec after the field name
                let format_spec = &brace_content[field_name.len()..];
                result.push('{');
                result.push_str(format_spec);
                result.push('}');
            } else {
                // Not a field name (might be positional arg or empty), keep as-is
                result.push('{');
                result.push_str(&brace_content);
                result.push('}');
            }
        } else if ch == '}' {
            // Check if it's an escaped brace }}
            if chars.peek() == Some(&'}') {
                chars.next(); // consume second }
                result.push_str("}}");
            } else {
                result.push('}');
            }
        } else {
            result.push(ch);
        }
    }

    (result, fields)
}

/// Generate the fmt implementation body.
/// Automatically adds `self.` prefix to simple field identifiers.
fn generate_fmt_body(fmt_args: &FmtArgs) -> proc_macro2::TokenStream {
    let original_format_str = fmt_args.format_str.value();

    // Transform {field} syntax in format string
    let (transformed_format, extracted_fields) = transform_format_string(&original_format_str);

    // Build the final format string literal
    let format_str = LitStr::new(&transformed_format, fmt_args.format_str.span());

    // Collect arguments: first from extracted fields in format string, then from explicit args
    let mut all_args: Vec<proc_macro2::TokenStream> = Vec::new();

    // Add extracted fields from {field} syntax (as self.field)
    for field in &extracted_fields {
        let field_ident = syn::Ident::new(field, proc_macro2::Span::call_site());
        all_args.push(quote! { self.#field_ident });
    }

    // Add explicit arguments (with self. prefix for simple fields)
    for arg in &fmt_args.args {
        if is_simple_field(arg) {
            // Simple field identifier: add self. prefix
            all_args.push(quote! { self.#arg });
        } else {
            // Complex expression: keep as-is
            all_args.push(quote! { #arg });
        }
    }

    quote! {
        write!(f, #format_str #(, #all_args)*)
    }
}

/// Derive macro for implementing `std::fmt::Display`.
///
/// # Attributes
///
/// - `#[fmt_display("...", args...)]` - Format string specifically for `Display` (highest priority)
/// - `#[fmt("...", args...)]` - Shared format string for both `Display` and `Debug` (fallback)
///
/// # Example
///
/// ```
/// use format_attr::DisplayAttr;
///
/// #[derive(DisplayAttr)]
/// #[fmt("User: {}", self.name)]
/// struct User {
///     name: String,
/// }
/// ```
#[proc_macro_derive(DisplayAttr, attributes(fmt, fmt_display))]
pub fn derive_display(input: TokenStream) -> TokenStream {
    let input = syn::parse_macro_input!(input as DeriveInput);

    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let fmt_args = match parse_display_attr(&input.attrs) {
        Some(args) => args,
        None => {
            return syn::Error::new_spanned(
                &input,
                "DisplayAttr requires a #[fmt(...)] or #[fmt_display(...)] attribute",
            )
            .to_compile_error()
            .into();
        }
    };

    let fmt_body = generate_fmt_body(&fmt_args);

    let expanded = quote! {
        impl #impl_generics std::fmt::Display for #name #ty_generics #where_clause {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                #fmt_body
            }
        }
    };

    TokenStream::from(expanded)
}

/// Derive macro for implementing `std::fmt::Debug`.
///
/// # Attributes
///
/// - `#[fmt_debug("...", args...)]` - Format string specifically for `Debug` (highest priority)
/// - `#[fmt("...", args...)]` - Shared format string for both `Display` and `Debug` (fallback)
///
/// # Example
///
/// ```
/// use format_attr::DebugAttr;
///
/// #[derive(DebugAttr)]
/// #[fmt_debug("User {{ name: {} }}", self.name)]
/// struct User {
///     name: String,
/// }
/// ```
#[proc_macro_derive(DebugAttr, attributes(fmt, fmt_debug))]
pub fn derive_debug(input: TokenStream) -> TokenStream {
    let input = syn::parse_macro_input!(input as DeriveInput);

    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let fmt_args = match parse_debug_attr(&input.attrs) {
        Some(args) => args,
        None => {
            return syn::Error::new_spanned(
                &input,
                "DebugAttr requires a #[fmt(...)] or #[fmt_debug(...)] attribute",
            )
            .to_compile_error()
            .into();
        }
    };

    let fmt_body = generate_fmt_body(&fmt_args);

    let expanded = quote! {
        impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                #fmt_body
            }
        }
    };

    TokenStream::from(expanded)
}

/// Derive macro for implementing `std::fmt::Display` using the existing `Debug` implementation.
///
/// This macro requires that the type already implements `Debug`. It delegates the `Display`
/// implementation to the `Debug` implementation, so both `{}` and `{:?}` will produce the same output.
///
/// # Example
///
/// ```
/// use format_attr::DisplayAsDebug;
///
/// #[derive(Debug, DisplayAsDebug)]
/// struct Value(i32);
///
/// let v = Value(42);
/// assert_eq!(format!("{}", v), "Value(42)");
/// assert_eq!(format!("{:?}", v), "Value(42)");
/// ```
#[proc_macro_derive(DisplayAsDebug)]
pub fn derive_display_as_debug(input: TokenStream) -> TokenStream {
    let input = syn::parse_macro_input!(input as DeriveInput);

    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let expanded = quote! {
        impl #impl_generics std::fmt::Display for #name #ty_generics #where_clause {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                std::fmt::Debug::fmt(self, f)
            }
        }
    };

    TokenStream::from(expanded)
}