Support Ukraine. DONATE.
A blog about software development.

Handling Rust enum variants with kinded crate

Serhii Potapov August 07, 2023 #rust #traits #macro #enum

Over the weekend, I delved into crafting a tiny procedural macro library called kinded. The idea behind this venture originated from my need to support another procedural macro library of mine, nutype.

In this article, I will walk you through the concept and application of the kinded library.

The Use Case: Building a Beverage Selection UI

Imagine you're developing a beverage ordering application. Users can choose from a variety of drinks, some of which might have additional customization options. For instance, you offer various types of coffee and tear, each with different flavors. To create a smooth user experience, you want to present users with a selection of drink categories first and then prompt them for specific details based on their selection.

The Problem

Your Drink enum captures the different types of beverages available:

enum Drink {
    TapWater,
    Coffee(String),
    Tea { variety: String, caffeine: bool },
}

However, building the UI becomes a bit tricky. You want to display a list of drink categories to users before they provide additional details for their chosen drink. To do this, you need to extract just the kind of each variant without dealing with the associated data.

Initial solution

So we introduce DrinkKind type, which is just a plain twin of Drink without extra data attached to variants:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DrinkKind
    TapWater,
    Coffee,
    Tea,
}

We may also want to have ability to convert Drink -> DrinkKind:

impl Drink {
    fn kind(&self) -> DrinkKind {
        match self {
            Drink::TapWater -> DrinkKind::TapWater,
            Drink::Coffee(..) -> DrinkKind::Coffee,
            Drink::Tea { .. } -> DrinkKind::Tea
        }
    }
}

And ability to iterate over all variants of DrinkKind:

impl DrinkKind {
    fn all() -> Vec<DrinkKind> {
        use DrinkKind::*;
        vec![TapWater, Coffee, Tea]
    }
}

The problem with the solution

The solution above will work. But if you have 20 of such enums, the boilerplate work multiplies. Also the maintenance cost is doubled: if you want to add a new drink (e.g. Mate 🧉), you'd need extend Drink and DrinkKind. And do not forget to update DrinkKind::all() implementation! The compiler won't ask you to do it.

Enter kinded crate

The boilerplate can be eliminated with help of kinded crate and its Kinded derive macro:

#[derive(Kinded)]
#[kinded(kind = DrinkKind)]
enum Drink {
    TapWater,
    Coffee(String),
    Tea { variety: String, caffeine: bool },
}

This generates exactly the same code (and a bit more), that wrote manually in the previous step.

Listing drink variants

So now we can list all the possible drink variants as the following:

for drink_kind in DrinkKind::all() {
    println!("{drink_kind}");
}

Output:

TapWater
Coffee
Tea

Fine but TapWater does not look very human friendly. We'd rather like to see Tap Water instead.

It's possible to customize implementation of Display crate for DrinkKind with display = option.

#[derive(Kinded)]
#[kinded(kind = DrinkKind, display = "Title Case")]
enum Drink {
    // variants
}

So now TapWater variant is displayed as Tap Water. At the moment of writing, the library supports 9 different casing strategies like snake_case, camelCase, etc.

What about parsing?

Implementation of FromStr is also generated for free and is capable to parse all possible spelling of the variants. For example:

let alt_spellings = ["TapWater", "tap_water", "TAPWATER", "tap-water"];
for alt_spelling in alt_spellings {
    let drink: DrinkKind = alt_spelling.parse().unwrap();
    assert_eq!(drink, DrinkKind::TapWater);
}

Closing the Beverage Select UI

Once the user selects a drink category, you can further retrieve the associated data from the original Drink enum based on the choice:

fn process_user_selection(kind: DrinkKind) {
    match kind {
        DrinkKind::TapWater => {
            // Process TapWater selection
        }
        DrinkKind::Coffee => {
            // Ask for coffee details and process
        }
        DrinkKind::Tea => {
            // Ask for tea details and process
        }
    }
}

Conclusion

The kinded library simplifies the handling of enums with associated data by generating a kind type that abstracts away the data, making it easier to build user interfaces and handle various enum variants and to write parsers.

To learn more about the kinded library please check the following links:

Back to top