#sealed #convert #cast #any #from

macro struct-variant

Minimal helper macro to generate an enum out of a list of structs

1 stable release

1.0.2 May 15, 2021

#2797 in Rust patterns

28 downloads per month

MIT license

16KB
261 lines

Crates Docs License Build

Minimal helper macro to generate an enum out of a list of structs.

60 Seconds Example

Install:

cargo add struct-variant

Setup:

// Common trait.
trait Event {
	fn apply(&self);
};

// First trait implementation.
struct MouseEvent {
	// ...
}
impl Event for MouseEvent {
	fn apply(&self) {
		println!("Applied mouse");
	}
}

// Second trait implementation.
struct KeyboardEvent {
	// ...
}
impl Event for KeyboardEvent {
	fn apply(&self) {
		println!("Applied keyboard");
	}
}

// The *magic*!
#[struct_variant(Event)]
enum EventEnum {
	MouseEvent,
	KeyboardEvent,
}

Result:

fn process_event(event: EventEnum) {
	// No need to match to get inner type.
	// AsRef<Event> implemented for you.
	event.as_ref().apply();

	// But you can still match if you want to.
	match event {
		EventEnum::MouseEvent(_) => println!("Got mouse"),
		EventEnum::KeyboardEvent(_) => println!("Got keyboard"),
	}
}

fn main() {
	// From<MouseEvent> for EventEnum implemented for you.
	let mouse_event = MouseEvent {};
	process_event(mouse_event.into())
}

Detailed Motivation

Suppose you have a trait Shape which is implemented by a finite amount of structs:

trait Shape {
	fn area(&self) -> usize;
}
struct Circle { ... }
struct Rectangle { ... };

Occasionally we may want to pass around a dynamic Shape type with the ability to downcast. std::any::Any would work occasionally, but it does not work with all scenarios (see the official Rust docs for limitations). We can pass a &dyn Shape around but you can only call common trait methods. To get around both of those issues, we can create an enum that holds all of our variants:

enum ShapeEnum {
	Circle(Circle),
	Rectangle(Rectangle),
}

Now, instead of passing a dyn Shape, we can just pass the ShapeEnum and use the match keyword for downcasting. The problem with this approach is twofold:

  1. Each variant implements Shape so why doesn't ShapeEnum implement all methods from Shape?
  2. Each struct only has 1 enum discriminant so why can't we automatically convert any Shape to ShapeEnum.?
  3. There's additional boilerplate. We can solve #1 and #2 by implementing traits but that's a lot of additional code.

This library helps with all of those issues:

#[struct_variant(Shape)]
enum ShapeEnum {
	Circle,
	Rectangle,
}

The Shape in the macro attribute indicates that all structs in the enum implement Shape. You can use std::convert::AsRef to cast your type to any type listed in the macro attribute. This solves issue #1. Now we can use this more conveniently:

fn print_shape(shape: ShapeEnum) {
	// We can use pattern matching to downcast.
	let name = match shape {
		ShapeEnum::Circle(_) => "Circle",
		ShapeEnum::Rectangle(_) => "Rectangle",
	};

	// AsRef<dyn Shape> is implemented for you.
	println!("Shape: {}, Area: {}", name, shape.as_ref().area());
}

std::convert::From is implemented for each struct so you can also upcast. This solves problem #2:

fn print_area(shape: &dyn Shape) {
	println!("Area: {}", shape.area());
}

let circle: ShapeEnum = Circle::with_radius(2).into();
print_area(circle.as_ref());

Sealed Types

In the earlier examples, ShapeEnum includes two implementations of Shape: Circle and Rectangle. A type that implements all subtypes of a base type is known as a sealed type in other languages. While this library can't guarantee that you include all subtypes in your enum, with due diligence you can write libraries that do. The sealed trait design pattern ensures that consumers of your library cannot add new subtypes and thus in conjunction with this library, you can ensure you are creating sealed types.

Trait Bounds

Earlier examples created a ShapeEnum type over the Shape trait. The macro attribute allows multiple trait bounds:

#[struct_variant(Shape + Debug)]
enum ShapeEnumWithDebug {
	Circle,
	Rectangle,
}

fn print_debug(debug: &dyn Debug) {
	println!("Debug: {:?}", debug);
}

let circle: ShapeEnumWithDebug = Circle::with_radius(2).into();
print_debug(circle.as_ref());
println!("Manual: {:?}", AsRef::<dyn Debug>::as_ref(&circle));

Or you can also opt for no trait bounds at all. This means you'd get no std::convert::AsRef implementations but you'd still benefit from the std::convert::From implementations:

#[struct_variant]
enum ShapeEnumNoBounds {
	Circle,
	Rectangle,
}

Name Conflicts

One source of contention with this library are structs with conflicting names. Suppose we have two different Foo in both the bar and baz namespaces. One simple way to use them with this library is using as when importing one or both of them:

use bar::Foo as FooBar;
use baz::Foo;

#[struct_variant]
enum Qux {
	FooBar,
	Foo
}

An alternative is using the standard enum syntax to declare which struct it uses:

use baz::Foo;

#[struct_variant]
enum Qux {
	FooBar(bar::Foo),
	Foo
}

The downside with both approaches is that you have to use the re-exported name during pattern matching.

We can use the same solution for supporting multiple variants of the same generic type:

struct Foo<X> {
	phantom: PhantomType<X>,
};

enum Qux {
	FooU8(Foo<u8>),
	FooI8(Foo<i8>),
}

Contributing

Contributions are welcome and highly appreciated!

Please run the formatter, linter and unit tests first before making a pull request:

cargo +nightly fmt
cargo clippy
cargo test

Building Docs

This library uses an unstable feature to build docs from the README file:

cargo +nightly doc --open --features doc

Dependencies

~2MB
~46K SLoC