Expand description
§Detailed Documentation
The DtoFrom derive macro generates a From<Source> implementation for the target struct.
It supports the following attributes on structs and fields:
§Struct-level Attribute (required)
#[dto(from = Type)]- Specifies the source type
Type(a RustPath) from which to map. - Must appear on the same item as
#[derive(DtoFrom)].
- Specifies the source type
§Field-level Attributes
#[dto(rename = "orig_name")]- Maps the struct field to a differently named source field (by name).
#[dto(transform_fn = path::to::function)]- Applies the function
path::to::function(source_field)to transform the input. - The function must have the signature
FnOnce(SourceFieldType) -> FieldType.
- Applies the function
#[dto(skip)]- Omits this field from the mapping; the field is initialized with
Default::default().
- Omits this field from the mapping; the field is initialized with
#[dto(into)]- Uses
Intoto convert the source field into the DTO field type, i.e.source_field.into().
- Uses
§Usage Example
use simple_dto_mapper_derive::DtoFrom;
mod types {
#[derive(Debug)]
pub struct User {
pub id: String,
pub name: String,
pub age: u32,
pub password: String, // intentionally not mapped into the DTO
pub status: SourceStatus,
}
#[derive(Debug, Clone)]
pub enum SourceStatus { Active, Inactive }
#[derive(Debug, Clone)]
pub enum DtoStatus { Active, Inactive }
// Infallible enum conversion for `#[dto(into)]`
impl From<SourceStatus> for DtoStatus {
fn from(s: SourceStatus) -> Self {
match s {
SourceStatus::Active => DtoStatus::Active,
SourceStatus::Inactive => DtoStatus::Inactive,
}
}
}
// Owned transform used by `transform_fn`
pub fn mask_name(name: String) -> String {
let ch = name.chars().next().unwrap_or('*');
format!("{ch}***")
}
}
#[derive(DtoFrom, Debug)]
#[dto(from = types::User)]
struct UserDto {
/// `rename` + `transform_fn`: read from `name` and apply `types::mask_name`
#[dto(rename = "name", transform_fn = types::mask_name)]
display_name: String,
/// Direct mapping
age: u32,
/// `skip`: initialize with `Default::default()`
#[dto(skip)]
note: Option<String>,
/// `into`: uses `From<SourceStatus> for DtoStatus`
#[dto(into)]
status: types::DtoStatus,
}
// Convert
let user = types::User {
id: "u1".into(),
name: "Alice".into(),
age: 30,
password: "secret".into(),
status: types::SourceStatus::Active,
};
let dto: UserDto = user.into();§Additional Example: Collections
Converting collections and options is handled explicitly with transform_fn helpers.
This keeps behavior clear without extra macro features.
use simple_dto_mapper_derive::DtoFrom;
use chrono::{DateTime, Utc};
mod types {
#[derive(Debug)]
pub struct SourceTag(pub String);
#[derive(Debug)]
pub struct SourceAuthor { pub name: String }
#[derive(Debug)]
pub struct Article {
pub id: String,
pub title: String,
pub labels: Vec<SourceTag>, // -> Vec<DtoTag>
pub keywords: Vec<String>, // same type
pub author: Option<SourceAuthor>, // -> Option<DtoAuthor>
pub published_at: Option<chrono::DateTime<chrono::Utc>>, // -> Option<DateTime<Utc>>
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DtoTag(pub String);
impl From<SourceTag> for DtoTag { fn from(t: SourceTag) -> Self { DtoTag(t.0) } }
#[derive(Debug, Clone)]
pub struct DtoAuthor { pub name: String }
impl From<SourceAuthor> for DtoAuthor { fn from(a: SourceAuthor) -> Self { DtoAuthor { name: a.name } } }
// Helpers for transform_fn
pub fn vec_into<T, U>(v: Vec<T>) -> Vec<U> where U: From<T> {
v.into_iter().map(Into::into).collect()
}
pub fn opt_into<T, U>(o: Option<T>) -> Option<U> where U: From<T> {
o.map(Into::into)
}
}
#[derive(DtoFrom, Debug)]
#[dto(from = types::Article)]
struct ArticleDto {
/// Direct mapping (same type)
id: String,
/// Rename + direct mapping
#[dto(rename = "title")]
headline: String,
/// Same type; no transform needed
keywords: Vec<String>,
/// Vec<SourceTag> → Vec<DtoTag>
#[dto(rename = "labels", transform_fn = types::vec_into::<types::SourceTag, types::DtoTag>)]
tags: Vec<types::DtoTag>,
/// Option<SourceAuthor> → Option<DtoAuthor>
#[dto(transform_fn = types::opt_into::<types::SourceAuthor, types::DtoAuthor>)]
author: Option<types::DtoAuthor>,
published_at: Option<DateTime<Utc>>,
}§Error Messages
The derive macro produces clear, span-accurate diagnostics for common mistakes:
- Missing struct attribute:
#[dto(from = Type)]. - Unsupported item shapes: only named-field structs are supported (tuple/unit structs and enums are rejected).
- Unknown field attribute keys: reports the unknown key and the allowed set (
rename,transform_fn,skip,into). - Duplicate attributes on a field:
rename,transform_fn,skip, orintorepeated. - Conflicting attributes on a field:
skipcannot appear with any other attribute;transform_fnconflicts withinto. - Invalid
renamevalue: empty string is rejected. - Unknown/duplicate struct-level keys: only
fromis allowed at the struct level.
See tests/ui for compile-fail cases that exercise each diagnostic.
§Limitations
- Named-field structs only: tuple/unit structs and enums are not supported.
- Structs only: traits/unions/enums cannot derive
DtoFrom. - Owned-only mapping: generates
impl From<Source> for Target(no zero-copy/by-ref mode). transform_fnsignature: must beFnOnce(SourceFieldType) -> FieldType(owned input, owned output).intorequiresFrom:From<SourceFieldType> for FieldTypemust exist.skiprequiresDefault: the target field type must implementDefault.- No automatic element mapping: collections/options do not map inner elements automatically; use
transform_fn. - No
auto_into/try_into: conversions are explicit per-field with#[dto(into)]. - Field existence is validated by the compiler: a missing/renamed source field causes a compile error at the attribute span.
§Mapping Rules (at a glance)
-
Default (owned move)
- Same name & same type →
target = source.field - “Compatible type” means:
- Identical type, or
#[dto(into)]whereFrom<SourceFieldType> for FieldTypeexists, or#[dto(transform_fn = ...)]provides an explicit conversion
- Same name & same type →
-
Field attributes
-
#[dto(rename = "orig_name")]
Reads from a different source field name (type must still be compatible). -
#[dto(transform_fn = path::to::function)]
Callsfunction(source.orig_name)before assignment.
Signature:FnOnce(SourceFieldType) -> FieldType. -
#[dto(skip)]
Skips mapping; initializes the field withDefault::default(). -
#[dto(into)]
Calls::core::convert::Into::into(source.orig_name).
RequiresFrom<SourceFieldType> for FieldTypeand is infallible.
-
-
Struct attribute (required)
#[dto(from = Type)]— Specifies the source struct for the mapping.
Violations of these rules cause compile-time errors with span-accurate diagnostics (see the “Error Messages” section).