A set of traits and macros for defining a common api for Rust enums based on the std::variant api in the C++ standard library
Consider the following enum:
rust
enum Enum {
F1(i32),
F2(bool)
}
We of course may use such an enum directly using the field names and match statements.
The traits provided in this crate allow one to perform that same access of inner values without
explicit use of tags.
This is modelled on the api for the C++ type std::variant which is like Rust enums but without explicit
names for each possible active field. Consider the following example:
rust
let instance = Enum::F1(42);
if instance.has_variant::<i32>() && instance.contains_variant::<i32>().unwrap() {
let inner: &i32 = instance.get_variant().unwrap();
...
}
The above code first checks that instance has a field of type i32
, then checks that this is the active field,
and then gets a reference to the raw value contained therein.
In general, the traits provided in this crate give the following functionality to enums: ```rust let mut instance = ...;
// determines whether on of the possible fields has type T
let result: bool = instance.has_variant::
// determines whether or not the active field has type T.
// If not field has type T, returns Err
let result: Result
// retrieves a reference to the raw value of the field of type T. If the // active field is not of type T, getvariant returns Err causing the following // code to panic. If no field has type T, the following will not compile. let inner: &T = instance.getvariant().unwrap();
// retrieves a mutable reference to the raw value of the field of type T. If the // active field is not of type T, getvariant returns Err causing the following // code to panic. If no field has type T, the following will not compile. let inner: &mut T = instance.getvariant_mut().unwrap();
// If instance has a field of type bool, this becomes the active field with
// value of false
. Otherwise, this will not compile.
instance.set_variant(false);
// Since instance can have multiple number-like fields, the following can be
// used to enforce that the field of type i64 is set (if it exists).
// Otherwise the outcome will be ambiguous to the user.
instance.setvariant(3 as i64);
For enum types (subject to certain restrictions detailed in the Type Requirements section below), these traits can be
derived using the `derive_variant_access` macro. This macro derives all the traits in this crate.
rust
use variantaccess_derive::*;
enum Enum { F1(i32), F2(bool) } ```
Several restrictions apply in order for this macro to succeed. First of all, it can only be applied to enums. Secondly, each field must have a unique type. If any field of the enum itself has more than one field or any named fields, the macro will not work (this may be expanded in the future). If any of these conditions are not met, the code will not compile.
Out of the box, accessing the active fields in a Rust enum requires direct use of the tags used for the active field. This is problematic in cases where a more uniform interface for variant / union like types is needed. A prime example is computer generated code.
For types that are auto-generated, it is difficult to support union types as one needs to generate an enum whose name and field tags will also be auto-generated and thus opaque to any user. Thus a uniform interface that does not require knowledge of field names allows the use of such auto-generated types, without being over burdensome to the end user.
As an example, code-generated from protobuf schemas by default make all inner values private and provide getters and setters to uniformize interaction with these entities.
The derive macro is able to fully distinguish types, even those with the same name but in different modules. The full
namespace resolution is achieved by using std::any::TypeId
. For example
```rust
pub struct Complex { fieldone: bool, fieldtwo: f64 }
pub mod namespace {
#[derive(Debug, PartialEq)]
pub struct Complex {
pub field_one: bool,
pub field_two: f64
}
#[derive(VariantAccess, PartialEq, Debug)]
pub enum ComplexEnum {
F1(Complex),
F2(super::Complex)
}
}
works and the various trait methods can distinguish between the type
Complexand
namespace::Complex```.
Generics are also supported. For example ```rust
pub struct Test
pub enum Enum
works and means that the instantiated trait methods will automatically work, e.g., for the
the type
Enum. So for example,
rust
fn main() {
let test = Enum::
As such, in the above example, the following module and marker structs will also be created: ```rust
mod variantaccessEnum {
pub (crate) struct F1;
pub (crate) struct F2;
}
So beware in case you were thinking of creating the module
variantaccessEnum``` yourself! :stuckouttongueclosedeyes:
We also provide a trait and function for creating instance of variants given a value of a certain type. Consider the following example: ```rust use variantaccesstraits::; use variant_access_derive::;
#[(VariantAccessDerive)] enum HorribleComputerGeneratedEnumName { AwfulComputerGeneratedField1(f64), AwfulComputerGeneratedField2(bool) }
struct LovelyStruct { lovelyfieldname: HorribleComputerGeneratedEnumName }
fn main() { let lovely = LovelyStruct{lovelyfieldname: createvariantfrom(3.0)}; }
``
The
createvariantfromfunction is able to deduce that since
lovelyfieldnameis of type
HorribleComputerGeneratedEnumName
and the input to the function is an
f64, that it should return
HorribleComputerGeneratedEnumName::AwfulComputerGeneratedField1(3.0)`.
This example goes back to the original motivation of this crate.
There are several requirements that your enum definition must satisfy in order for the traits and / or the
derive macro to work. First of all, all types must subscribe to 'static
. This is a requirement of std::any::TypeId
(as apparently it is difficult to distinguish to types that differ only in lifetime). Furthermore, this a trait bound
for some of the variant_access traits.
This also means that when using generics in your enum definition, you must add the 'static
trait bound (see the
example in the previous section).
For the derive macro to work, it is also necessary that all field types of the enum implement the PartialEq
and Debug
traits.
For a more complete list of restrictions and misuses, see the uncompilable_examples
subdirectory in the tests
folder.
Currently, the only main known issue involves running the test suite for this crate. cargo test
fails due to an
issue in trybuild
and Rust workspaces. I have been unable to resolve it, but running the tests one by one
via cargo test --package variant_access --test tests {{test name}}
ensures that all are passing.