🔄

Zero-boilerplate struct field mapping for Rust

Skip to main content

struct_mapper/
lib.rs

1//! # struct-mapper
2//!
3//! **Derive macro to auto-generate `impl From<Source> for Target` by mapping struct fields.**
4//!
5//! Stop writing tedious manual `From` implementations for struct-to-struct conversions.
6//! `struct-mapper` generates them at compile time with **zero runtime overhead**.
7//!
8//! ---
9//!
10//! ## Overview
11//!
12//! In Rust backend development with frameworks like Axum, Actix, or Rocket, you constantly
13//! need to convert between different struct representations — database entities to API responses,
14//! form inputs to domain models, and so on. This leads to dozens of repetitive `impl From<A> for B`
15//! blocks that are tedious to write and maintain.
16//!
17//! `struct-mapper` eliminates this boilerplate with a single `#[derive(MapFrom)]` annotation.
18//!
19//! ## Quick Start
20//!
21//! ```rust
22//! use struct_mapper::MapFrom;
23//!
24//! // Your source struct (e.g., from database layer)
25//! struct UserEntity {
26//!     name: String,
27//!     email: String,
28//!     age: u32,
29//! }
30//!
31//! // Target struct — From<UserEntity> is auto-generated!
32//! #[derive(Debug, MapFrom)]
33//! #[map_from(UserEntity)]
34//! struct UserResponse {
35//!     name: String,
36//!     email: String,
37//!     age: u32,
38//! }
39//!
40//! let entity = UserEntity {
41//!     name: "Khushi".to_string(),
42//!     email: "khushi@gmail.com".to_string(),
43//!     age: 30,
44//! };
45//!
46//! // Zero-boilerplate conversion!
47//! let response: UserResponse = entity.into();
48//! assert_eq!(response.name, "Khushi");
49//! assert_eq!(response.email, "khushi@gmail.com");
50//! assert_eq!(response.age, 30);
51//! ```
52//!
53//! ---
54//!
55//! ## Feature Guide
56//!
57//! ### 1. Basic Mapping — Same Name, Same Type
58//!
59//! When source and target fields have the **same name and same type**, no extra
60//! annotation is needed. The macro maps them automatically:
61//!
62//! ```rust
63//! use struct_mapper::MapFrom;
64//!
65//! struct Source { name: String, age: u32 }
66//!
67//! #[derive(MapFrom)]
68//! #[map_from(Source)]
69//! struct Target { name: String, age: u32 }
70//!
71//! let t: Target = Source { name: "Deendayal".into(), age: 25 }.into();
72//! assert_eq!(t.name, "Deendayal");
73//! assert_eq!(t.age, 25);
74//! ```
75//!
76//! ### 2. Field Renaming — `#[map(from = "...")]`
77//!
78//! When source and target use **different field names**, use `from` to specify
79//! which source field to read from:
80//!
81//! ```rust
82//! use struct_mapper::MapFrom;
83//!
84//! struct DbRow {
85//!     user_name: String,
86//!     user_age: u32,
87//! }
88//!
89//! #[derive(MapFrom)]
90//! #[map_from(DbRow)]
91//! struct ApiUser {
92//!     #[map(from = "user_name")]
93//!     name: String,
94//!     #[map(from = "user_age")]
95//!     age: u32,
96//! }
97//!
98//! let row = DbRow { user_name: "Anjali".into(), user_age: 35 };
99//! let user: ApiUser = row.into();
100//! assert_eq!(user.name, "Anjali");
101//! assert_eq!(user.age, 35);
102//! ```
103//!
104//! ### 3. Skip + Default — `#[map(skip, default)]`
105//!
106//! For target fields that **don't exist in the source**, mark them as skipped.
107//! They will be filled with [`Default::default()`]:
108//!
109//! ```rust
110//! use struct_mapper::MapFrom;
111//!
112//! struct Entity { name: String }
113//!
114//! #[derive(MapFrom)]
115//! #[map_from(Entity)]
116//! struct Response {
117//!     name: String,
118//!     #[map(skip, default)]
119//!     request_id: String,    // → "" (String::default())
120//!     #[map(skip, default)]
121//!     retry_count: u32,      // → 0 (u32::default())
122//! }
123//!
124//! let r: Response = Entity { name: "Nikhil".into() }.into();
125//! assert_eq!(r.name, "Nikhil");
126//! assert_eq!(r.request_id, "");
127//! assert_eq!(r.retry_count, 0);
128//! ```
129//!
130//! > **Note:** `skip` always requires `default`. Using `#[map(skip)]` alone
131//! > will produce a clear compile error telling you to add `default`.
132//!
133//! ### 4. Nested Conversion — `#[map(into)]`
134//!
135//! When a source field's type implements `Into<TargetFieldType>`, use `into`
136//! to automatically call `.into()` during conversion:
137//!
138//! ```rust
139//! use struct_mapper::MapFrom;
140//!
141//! struct AddressEntity { city: String }
142//!
143//! #[derive(Debug, PartialEq, MapFrom)]
144//! #[map_from(AddressEntity)]
145//! struct AddressDTO { city: String }
146//!
147//! struct OrderEntity {
148//!     id: u64,
149//!     address: AddressEntity,
150//! }
151//!
152//! #[derive(Debug, MapFrom)]
153//! #[map_from(OrderEntity)]
154//! struct OrderDTO {
155//!     id: u64,
156//!     #[map(into)]
157//!     address: AddressDTO,  // source.address.into()
158//! }
159//!
160//! let order = OrderEntity {
161//!     id: 42,
162//!     address: AddressEntity { city: "Springfield".into() },
163//! };
164//! let dto: OrderDTO = order.into();
165//! assert_eq!(dto.id, 42);
166//! assert_eq!(dto.address.city, "Springfield");
167//! ```
168//!
169//! ### 5. Custom Function — `#[map(with = "...")]`
170//!
171//! For complex transformations, pass a **function path** that takes the source
172//! field value and returns the target field type:
173//!
174//! ```rust
175//! use struct_mapper::MapFrom;
176//!
177//! fn cents_to_dollars(cents: u64) -> f64 {
178//!     cents as f64 / 100.0
179//! }
180//!
181//! struct PriceEntity { amount_cents: u64 }
182//!
183//! #[derive(MapFrom)]
184//! #[map_from(PriceEntity)]
185//! struct PriceDTO {
186//!     #[map(from = "amount_cents", with = "cents_to_dollars")]
187//!     amount: f64,
188//! }
189//!
190//! let dto: PriceDTO = PriceEntity { amount_cents: 1999 }.into();
191//! assert!((dto.amount - 19.99).abs() < f64::EPSILON);
192//! ```
193//!
194//! ### 6. Combining Attributes
195//!
196//! All field attributes can be **combined freely**:
197//!
198//! ```rust
199//! use struct_mapper::MapFrom;
200//!
201//! fn to_upper(s: String) -> String { s.to_uppercase() }
202//!
203//! struct Source {
204//!     id: u64,
205//!     user_name: String,
206//!     raw_email: String,
207//! }
208//!
209//! #[derive(Debug, MapFrom)]
210//! #[map_from(Source)]
211//! struct Target {
212//!     id: u64,                                       // direct
213//!     #[map(from = "user_name")]
214//!     name: String,                                  // renamed
215//!     #[map(from = "raw_email", with = "to_upper")]
216//!     email: String,                                 // renamed + custom fn
217//!     #[map(skip, default)]
218//!     request_id: String,                            // skipped
219//! }
220//!
221//! let t: Target = Source {
222//!     id: 1,
223//!     user_name: "Sulochan".into(),
224//!     raw_email: "sulochan@gmail.com".into(),
225//! }.into();
226//!
227//! assert_eq!(t.id, 1);
228//! assert_eq!(t.name, "Sulochan");
229//! assert_eq!(t.email, "SULOCHAN@GMAIL.COM");
230//! assert_eq!(t.request_id, "");
231//! ```
232//!
233//! ---
234//!
235//! ## Attribute Reference
236//!
237//! ### Struct-Level
238//!
239//! | Attribute | Required | Description |
240//! |:----------|:--------:|:------------|
241//! | `#[map_from(Type)]` | **Yes** | Specifies the source type for `From<Type>` generation |
242//!
243//! ### Field-Level
244//!
245//! | Attribute | Description |
246//! |:----------|:------------|
247//! | `#[map(from = "name")]` | Map from a differently-named source field |
248//! | `#[map(skip, default)]` | Skip this field; use `Default::default()` |
249//! | `#[map(into)]` | Call `.into()` on the source field value |
250//! | `#[map(with = "path")]` | Apply a conversion function `fn(SourceFieldType) -> TargetFieldType` |
251//!
252//! Attributes can be combined: `#[map(from = "old", with = "convert")]`
253//!
254//! ---
255//!
256//! ## Error Messages
257//!
258//! `struct-mapper` provides clear, actionable compile errors:
259//!
260//! - **Missing `#[map_from]`:** Tells you exactly which attribute to add, with an example.
261//! - **`#[map(skip)]` without `default`:** Shows the fix: `#[map(skip, default)]`.
262//! - **Contradictory attributes:** Explains why `from` + `skip` can't be combined.
263//! - **Non-struct usage:** Explains that only named-field structs are supported.
264//!
265//! ---
266//!
267//! ## How It Works
268//!
269//! The `#[derive(MapFrom)]` macro:
270//!
271//! 1. **Parses** the `#[map_from(Source)]` attribute to find the source type.
272//! 2. **Inspects** each field's `#[map(...)]` attributes.
273//! 3. **Generates** an `impl From<Source> for Target` block with the correct
274//!    field assignments — direct copies, renames, `.into()` calls, or function applications.
275//!
276//! All work happens at **compile time**. The generated code is identical to what you
277//! would write by hand — zero runtime overhead, zero allocations, zero dependencies.
278//!
279//! ---
280//!
281//! ## Limitations
282//!
283//! **v0.2 constraints:**
284//!
285//! - Only named-field structs. Tuple structs and enums are not yet supported.
286//! - Generic target structs work; generic source types need manual handling.
287
288#![deny(missing_docs)]
289
290/// Derive macro that generates `impl From<Source> for Target` by mapping struct fields.
291///
292/// Place `#[derive(MapFrom)]` on your target struct along with `#[map_from(SourceType)]`
293/// to auto-generate the `From` implementation.
294///
295/// # Example
296///
297/// ```rust
298/// use struct_mapper::MapFrom;
299///
300/// struct UserEntity {
301///     name: String,
302///     email: String,
303/// }
304///
305/// #[derive(MapFrom)]
306/// #[map_from(UserEntity)]
307/// struct UserResponse {
308///     name: String,
309///     email: String,
310/// }
311///
312/// let entity = UserEntity {
313///     name: "Khushi".to_string(),
314///     email: "khushi@gmail.com".to_string(),
315/// };
316/// let response: UserResponse = entity.into();
317/// assert_eq!(response.name, "Khushi");
318/// ```
319///
320/// # Field Attributes
321///
322/// | Attribute | Description |
323/// |:----------|:------------|
324/// | `#[map(from = "name")]` | Map from a differently-named source field |
325/// | `#[map(skip, default)]` | Skip this field, use `Default::default()` |
326/// | `#[map(into)]` | Call `.into()` on the source value |
327/// | `#[map(with = "fn_path")]` | Apply a custom conversion function |
328///
329/// Attributes can be combined: `#[map(from = "old_name", with = "convert_fn")]`
330pub use struct_mapper_derive::MapFrom;
331
332/// Derive macro that generates `impl TryFrom<Source> for Target` by mapping struct fields.
333///
334/// Use this when one or more field conversions can fail. The generated implementation
335/// returns `Result<Self, MapError>`.
336///
337/// # Example
338///
339/// ```rust
340/// use struct_mapper::TryMapFrom;
341/// use std::convert::TryInto;
342///
343/// struct RawInput {
344///     count: i64,
345///     name: String,
346/// }
347///
348/// #[derive(TryMapFrom)]
349/// #[try_map_from(RawInput)]
350/// struct ValidInput {
351///     #[map(try_into)]
352///     count: u32,
353///     name: String,
354/// }
355///
356/// let raw = RawInput { count: 42, name: "Alice".into() };
357/// let valid: ValidInput = raw.try_into().unwrap();
358/// assert_eq!(valid.count, 42);
359/// assert_eq!(valid.name, "Alice");
360/// ```
361///
362/// # Field Attributes
363///
364/// All `MapFrom` attributes work here, plus:
365///
366/// | Attribute | Description |
367/// |:----------|:------------|
368/// | `#[map(try_into)]` | Call `.try_into()` on the source value (fallible) |
369/// | `#[map(try_with = "fn")]` | Apply a fallible conversion function |
370pub use struct_mapper_derive::TryMapFrom;
371
372/// Error type for fallible struct mapping via [`TryMapFrom`].
373///
374/// Contains the field name where the conversion failed and the underlying error.
375///
376/// # Example
377///
378/// ```rust
379/// use struct_mapper::{TryMapFrom, MapError};
380/// use std::convert::TryInto;
381///
382/// struct Source { value: i64 }
383///
384/// #[derive(Debug, TryMapFrom)]
385/// #[try_map_from(Source)]
386/// struct Target {
387///     #[map(try_into)]
388///     value: u32,
389/// }
390///
391/// let src = Source { value: -1 };
392/// let err = Target::try_from(src).unwrap_err();
393/// assert_eq!(err.field, "value");
394/// ```
395#[derive(Debug)]
396pub struct MapError {
397    /// The name of the field where conversion failed.
398    pub field: &'static str,
399    /// The underlying error that caused the failure.
400    pub source: Box<dyn std::error::Error + Send + Sync>,
401}
402
403impl MapError {
404    /// Create a new mapping error for a specific field.
405    pub fn field<E: std::error::Error + Send + Sync + 'static>(
406        field: &'static str,
407        error: E,
408    ) -> Self {
409        MapError {
410            field,
411            source: Box::new(error),
412        }
413    }
414}
415
416impl std::fmt::Display for MapError {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        write!(
419            f,
420            "mapping failed at field `{}`: {}",
421            self.field, self.source
422        )
423    }
424}
425
426impl std::error::Error for MapError {
427    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
428        Some(&*self.source)
429    }
430}