Skip to content

Formatting #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Formatting #5

wants to merge 3 commits into from

Conversation

jngls
Copy link

@jngls jngls commented Jun 19, 2025

This is a considerable update of the proc macro which supports optional custom format strings per variant with full access to variant fields via the #[variant_display(format = "xyz")] attribute.

I recommend reading the code in isolation rather than as a diff.

Summary

  • Added EnumAttrs which handles parsing of the main enum_display attribute
  • Added VariantAttrs which handles parsing of the new variant_display field attribute
  • variant_display field attribute allows the user to customise each variant
    • The format = "xyz" argument defines a custom display format (see below for Format Behaviour) - if omitted, the original crate behaviour is used
  • Introduced a new Intermediate Representation (IR) to hold variant info and allow for more complex processing:
    • VariantInfo holds properties common to all variant types
    • NamedVariantIR, UnnamedVariantIR, and UnitVariantIR are the intermediate representations of variant types
    • VariantIR wraps all of those
  • Pipeline updated to account for new Intermediate Representation
    • Parsing of syn tokens is moved to the from_*() methods of EnumAttrs, VariantAttrs, and VariantIR
    • We detect if any variant specifies a "format" string - this is to ensure all match arms return the same type
    • Finally we generate match arm tokens with the VariantIR::gen() method
  • Tests have been updated

Format Behaviour

Format strings behave almost identically to using format!() in normal rust code so it should be very familiar to users. Besides a bit of translation of unnamed fields, the string specified by the user is the string that ends up as the parameter to format!() in each match arm.

The following considerations apply:

  • To print the case adjusted variant name, you use the {variant} placeholder
  • To print named variant fields, you use the field name placeholder, eg. if we have Variant { my_field: u32 }, you use the {my_field} placeholder
  • To print unnamed variant fields, you use a numbered placeholder, eg. if we have Variant(u32), you use the {0} placeholder (to access the first tuple element)
  • It is not currently possible to access fields within fields - each field must implment its own Display or Debug
  • Most of the common inline format specs are supported, eg. {my_field:?} displays my_field using its Debug implementation, {0:5} displays the first unnamed field with a width of 5 - however - format spec options which depend on extra arguments passed into format!() are not supported

Additional Notes

Full examples

#[derive(EnumDisplay)]
enum TestEnum {
    // Displayed as "Name"
    Name,

    // Displayed as "Unit: NameFullFormat"
    #[variant_display(format = "Unit: {variant}")]
    NameFullFormat,

    // Displayed as "Address"
    Address {
        street: String,
        city: String,
        state: String,
        zip: String,
    },

    // Displayed as "Named: AddressPartialFormat {123 Main St, 12345}"
    #[variant_display(format = "Named: {variant} {{{street}, {zip}}}")]
    AddressPartialFormat {
        street: String,
        city: String,
        state: String,
        zip: String,
    },

    // Displayed as "Named: AddressFullFormat {123 Main St, Any Town, CA, 12345}"
    #[variant_display(format = "Named: {variant} {{{street}, {city}, {state}, {zip}}}")]
    AddressFullFormat {
        street: String,
        city: String,
        state: String,
        zip: String,
    },

    // Displayed as "DateOfBirth"
    DateOfBirth(u32, u32, u32),

    // Displayed as "Unnamed: DateOfBirthPartialFormat(1999)"
    #[variant_display(format = "Unnamed: {variant}({2})")]
    DateOfBirthPartialFormat(u32, u32, u32),

    // Displayed as "Unnamed: DateOfBirthFullFormat(1, 2, 1999)"
    #[variant_display(format = "Unnamed: {variant}({0}, {1}, {2})")]
    DateOfBirthFullFormat(u32, u32, u32),
}

#[test]
fn test_unit_field_variant() {
    assert_eq!(TestEnum::Name.to_string(), "Name");
    assert_eq!(TestEnum::NameFullFormat.to_string(), "Unit: NameFullFormat");
}

#[test]
fn test_named_fields_variant() {
    assert_eq!(
        TestEnum::Address {
            street: "123 Main St".to_string(),
            city: "Any Town".to_string(),
            state: "CA".to_string(),
            zip: "12345".to_string()
        }
        .to_string(),
        "Address"
    );
    assert_eq!(
        TestEnum::AddressPartialFormat {
            street: "123 Main St".to_string(),
            city: "Any Town".to_string(),
            state: "CA".to_string(),
            zip: "12345".to_string()
        }
        .to_string(),
        "Named: AddressPartialFormat {123 Main St, 12345}"
    );
    assert_eq!(
        TestEnum::AddressFullFormat {
            street: "123 Main St".to_string(),
            city: "Any Town".to_string(),
            state: "CA".to_string(),
            zip: "12345".to_string()
        }
        .to_string(),
        "Named: AddressFullFormat {123 Main St, Any Town, CA, 12345}"
    );
}

#[test]
fn test_unnamed_fields_variant() {
    assert_eq!(TestEnum::DateOfBirth(1, 2, 1999).to_string(), "DateOfBirth");
    assert_eq!(TestEnum::DateOfBirthPartialFormat(1, 2, 1999).to_string(), "Unnamed: DateOfBirthPartialFormat(1999)");
    assert_eq!(TestEnum::DateOfBirthFullFormat(1, 2, 1999).to_string(), "Unnamed: DateOfBirthFullFormat(1, 2, 1999)");
}

@jngls
Copy link
Author

jngls commented Jun 19, 2025

Simplified the code a bit. It was previously checking that the user actually referenced each field before emitting variables for them, but this adds unnecessary complexity. We can just emit all field variables and let the compiler decide if they're unused.

@jngls
Copy link
Author

jngls commented Jun 19, 2025

Btw, my specific use case was this:

#[derive(Debug, Clone, PartialEq, Eq, EnumDisplay)]
#[enum_display(case = "Lower")]
pub enum ParserErrorKind<I> {
    NomError(ErrorKind),
    #[variant_display(format = "{variant}: {0}")]
    Keyword(Keyword),
    Ident(Ident<I>),
    ...
}

Where I wanted to provide a general impl of Display for most variants, but be specific for required Keywords.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant