Skip to content

greyblake/kinded

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

91 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kinded

Nutype Build Status Nutype Documentation

Generate Rust enum variants without associated data

Get Started

use kinded::Kinded;

#[derive(Kinded)]
enum Drink {
    Mate,
    Coffee(String),
    Tea { variety: String, caffeine: bool }
}

let drink = Drink::Coffee("Espresso".to_owned());
assert_eq!(drink.kind(), DrinkKind::Coffee);

Note, the definition of DrinkKind enum is generated automatically as well as Drink::kind() method. To put it simply you get something similar to the following:

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

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

Const context

The kind() method is a const fn, so it can be used in const contexts:

use kinded::Kinded;

#[derive(Kinded)]
enum Status {
    Active,
    Inactive,
}

const ACTIVE_KIND: StatusKind = Status::Active.kind();

Kinded trait

The library provides Kinded trait:

pub trait Kinded {
    type Kind: PartialEq + Eq + Debug + Clone + Copy;

    fn kind(&self) -> Self::Kind;
}

From the example above, the derived implementation of Kinded for Drink resembles the following:

impl Kinded for Drink {
    type Kind = DrinkKind;

    fn kind(&self) -> DrinkKind { /* implementation */ }
}

The Kinded trait allows to build abstract functions that can be used with different enum types.

Get all kind variants

The kind type gets implementation of ::all() associated function, which returns a vector with all kind variants:

assert_eq!(DrinkKind::all(), [DrinkKind::Mate, DrinkKind::Coffee, DrinkKind::Tea]);

Attributes

Custom kind type name

By default the kind type name is generated by adding postfix Kind to the original enum name. This can be customized with kind = attribute:

use kinded::Kinded;

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

Derive traits

By default the kind type implements the following traits: Debug, Clone, Copy, PartialEq, Eq, Display, FromStr, From<T>, From<&T>.

Extra traits can be derived with derive(..) attribute:

use kinded::Kinded;
use std::collections::HashSet;

#[derive(Kinded)]
#[kinded(derive(Hash))]
enum Drink {
    Mate,
    Coffee(String),
    Tea { variety: String, caffeine: bool }
}

let mut drink_kinds = HashSet::new();
drink_kinds.insert(DrinkKind::Mate);

Skip default traits

In some cases you may need to opt out of default trait implementations. For example, when using kinded with crates like enumset that provide their own trait implementations, you can use skip_derive(..) to avoid conflicts:

use kinded::Kinded;

#[derive(Kinded)]
#[kinded(skip_derive(Display, FromStr))]
enum Task {
    Download { url: String },
    Process(Vec<u8>),
}

// Display and FromStr are not implemented for TaskKind
// You can provide your own custom implementations if needed

The following traits can be skipped:

  • Derived traits: Debug, Clone, Copy, PartialEq, Eq
  • Implemented traits: Display, FromStr, From

You can combine skip_derive with derive to replace default traits:

use kinded::Kinded;

#[derive(Kinded)]
#[kinded(skip_derive(Display), derive(Hash))]
enum Expr {
    Literal(i64),
    Variable(String),
    BinaryOp { left: Box<Expr>, op: char, right: Box<Expr> },
}

Extra attributes

If you're using derive macros from other libraries like Serde or Strum, you may want to add extra attributes specific to those libraries to the generated kind enum. You can do this using the attrs attribute:

use kinded::Kinded;
use serde::Serialize;

#[derive(Kinded, Serialize)]
#[kinded(derive(Serialize), attrs(serde(rename_all = "snake_case")))]
enum Drink {
    VeryHotBlackTea,
    Milk { fat: f64 },
}

let json = serde_json::to_string(&DrinkKind::VeryHotBlackTea).unwrap();
assert_eq!(json, r#""very_hot_black_tea""#);

You can pass multiple attributes:

#[kinded(attrs(
    serde(rename_all = "camelCase"),
    doc = "Kind of drink"
))]

Variant attributes

You can also apply attributes to individual variants of the generated kind enum using attrs on the variant:

use kinded::Kinded;
use serde::Serialize;

#[derive(Kinded, Serialize)]
#[kinded(derive(Default, Serialize), attrs(serde(rename_all = "snake_case")))]
enum Priority {
    Low,
    #[kinded(attrs(default, serde(rename = "normal")))]
    Medium,
    High,
}

// Medium is the default
assert_eq!(PriorityKind::default(), PriorityKind::Medium);

// Serde uses the custom rename for Medium
let json = serde_json::to_string(&PriorityKind::Medium).unwrap();
assert_eq!(json, r#""normal""#);

// Other variants use the enum-level rename_all
let json = serde_json::to_string(&PriorityKind::High).unwrap();
assert_eq!(json, r#""high""#);

You can combine attrs with rename on the same variant:

#[derive(Kinded)]
#[kinded(derive(Default))]
enum Level {
    #[kinded(rename = "low_level", attrs(default))]
    Low,
    Medium,
}

Display trait

Implementation of Display trait can be customized in the serde fashion:

use kinded::Kinded;

#[derive(Kinded)]
#[kinded(display = "snake_case")]
enum Drink {
    VeryHotBlackTea,
    Milk { fat: f64 },
}

let tea = DrinkKind::VeryHotBlackTea;
assert_eq!(tea.to_string(), "very_hot_black_tea");

The possible values are "snake_case", "camelCase", "PascalCase", "SCREAMING_SNAKE_CASE", "kebab-case", "SCREAMING-KEBAB-CASE", "Title Case", "lowercase", "UPPERCASE".

Rename variants

Individual variants can have custom display/parse names using the rename attribute. This is useful when the automatic case conversion doesn't produce the desired result:

use kinded::Kinded;

#[derive(Kinded)]
#[kinded(display = "snake_case")]
enum Validator {
    NotEmpty,
    // Without rename, this would display as "len_utf_16_min" (with extra underscore)
    #[kinded(rename = "len_utf16_min")]
    LenUtf16Min,
    #[kinded(rename = "len_utf16_max")]
    LenUtf16Max,
}

assert_eq!(ValidatorKind::NotEmpty.to_string(), "not_empty");
assert_eq!(ValidatorKind::LenUtf16Min.to_string(), "len_utf16_min");
assert_eq!(ValidatorKind::LenUtf16Max.to_string(), "len_utf16_max");

// Parsing also works with the renamed values
assert_eq!("len_utf16_min".parse::<ValidatorKind>().unwrap(), ValidatorKind::LenUtf16Min);

Note: The original variant name and its case alternatives can still be parsed (e.g., "LenUtf16Min", "len_utf_16_min").

FromStr trait

The kind type implements FromStr trait. The implementation tries it's best to parse, checking all the possible cases mentioned above.

use kinded::Kinded;

#[derive(Kinded)]
#[kinded(display = "snake_case")]
enum Drink {
    VeryHotBlackTea,
    Milk { fat: f64 },
}

assert_eq!(
    "VERY_HOT_BLACK_TEA".parse::<DrinkKind>().unwrap(),
    DrinkKind::VeryHotBlackTea
);

assert_eq!(
    "veryhotblacktea".parse::<DrinkKind>().unwrap(),
    DrinkKind::VeryHotBlackTea
);

A note about enum-kinds

There is a very similar crate enum-kinds that does almost the same job.

Here is what makes kinded different:

  • It provides Kinded trait, on top of which users can build abstractions.
  • Generates customizable implementation of Display trait.
  • Generates implementation of FromStr trait.
  • Generates kind() function to extra ergonomics.

A note about the war in Ukraine 🇺🇦

Today I live in Berlin, I have the luxury to live a physically safe life. But I am Ukrainian. The first 25 years of my life I spent in Kharkiv, the second-largest city in Ukraine, 60km away from the border with russia. Today about a third of my home city is destroyed by russians. My parents, my relatives and my friends had to survive the artillery and air attack, living for over a month in basements.

Some of them have managed to evacuate to EU. Some others are trying to live "normal lives" in Kharkiv, doing there daily duties. And some are at the front line right now, risking their lives every second to protect the rest.

I encourage you to donate to Charity foundation of Serhiy Prytula. Just pick the project you like and donate. This is one of the best-known foundations, you can watch a little documentary about it. Your contribution to the Ukrainian military force is a contribution to my calmness, so I can spend more time developing the project.

Thank you.

License

MIT © Serhii Potapov

About

Generate Rust enum variants without associated data

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 5