#enums #proc-macro #variant #constant #const

macro enum-assoc

Procedural macro to associate constants with enum variants

18 releases (2 stable)

1.1.0 Apr 23, 2023
1.0.0 Mar 21, 2023
0.4.0 Oct 17, 2022
0.3.4 May 10, 2022
0.1.8 Feb 21, 2022

#1151 in Rust patterns

Download history 1019/week @ 2024-07-02 1458/week @ 2024-07-09 1180/week @ 2024-07-16 1129/week @ 2024-07-23 2758/week @ 2024-07-30 2191/week @ 2024-08-06 2644/week @ 2024-08-13 2258/week @ 2024-08-20 2277/week @ 2024-08-27 2576/week @ 2024-09-03 2641/week @ 2024-09-10 3081/week @ 2024-09-17 2300/week @ 2024-09-24 3003/week @ 2024-10-01 1840/week @ 2024-10-08 2655/week @ 2024-10-15

10,435 downloads per month
Used in 7 crates (5 directly)

MIT/Apache

26KB
363 lines

enum-assoc

This crate defines a few macros that allow you to associate constants or data with enum variants.

To use, #[derive(Assoc)] must be attached to an enum. From there, the func attribute is used to define function signatures which will be implemented for that enum. The assoc attribute is used to define constants which each variant will return when that function is called.

Forward associations

Here's an example:

use enum_assoc::Assoc;

const WA: &'static str = "wa";

#[derive(Assoc)]
#[func(pub const fn foo(&self) -> u8)]
#[func(pub fn bar(&self) -> &'static str)]
#[func(pub fn maybe_foo(&self) -> Option<u8>)]
#[func(pub fn with_default(&self) -> u8 { 4 })]
enum TestEnum {
    #[assoc(foo = 255)]
    #[assoc(bar = "wow")]
    Variant1,
    #[assoc(foo = 1 + 7)]
    #[assoc(bar = "wee")]
    #[assoc(with_default = 2)]
    Variant2,
    #[assoc(foo = 0)]
    #[assoc(bar = WA)]
    #[assoc(maybe_foo = 18 + 2)]
    Variant3
}

fn main() {
    println!("Variant1 foo: {}", TestEnum::Variant1.foo());
    println!("Variant2 foo: {}", TestEnum::Variant2.foo());
    println!("Variant3 foo: {}", TestEnum::Variant3.foo());
    println!("Variant1 bar: {}", TestEnum::Variant1.bar());
    println!("Variant2 bar: {}", TestEnum::Variant2.bar());
    println!("Variant3 bar: {}", TestEnum::Variant3.bar());
    println!("Variant1 maybe_foo: {:?}", TestEnum::Variant1.maybe_foo());
    println!("Variant2 maybe_foo: {:?}", TestEnum::Variant2.maybe_foo());
    println!("Variant3 maybe_foo: {:?}", TestEnum::Variant3.maybe_foo());
    println!("Variant1 with_default: {:?}", TestEnum::Variant1.with_default());
    println!("Variant2 with_default: {:?}", TestEnum::Variant2.with_default());
    println!("Variant3 with_default: {:?}", TestEnum::Variant3.with_default());
}

Output:

Variant1 foo: 255
Variant2 foo: 8
Variant3 foo: 0
Variant1 bar: wow
Variant2 bar: wee
Variant3 bar: wa
Variant1 maybe_foo: None
Variant2 maybe_foo: None
Variant3 maybe_foo: Some(20)
Variant1 with_default: 4
Variant2 with_default: 2
Variant3 with_default: 4

Note that functions which return an Option type have special functionality: Variants may leave out the assoc attribute entirely to automatically return None, and variants which do yield a value need not explicitly wrap it in Some.

What does this output?

Every #[func(fn_signature)] attribute generates something like the following:

impl Enum {
    fn_signature {
        match self {
            // ... arms
        }
    }
}

And every #[assoc(fn_name = association)] attribute generates an arm for its associated function like the following:

    variant_name => association,

That's it. Both the details of the fn_signature you use and what you put in the association area are up to you.

So while technically not the original intention of this crate, you can generate some more interesting/complex associations for free:

use enum_assoc::Assoc;

#[derive(Assoc)]
#[func(pub fn foo(&self, param: u8) -> Option<u8>)]
#[func(pub fn bar(&self, param: &str) -> String)]
#[func(pub fn baz<T: std::fmt::Debug>(&self, param: T) -> Option<String>)]
enum TestEnum2 {
    #[assoc(bar = String::new() + param)]
    Variant1,
    #[assoc(foo = 16 + param)]
    #[assoc(bar = String::from("Hello") + param)]
    Variant2,
    #[assoc(bar = some_str_func(param))]
    #[assoc(baz = format!("{:?}", param))]
    Variant3
}

fn some_str_func(s: &str) -> String {
    String::from("I was created in a function") + s
}

fn main() {
    println!("Variant1 foo: {:?}", TestEnum2::Variant1.foo(0));
    println!("Variant2 foo: {:?}", TestEnum2::Variant2.foo(22));
    println!("Variant1 bar: {}", TestEnum2::Variant1.bar("string"));
    println!("Variant2 bar: {}", TestEnum2::Variant2.bar(" World!"));
    println!("Variant3 bar: {}", TestEnum2::Variant3.bar("!"));
    println!("Variant3 baz: {:?}", TestEnum2::Variant3.baz(1));
}

Output:

Variant1 foo: None
Variant2 foo: 34
Variant1 bar: string
Variant2 bar: Hello World!
Variant3 bar: I was created in a function!
Variant3 baz: Some("1")

Accessing enum fields in assoc attribute

It is possible to access an enum variant field value in assoc attribute prefixing its name with an underscore. For tuples the names is composed of underscore prefix and field index.

use thiserror::Error;

#[derive(Error, Debug, Assoc, Clone)]
#[func(pub const fn status(&self) -> u16)]
pub enum ServiceError {
    #[error("failed to start or finish a transaction")]
    #[assoc(status = 500)]
    TransactionError { source: sea_orm::DbErr },

    #[error(transparent)]
    #[assoc(status = _source.status())]
    RequestParsingError {
        source: request_parser::RequestParsingError,
    },
}

mod request_parser {
    use super::*;

    #[derive(Error, Debug, Assoc, Clone)]
    #[func(pub const status(&self) -> u16)]
    pub enum RequestParsingError{
        #[error("provided input was too large")]
        #[assoc(status = 500)]
        OutOfMemory
        #[error("the resource id did not have correct format")]
        #[assoc(status = 400)]
        InvalidResourceIdFormat
        #[error("external validator service returned an error")]
        #[assoc(status = _0.status())]
        ExternalValidatorError(validator::Error)
    }
}

pub mod validator{
    //...
}

Reverse associations

This can also generate reverse associations (constants to enum variants). See below for an example.

use enum_assoc::Assoc;

#[derive(Assoc, Debug)]
#[func(pub fn foo(s: &str) -> Option<Self>)]
#[func(pub fn bar(u: u8) -> Self)]
#[func(pub fn baz(u1: u8, u2: u8) -> Self)]
enum TestEnum3
{
    #[assoc(foo = "variant1")]
    #[assoc(bar = _)]
    Variant1,
    #[assoc(bar = 2)]
    #[assoc(foo = "variant2")]
    #[assoc(baz = (3, 7))]
    Variant2,
    #[assoc(foo = "I'm variant 3!")]
    #[assoc(foo = "variant3")]
    #[assoc(baz = _)]
    Variant3
}

fn main()
{
    println!("TestEnum3 foo(\"variant1\"): {:?}", TestEnum3::foo("variant1"));
    println!("TestEnum3 foo(\"variant3\"): {:?}", TestEnum3::foo("variant3"));
    println!("TestEnum3 foo(\"I'm variant 3!\"): {:?}", TestEnum3::foo("I'm variant 3!"));
    println!("TestEnum3 foo(\"I don't exist\"): {:?}", TestEnum3::foo("I don't exist"));
    println!("TestEnum3 bar(2): {:?}", TestEnum3::bar(2));
    println!("TestEnum3 bar(55): {:?}", TestEnum3::bar(55));
    println!("TestEnum3 baz(3, 7): {:?}", TestEnum3::baz(3, 7));
    println!("TestEnum3 baz(0, 0): {:?}", TestEnum3::baz(0, 0));
}

Output:

TestEnum3 foo("variant1"): Some(Variant1)
TestEnum3 foo("variant3"): Some(Variant3)
TestEnum3 foo("I'm variant 3!"): Some(Variant3)
TestEnum3 foo("I don't exist"): None
TestEnum3 bar(2): Variant2
TestEnum3 bar(55): Variant1
TestEnum3 baz(3, 7): Variant2
TestEnum3 baz(0, 0): Variant3

Reverse associations work slightly differently than forward associations:

  • Reverse associations must not include a self parameter (the lack of a self paramater is what differentiates a forward association from a reverse association)
  • They must return either Self or Option<Self>
  • Unlike forward associations, any number of assoc attributes for the same function may be defined for a single enum variant.
  • Unlike forward associations, the assoc attribute defines a pattern rather than an expression. This is because reverse associations control the left side of a match arm rather than the right side.
  • The function generated will match on a tuple containing all of the function arguments.
  • Match arms will be ordered exactly as written from top to bottom with one excpetion: any wildcard pattern _ will always be placed at the bottom.
  • There can be no more than 1 wildcard association for any reverse-associative function. Any more will result in a compile error.
  • If no wildcard pattern is defined for a function that returns Option<Self>, a _ => None arm will be inserted automatically.

So for a simple reverse association to generate valid code, 1 of these 3 conditions must be satisfied:

  1. The reverse association returns Option<Self>, or
  2. A wildcard (_) pattern is defined for exactly 1 variant, or
  3. Every possible value maps to an enum variant
  • Note: For reverse associations that return more than 1 argument, it is possible to use wildcards for specific arguments (eg (5, _)). This macro does not attempt to re-order this in the same way it does to catch-all wildcards (_). The match arm will be placed exactly where it appears in the column of enum attributes.

Currently, there is no way for reverse associations to map to tuple or struct-like variants.

What does this output?

Every #[func(fn_signature)] attribute for reverse associations generates something like the following:

impl Enum {
    fn_signature {
        match (param1, param2, etc) {
            // ... arms
        }
    }
}

And every #[assoc(fn_name = pattern)] attribute for reverse associations generates an arm for its associated function like the following:

    pattern => variant_name,

Dependencies

~1.5MB
~37K SLoC