A Rust derive macro for easily creating conversions between structs and enums.
- Automate conversions between similar data structures
- Support for struct-to-struct, tuple struct, and enum conversions
- Field renaming capabilities
- Automatic handling of wrapped types with
From/Intoimplementations - Special handling for
Option,Vec, andHashMaptypes, including recursive nested containers - Support for both infallible (
From/Into) and fallible (TryFrom) conversions - Fine-grained control with field-level attributes
- Support for nested type conversions
- HashMap conversion with key and value type conversions
- Custom conversion functions with the
with_funcattribute
Add to your Cargo.toml:
[dependencies]
derive-into = "0.1.0"use derive_into::Convert;
// Source struct with conversion attributes
#[derive(Convert)]
#[convert(into(path = "Destination"))] // Generate Into<Destination> implementation
struct Source {
id: u32,
#[convert(rename = "full_name")] // Field will be mapped to "full_name" in target
name: String,
}
// Destination struct
struct Destination {
id: u32,
full_name: String,
}
// Usage
let source = Source {
id: 1,
name: "Example".to_string(),
};
let destination: Destination = source.into();Struct-level attributes can be applied at the struct or enum level to control conversion behavior:
| Attribute | Description |
|---|---|
#[convert(into(path = "Type"))] |
Generate an From<Self> for Type implementation |
#[convert(try_into(path = "Type"))] |
Generate an TryFrom<Self> for Type implementation |
#[convert(try_from(path = "Type"))] |
Generate a TryFrom<Type> for Self implementation |
#[convert(from(path = "Type"))] |
Generate a From<Type> for Self implementation |
#[convert(into(path = "Type", default))] |
Enable default values for fields not explicitly mapped in the target type |
#[convert(try_from(path = "Type", validate = "func"))] |
Call a validation function on the source before conversion. Only works with fallible conversions (try_from/try_into) |
#[convert(into(path = "Type", wrap_unit))] |
For enums: every unit variant on this side maps to a tuple variant Variant(Default::default()) on the other side. Useful for prost-generated proto oneof enums (Land(()), VtolCalibrateAirspeed(super::VtolCalibrateAirspeed), ...). The wrapped value uses Default::default(), so it works with both () and any prost Message struct |
Multiple conversion types can be specified for a single struct:
#[derive(Convert)]
#[convert(into(path = "TargetType"))]
#[convert(try_from(path = "TargetType"))]
struct MyStruct {
// fields
}| #[convert(try_from(path = "Type"))] | Specify a path for try_from conversion |
Field-level attributes can be applied at three different scopes:
-
Global scope - applies to all conversion types:
#[convert(rename = "new_name", skip)] -
Conversion type scope - applies only to a specific conversion type (into, from, try_from):
#[convert(try_from(skip, default))] -
Specific conversion scope - applies only to a singular conversion target:
#[convert(try_from(path = "ApiProduct", skip, default))]
Common field-level attributes:
| Attribute | Description |
|---|---|
#[convert(rename = "new_name")] |
Map this field to a differently named field in the target type |
#[convert(unwrap_or_default)] |
Automatically calls unwrap_or_default on Option value before converting it |
#[convert(unwrap)] |
Automatically unwrap an Option value (fails in try_from if None) |
#[convert(skip)] |
Skip this field during conversion (target must provide a default) |
#[convert(default)] |
Use default value for this field during conversion |
#[convert(with_func = func_name)] |
Use custom function for conversion. The function needs to take a reference to the parent struct |
The macro supports enum-to-enum conversion with similar attribute control:
#[derive(Convert)]
#[convert(into(path = "TargetEnum"))]
enum SourceEnum {
Variant1(u32),
#[convert(rename = "RenamedVariant")]
Variant2 {
value: String,
#[convert(rename = "renamed_field")]
field: u8,
},
Unit,
// Unit variant on this side maps to a tuple variant `Unit2(())` on the
// other side (e.g. prost-generated proto oneof enums).
#[convert(wrap_unit)]
Unit2,
}
enum TargetEnum {
Variant1(u32),
RenamedVariant {
value: String,
renamed_field: u32,
},
Unit,
Unit2(()),
}Variant-level attributes:
| Attribute | Description |
|---|---|
#[convert(rename = "Other")] |
Map this variant to a different name on the other side |
#[convert(skip)] |
Skip this variant entirely |
#[convert(wrap_unit)] |
The unit variant on the deriving enum corresponds to a tuple variant Variant(Default::default()) on the other side (e.g. Variant(()) or Variant(SomeProstMessage)). Can also be set at the enum level via #[convert(into(path = "...", wrap_unit))] to apply to all unit variants at once |
The macro intelligently handles various type scenarios:
- Direct Mapping: Fields with identical types are directly copied
- Automatic Conversion: Fields with types that implement
From/Intoare automatically converted - Container Types: Special handling for
Option<T>,Vec<T>, andHashMap<K, V>with inner type conversion - Recursive Container Conversion: Nested containers like
Option<Vec<T>>,Vec<Option<T>>,HashMap<K, Vec<V>>,Option<HashMap<K, V>>, etc. are converted recursively — inner types are converted at every nesting level - Tuple Structs: Support for conversions between tuple structs
- Nested Type Conversions: Automatically handles nested struct and enum conversions
use derive_into::Convert;
#[derive(Convert)]
#[convert(into(path = "Target"))]
struct Source {
id: u32,
name: String,
}
struct Target {
id: u32,
name: String,
}
// Usage
let source = Source { id: 1, name: "Example".to_string() };
let target: Target = source.into();The macro automatically handles conversion of inner types for Option and Vec:
use derive_into::Convert;
#[derive(Debug, PartialEq, Default)]
struct Number(u8);
impl From<u8> for Number {
fn from(n: u8) -> Number {
Number(n)
}
}
#[derive(Convert)]
#[convert(into = "Target")]
struct Source {
// Option's inner type will be converted
opt_value: Option<u8>,
// Vec's inner type will be converted
vec_values: Vec<u8>,
}
struct Target {
opt_value: Option<Number>,
vec_values: Vec<Number>,
}Container types are converted recursively at every nesting level. This means types like Option<Vec<T>>, Vec<Option<T>>, Vec<Vec<T>>, HashMap<K, Vec<V>>, and any arbitrary nesting depth just work — inner types are automatically converted using their From/Into/TryFrom/TryInto implementations.
use derive_into::Convert;
use std::collections::HashMap;
#[derive(Debug, PartialEq)]
struct Tag(String);
impl From<String> for Tag {
fn from(s: String) -> Self { Tag(s) }
}
#[derive(Debug, PartialEq)]
struct Score(u32);
impl From<u32> for Score {
fn from(n: u32) -> Self { Score(n) }
}
#[derive(Convert)]
#[convert(into(path = "Target"))]
struct Source {
// Option<Vec<T>> — both layers are handled
tags: Option<Vec<String>>,
// Vec<Option<T>>
scores: Vec<Option<u32>>,
// HashMap with Vec values
grouped: HashMap<String, Vec<u32>>,
}
struct Target {
tags: Option<Vec<Tag>>,
scores: Vec<Option<Score>>,
grouped: HashMap<String, Vec<Score>>,
}use derive_into::Convert;
#[derive(Convert)]
#[convert(try_from(path = "Source"))]
struct Target {
#[convert(unwrap_or_default)]
value: u32,
}
struct Source {
value: Option<u32>,
}
// This will succeed
let source = Source { value: None };
let target: Result<Target, _> = Target::try_from(source);
assert!(target.is_ok());
// This will fail because value is None
let source = Source { value: None };
let target: Result<Target, _> = Target::try_from(source);
assert!(target.is_err());use derive_into::Convert;
#[derive(Convert)]
#[convert(try_from(path = "Source"))]
struct Target {
#[convert(unwrap)]
value: u32,
}
struct Source {
value: Option<u32>,
}
// This will succeed
let source = Source { value: Some(42) };
let target: Result<Target, _> = Target::try_from(source);
assert!(target.is_ok());
// This will fail because value is None
let source = Source { value: None };
let target: Result<Target, _> = Target::try_from(source);
assert!(target.is_err());use derive_into::Convert;
#[derive(Convert)]
#[convert(into(path = "Target", default))]
struct Source {
id: u32,
// No 'extra' field - will use default
}
#[derive(Default)]
struct Target {
id: u32,
extra: String, // Will use Default::default()
}More examples
use derive_into::Convert;
#[derive(Convert)]
#[convert(into(path = "Target"))]
struct Source(Option<u8>, u8);
struct Target(Option<Number>, Number);use derive_into::Convert;
use std::collections::HashMap;
#[derive(Convert)]
#[convert(into(path = "ApiProduct", default))]
#[convert(try_from(path = "ApiProduct"))]
struct Product {
id: String,
name: NonEmptyString,
// Vector of complex types with renamed field
#[convert(rename = "variants")]
product_variants: Vec<ProductVariant>,
// HashMap with key/value type conversion
#[convert(rename = "price_by_region")]
regional_prices: HashMap<String, f64>,
// Nested struct with its own conversion
manufacturer: Manufacturer,
// Field that will be skipped only during into conversion
#[convert(into(skip))]
internal_tracking_code: String,
// Field that uses default value only during try_from conversion
#[convert(try_from(default))]
sku: String,
// Field that uses custom conversion function
#[convert(try_from(with_func = conversion_func))]
product_err: ProductError,
}
// Custom conversion function
fn conversion_func(val: &ApiProduct) -> ProductError {
ProductError {
message: if val.name.is_empty() {
"Name cannot be empty".to_string()
} else {
"Valid name".to_string()
},
}
}use derive_into::Convert;
#[derive(Convert)]
#[convert(into(path = "TargetEvent"))]
#[convert(try_from(path = "TargetEvent"))]
enum SourceEvent {
// Tuple variant with type conversion
Click(u64),
// Variant with renamed variant name
#[convert(rename = "LogoutEvent")]
Logout {
username: String,
timestamp: u64,
},
// Variant with nested enum conversion
UserAction {
user_id: u64,
action_type: SourceActionType,
},
}
enum TargetEvent {
// Type conversion in tuple variant
Click(CustomId),
// Renamed variant
LogoutEvent {
username: String,
timestamp: CustomId,
},
// Nested enum conversion
UserAction {
user_id: CustomId,
action_type: TargetActionType,
},
}You can validate the source struct before conversion using the validate attribute. The validation function receives a reference to the source and returns a Result<(), String>. If validation fails, the conversion returns an error.
use derive_into::Convert;
fn validate_source(source: &Source) -> Result<(), String> {
if source.name.is_empty() {
return Err("name must not be empty".into());
}
Ok(())
}
#[derive(Convert)]
#[convert(try_from(path = "Source", validate = "validate_source"))]
struct Target {
name: String,
}
struct Source {
name: String,
}
// Validation passes
let source = Source { name: "hello".into() };
let target: Result<Target, _> = source.try_into();
assert!(target.is_ok());
// Validation fails
let source = Source { name: "".into() };
let target: Result<Target, _> = source.try_into();
assert!(target.is_err());use derive_into::Convert;
#[derive(Convert)]
#[convert(try_from(path = "ApiProduct"))]
struct Product {
// Field that requires custom conversion
#[convert(try_from(with_func = validation_function))]
validated_field: SomeType,
}
// Custom conversion function
fn validation_function(source: &ApiProduct) -> SomeType {
// Custom conversion/validation logic
SomeType::new(source.some_field.clone())
}This project is licensed under the MIT License - see the LICENSE file for details.