Skip to content

ddsha441981/struct-mapper

Repository files navigation

🔄 struct-mapper

Derive macro to auto-generate From<Source> and TryFrom<Source> for your structs — zero boilerplate field mapping.

CI Crates.io Docs License MSRV

📖 Documentation · 📦 Crates.io · 🐛 Report Bug · 💡 Request Feature


Stop writing tedious manual From and TryFrom implementations for struct-to-struct conversions. struct-mapper generates them at compile time with zero runtime overhead.

use struct_mapper::MapFrom;

struct UserEntity {
    name: String,
    email: String,
    age: u32,
}

#[derive(MapFrom)]
#[map_from(UserEntity)]
struct UserResponse {
    name: String,
    email: String,
    age: u32,
}

// That's it! Now you can do:
let entity = UserEntity { name: "Alice".into(), email: "a@b.com".into(), age: 30 };
let response: UserResponse = entity.into();

No runtime cost. No reflection. Just a clean impl From<> generated at compile time.


✨ Why struct-mapper?

Every Rust backend developer writes this dozens of times:

😩 Before — Manual boilerplate 🚀 After — One derive
impl From<UserEntity> for UserResponse {
    fn from(e: UserEntity) -> Self {
        UserResponse {
            name: e.name,
            email: e.email,
            age: e.age,
            display_name: e.first_name,
            address: e.address.into(),
            created_at: Default::default(),
        }
    }
}
#[derive(MapFrom)]
#[map_from(UserEntity)]
struct UserResponse {
    name: String,
    email: String,
    age: u32,
    #[map(from = "first_name")]
    display_name: String,
    #[map(into)]
    address: AddressResponse,
    #[map(skip, default)]
    created_at: String,
}

📦 Installation

Add to your Cargo.toml:

[dependencies]
struct-mapper = "0.2"

Minimum Supported Rust Version: 1.71.0


🚀 Features

1️⃣ Basic Mapping — Same Name, Same Type

Fields with matching names are mapped automatically. No attributes needed.

use struct_mapper::MapFrom;

struct Source {
    name: String,
    age: u32,
}

#[derive(MapFrom)]
#[map_from(Source)]
struct Target {
    name: String,
    age: u32,
}

let target: Target = Source { name: "Alice".into(), age: 30 }.into();
assert_eq!(target.name, "Alice");

2️⃣ Renamed Fields — #[map(from = "...")]

When source and target field names differ:

use struct_mapper::MapFrom;

struct DbRow {
    user_name: String,
    user_age: u32,
}

#[derive(MapFrom)]
#[map_from(DbRow)]
struct ApiUser {
    #[map(from = "user_name")]
    name: String,
    #[map(from = "user_age")]
    age: u32,
}

3️⃣ Skip + Default — #[map(skip, default)]

For fields that don't exist in the source struct:

use struct_mapper::MapFrom;

struct Entity {
    name: String,
}

#[derive(MapFrom)]
#[map_from(Entity)]
struct Response {
    name: String,
    #[map(skip, default)]
    request_id: String,    // → Default::default() = ""
    #[map(skip, default)]
    retry_count: u32,      // → Default::default() = 0
}

4️⃣ Nested Conversion — #[map(into)]

For fields where the source type implements Into<TargetType>:

use struct_mapper::MapFrom;

struct AddressEntity { street: String, city: String }

#[derive(MapFrom)]
#[map_from(AddressEntity)]
struct AddressDTO { street: String, city: String }

struct OrderEntity {
    id: u64,
    address: AddressEntity,
}

#[derive(MapFrom)]
#[map_from(OrderEntity)]
struct OrderDTO {
    id: u64,
    #[map(into)]
    address: AddressDTO,   // → source.address.into()
}

5️⃣ Custom Function — #[map(with = "...")]

For complex transformations using any function:

use struct_mapper::MapFrom;

fn cents_to_dollars(cents: u64) -> f64 {
    cents as f64 / 100.0
}

struct PriceEntity {
    amount_cents: u64,
}

#[derive(MapFrom)]
#[map_from(PriceEntity)]
struct PriceResponse {
    #[map(from = "amount_cents", with = "cents_to_dollars")]
    amount: f64,
}

🔗 Combine Everything

All attributes work together seamlessly:

use struct_mapper::MapFrom;

struct OrderEntity {
    order_id: u64,
    user_name: String,
    total_cents: u64,
    address: AddressEntity,
}

#[derive(MapFrom)]
#[map_from(OrderEntity)]
struct OrderResponse {
    order_id: u64,                                      // direct
    #[map(from = "user_name")]
    name: String,                                       // renamed
    #[map(from = "total_cents", with = "cents_to_dollars")]
    total: f64,                                         // renamed + custom fn
    #[map(into)]
    address: AddressDTO,                                // nested conversion
    #[map(skip, default)]
    request_id: String,                                 // skipped
}

🔄 Fallible Conversions — TryMapFrom (v0.2)

When conversions can fail (type narrowing, parsing, validation), use TryMapFrom:

use struct_mapper::TryMapFrom;
use std::num::ParseIntError;

fn parse_port(s: String) -> Result<u16, ParseIntError> {
    s.parse::<u16>()
}

struct RawConfig {
    port_str: String,
    max_conn: i64,
    host: String,
}

#[derive(TryMapFrom)]
#[try_map_from(RawConfig)]
struct ValidConfig {
    #[map(from = "port_str", try_with = "parse_port")]
    port: u16,                    // fallible: string → u16
    #[map(try_into)]
    max_conn: u32,                // fallible: i64 → u32
    host: String,                 // direct (infallible)
}

// Success:
let raw = RawConfig { port_str: "8080".into(), max_conn: 100, host: "localhost".into() };
let config: ValidConfig = raw.try_into().unwrap();

// Failure — tells you exactly which field failed:
let bad = RawConfig { port_str: "not_a_port".into(), max_conn: 100, host: "x".into() };
let err = ValidConfig::try_from(bad).unwrap_err();
assert_eq!(err.field, "port");
println!("{}", err); // "mapping failed at field `port`: invalid digit found in string"

📋 Attribute Reference

Attribute Applies To Description
#[map_from(Type)] Struct Source type to generate From<Type> for
#[try_map_from(Type)] Struct Source type to generate TryFrom<Type> for
#[map(from = "name")] Field Map from a differently-named source field
#[map(skip, default)] Field Skip this field, use Default::default()
#[map(into)] Field Call .into() on the source value
#[map(with = "fn")] Field Apply a custom conversion function
#[map(try_into)] Field Call .try_into() on the source value (TryMapFrom only)
#[map(try_with = "fn")] Field Apply a fallible function (TryMapFrom only)

💡 Tip: Attributes can be combined: #[map(from = "old_name", try_with = "parse_fn")]


🛡️ Error Messages

struct-mapper provides clear, actionable error messages that tell you exactly what went wrong and how to fix it:

error: missing `#[map_from(SourceType)]` attribute.
       Add `#[map_from(YourSourceStruct)]` to specify which struct to map from.

       Example:
         #[derive(MapFrom)]
         #[map_from(UserEntity)]
         struct UserResponse { ... }
error: `#[map(skip)]` requires `#[map(skip, default)]`.
       When skipping a field, you must provide a default value.

       Fix: #[map(skip, default)]

📊 Comparison

How does struct-mapper compare to alternatives?

Feature struct-mapper derive-into more-convert structural-convert
Same-field mapping
Field renaming ⚠️
Skip + default ⚠️ ⚠️ ⚠️
Nested .into() ⚠️
Custom function ⚠️ ⚠️
TryFrom support
Fallible custom fn
Clear error messages
Clean syntax ⚠️ ⚠️ ⚠️
Compile-time only
Zero runtime deps

🗺️ Roadmap

  • From — infallible struct conversion
  • Field renaming, skipping, nesting, custom functions
  • Clear compile-time error messages
  • TryFrom — fallible conversions (v0.2) ✅
  • Enum variant mapping (v0.3)
  • Bi-directional mapping (v0.4)

⚠️ Limitations (v0.2)

  • Only named struct fields. Tuple structs and enums are not yet supported.
  • Generics on the target struct are supported; generic source types require manual annotation.

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

👨‍💻 Author

Deendayal Kumawat

LinkedIn GitHub Email


📄 License

Licensed under either of:

at your option.


⭐ If you find this useful, please consider giving it a star! ⭐

Made with ❤️ in Rust

About

No description, website, or topics provided.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors