Expand description
§Enum Companion
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
:
[dependencies]
enum_companion = "0.1.2"
Or using cargo:
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 valueupdate(&mut self, value: {StructName}Value)
: Update a field’s valuefields() -> [{StructName}Field; N]
: Get all field enum variants as an arrayas_values(&self) -> Vec<{StructName}Value>
: Get all field values as a vector
FromStr
Implementation: The{StructName}Field
enum implementsFromStr
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
#[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.
#[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.
#[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
.
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
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:
#[derive(EnumCompanion)]
#[companion(derive_value(Debug, PartialEq))]
struct Example {
id: u32,
name: String,
}
The macro generates:
// 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: Thevalue()
method needs to clone the field values. Therefore, all fields in the struct must implement theClone
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.
Traits§
- Enum
Companion Trait - A trait for accessing and updating struct fields dynamically.