# Enum Companion
[](https://crates.io/crates/enum_companion) [](https://docs.rs/enum_companion/latest/enum_companion/) [](./LICENSE) [](#)
A Rust procedural macro that generates companion enums for structs, enabling dynamic field access and updates. This crate provides type-safe runtime reflection capabilities for struct fields.
> ⚠️ **Work In Progress:** This crate is currently a big WIP (Work In Progress). The API is unstable and may change significantly. Use at your own risk!
## Adding to Your Project
Add this to your `Cargo.toml`:
```toml
[dependencies]
enum_companion = "0.1.2"
```
Or using cargo:
```bash
cargo add enum_companion
```
## Macro Summary
The `#[derive(EnumCompanion)]` macro generates:
- **Field Enum** (`{StructName}Field`): An enum representing all struct fields
- **Value Enum** (`{StructName}Value`): An enum containing the typed values of each field
- **Helper Methods**:
- `value(field: {StructName}Field) -> {StructName}Value`: Get a field's value
- `update(&mut self, value: {StructName}Value)`: Update a field's value
- `fields() -> [{StructName}Field; N]`: Get all field enum variants as an array
- `as_values(&self) -> Vec<{StructName}Value>`: Get all field values as a vector
- **`FromStr` Implementation**: The `{StructName}Field` enum implements `FromStr` to allow conversion from a string.
- **`EnumCompanionTrait`**: A trait providing a generic interface to the companion methods, implemented automatically when default method names are used.
### Attributes
- `#[companion(skip)]`: Skip a field from companion enum generation
- `#[companion(rename = "NewName")]`: Rename the enum variant for a field
- `#[companion(value_fn = "custom_name")]`: Customize the value getter method name
- `#[companion(update_fn = "custom_name")]`: Customize the value setter method name
- `#[companion(fields_fn = "custom_name")]`: Customize the fields getter method name
- `#[companion(derive_field(Trait1, Trait2))]`: Add derives to the field enum
- `#[companion(derive_value(Trait1, Trait2))]`: Add derives to the value enum
- `#[companion(to_serde_field(Attribute))]`: Add Serde attributes to the field enum
- `#[companion(to_serde_value(Attribute))]`: Add Serde attributes to the value enum
## Examples
### Basic Example
```rust
# use enum_companion::{EnumCompanion, EnumCompanionTrait};
#[derive(EnumCompanion)]
#[companion(derive_field(Debug, PartialEq), derive_value(Debug, PartialEq))]
struct Person {
id: u32,
name: String,
age: u8,
}
fn main() {
let mut person = Person {
id: 1,
name: "Alice".to_string(),
age: 30,
};
// Access field values dynamically
let name_value = person.value(PersonField::Name);
assert_eq!(name_value, PersonValue::Name("Alice".to_string()));
// Update field values dynamically
person.update(PersonValue::Age(31));
assert_eq!(person.age, 31);
// Get all fields
let fields = Person::fields();
assert_eq!(fields, &[PersonField::Id, PersonField::Name, PersonField::Age]);
// Get all values as a vector
let all_values = person.as_values();
println!("All values: {:?}", all_values);
}
```
### Converting Values Back
The generated `Value` enum implements `TryFrom<Value>` for each of the underlying types. This allows you to easily convert a `Value` enum back into a concrete type.
```rust
# use enum_companion::{EnumCompanion, EnumCompanionTrait};
# use std::convert::TryInto;
#[derive(EnumCompanion)]
#[companion(derive_field(Debug, PartialEq), derive_value(Debug, PartialEq))]
struct ServerConfig {
host: String,
port: u16,
}
fn main() {
let config = ServerConfig {
host: "localhost".to_string(),
port: 8080,
};
// Get a value from the struct
let port_value = config.value(ServerConfigField::Port);
// Convert the value back into a u16
let port_u16: u16 = port_value.try_into().expect("Should be a u16");
assert_eq!(port_u16, 8080);
// Trying to convert to the wrong type will fail
let host_value = config.value(ServerConfigField::Host);
let host_res: Result<u16, _> = host_value.try_into();
assert!(host_res.is_err());
}
```
### Creating Values from Tuples
You can also create a `Value` enum from a tuple of `(Field, InnerValue)`, which can be useful for constructing values dynamically.
```rust
# use enum_companion::{EnumCompanion, EnumCompanionTrait};
# use std::convert::TryInto;
#[derive(EnumCompanion)]
#[companion(derive_field(Debug, PartialEq), derive_value(Debug, PartialEq))]
struct MyStruct {
id: u32,
name: String,
}
fn main() {
let name_tuple = (MyStructField::Name, "Example".to_string());
let name_value: MyStructValue = name_tuple.try_into().unwrap();
assert_eq!(name_value, MyStructValue::Name("Example".to_string()));
// This would fail if the inner value type does not match the field.
let id_tuple_fail = (MyStructField::Name, 42u32);
let id_res: Result<MyStructValue, _> = id_tuple_fail.try_into();
assert!(id_res.is_err());
}
```
> **Limitation**: Due to Rust's orphan rule, `TryFrom` is not implemented for fields that are generic or contain generic types.
### The `EnumCompanionTrait`
When you use the default method names (`value`, `update`, `fields`), the macro will also implement the `enum_companion::EnumCompanionTrait` for your struct. This trait provides a generic way to interact with any struct that uses `EnumCompanion`.
```rust
use enum_companion::{EnumCompanion, EnumCompanionTrait};
#[derive(EnumCompanion)]
#[companion(derive_field(Debug), derive_value(Debug))]
struct MyStruct {
foo: i32,
bar: String,
}
fn process_any_companion<T, F, V>(companion: &T)
where
T: EnumCompanionTrait<F, V>,
F: Copy + std::fmt::Debug + 'static,
V: std::fmt::Debug,
{
println!("Processing fields...");
for &field in T::fields() {
let value = companion.value(field);
println!(" {:?}: {:?}", field, value);
}
}
fn main() {
let my_struct = MyStruct { foo: 42, bar: "hello".to_string() };
process_any_companion(&my_struct);
}
```
### Full Example with Attributes
```rust
use enum_companion::{EnumCompanion, EnumCompanionTrait};
use serde::{Serialize, Deserialize};
#[derive(EnumCompanion)]
#[companion(
value_fn = "get_field",
update_fn = "set_field",
fields_fn = "get_all_fields",
derive_field(Hash, Eq, PartialEq, Debug, Serialize, Deserialize),
derive_value(Serialize, Deserialize, Debug, PartialEq),
serde_field(rename_all = "camelCase"),
serde_value(rename_all = "camelCase", tag = "type", content = "value")
)]
struct UserProfile {
#[companion(rename = "UserId")]
id: u64,
#[companion(rename = "DisplayName")]
username: String,
email: String,
#[companion(skip)]
password_hash: String, // This field won't appear in companion enums
age: Option<u8>,
is_verified: bool,
}
let mut profile = UserProfile {
id: 12345,
username: "alice_dev".to_string(),
email: "alice@example.com".to_string(),
password_hash: "secret_hash".to_string(),
age: Some(28),
is_verified: true,
};
// Use custom method names
let user_id = profile.get_field(UserProfileField::UserId);
assert_eq!(user_id, UserProfileValue::UserId(12345));
// Update using custom method
profile.set_field(UserProfileValue::DisplayName("alice_developer".to_string()));
assert_eq!(profile.username, "alice_developer");
// The password_hash field is skipped, so it doesn't appear in enums
let fields = UserProfile::get_all_fields();
assert_eq!(
fields,
&[
UserProfileField::UserId,
UserProfileField::DisplayName,
UserProfileField::Email,
UserProfileField::Age,
UserProfileField::IsVerified
]
);
// Work with optional fields
profile.set_field(UserProfileValue::Age(None));
assert_eq!(profile.age, None);
// Serialize/deserialize the values (if serde feature is enabled)
let all_values = profile.as_values();
for value in all_values {
let serialized = serde_json::to_string(&value).unwrap();
println!("Field value: {}", serialized);
if let UserProfileValue::UserId(_) = value {
assert_eq!(serialized, r#"{"type":"userId","value":12345}"#);
}
}
```
## Generated Code
For a basic struct like:
```rust,ignore
#[derive(EnumCompanion)]
#[companion(derive_value(Debug, PartialEq))]
struct Example {
id: u32,
name: String,
}
```
The macro generates:
```rust,ignore
// Recursive expansion of EnumCompanion macro
// ===========================================
#[doc = r" An enum representing the fields of the struct."]
#[allow(dead_code)]
#[derive(Copy, Clone)]
enum ExampleField {
Id,
Name,
}
impl ExampleField {
pub const FIELDS: &'static [ExampleField] = &[ExampleField::Id, ExampleField::Name];
}
#[doc = r" An enum representing the values of the struct's fields."]
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
enum ExampleValue {
Id(u32),
Name(String),
}
impl std::str::FromStr for ExampleField {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"id" | "Id" => Ok(Self::Id),
"name" | "Name" => Ok(Self::Name),
_ => Err(format!("Invalid field name: {}", s)),
}
}
}
impl Example {
#[doc = r" Returns an array of all field enum variants."]
pub fn fields() -> &'static [ExampleField] {
ExampleField::FIELDS
}
#[doc = r" Returns a vector of all field values."]
pub fn as_values(&self) -> Vec<ExampleValue> {
Self::fields()
.iter()
.map(|&field| self.value(field))
.collect()
}
#[doc = r" Returns the value of a specific field."]
pub fn value(&self, field: ExampleField) -> ExampleValue {
match field {
ExampleField::Id => ExampleValue::Id(self.id.clone()),
ExampleField::Name => ExampleValue::Name(self.name.clone()),
}
}
#[doc = r" Updates the value of a specific field."]
pub fn update(&mut self, value: ExampleValue) {
match value {
ExampleValue::Id(value) => self.id = value,
ExampleValue::Name(value) => self.name = value,
}
}
}
impl enum_companion_trait::EnumCompanionTrait<ExampleField, ExampleValue> for Example {
fn value(&self, field: ExampleField) -> ExampleValue {
self.value(field)
}
fn update(&mut self, value: ExampleValue) {
self.update(value)
}
fn fields() -> &'static [ExampleField] {
&ExampleField::FIELDS
}
fn as_values(&self) -> Vec<ExampleValue> {
self.as_values()
}
}
```
## Use Cases
- **Dynamic forms**: Build forms that can handle any struct type
- **Serialization helpers**: Generic serialization without knowing field types at compile time
- **Configuration management**: Update struct fields from external configuration
- **API endpoints**: Generic CRUD operations over struct fields
- **Testing utilities**: Compare and manipulate struct fields generically
## Limitations
- **`Clone` Requirement**: The `value()` method needs to clone the field values. Therefore, all fields in the struct must implement the `Clone` trait.
- **Named Structs Only**: The macro can only be used on structs with named fields (e.g., `struct MyStruct { id: u32 }`). It does not support tuple structs or unit structs.
## License
This project is licensed under the MIT License - see the LICENSE file for details.