#mapper #model #entity

model-mapper

Derive macro to map between different types

3 unstable releases

0.2.1 Dec 3, 2023
0.2.0 Oct 13, 2023
0.1.0 Oct 12, 2023

#402 in Rust patterns

50 downloads per month

Apache-2.0

22KB
398 lines

Model Mapper

This library provides a macro to implement From and/or TryFrom trait between types (both enums and structs) without boilerplate.

It also provides a with module containing some utilities to convert between types that does not implement Into trait.

#[derive(Clone, Default, Mapper)]
#[mapper(from, ty = Entity)]
pub struct Model {
    id: i64,
    name: String,
    age: Option<i64>,
}

Usage

A mapper attribute is required at type-level and it's optional at field or variant level.

The following attributes are available.

  • Type level attributes:

    • ty = PathType (mandatory): The other type to derive the conversion
    • ignore (optional, multiple): Additional fields (for structs with named fields) or variants (for enums) the other type has and this one doesn't *
      • field = String (mandatory): The field or variant to ignore
      • default = Expr (optional): The default value (defaults to Default::default())
    • from (optional): Wether to derive From the other type for self
    • into (optional): Wether to derive From self for the other type
    • try_from (optional): Wether to derive TryFrom the other type for self
    • try_into (optional): Wether to derive TryFrom self for the other type
  • Variant level attributes:

    • ignore (optional, multiple): Additional fields of the variant that the other type variant has and this one doesn't *
      • field = String (mandatory): The field or variant to ignore
      • default = Expr (optional): The default value (defaults to Default::default())
    • skip (optional): Wether to skip this variant because the other enum doesn't have it *
    • default = Expr (optional): If skipped, the default value to populate this variant (defaults to Default::default())
    • rename = "OtherVariant" (optional): To rename this variant on the other enum
  • Field level attributes:

    • skip (optional): Wether to skip this field because the other type doesn't have it *
    • default = Expr (optional): If skipped, the default value to populate this field (defaults to Default::default())
    • rename = "other_field" (optional): To rename this field on the other type
    • with = mod::my_function (optional): If the field type doesn't implement Into the other, this property allows you to customize the behavior by providing a conversion function
    • try_with = mod::my_function (optional): If the field type doesn't implement TryInto the other, this property allows you to customize the behavior by providing a conversion function

* When ignoring or skipping fields or variants it might be required that the enum or the field type implements Default in order to properly populate it if no default value is provided.

Attributes can be set directly if only one type is involved in the conversion:

#[mapper(from, into, ty = OtherType, ignore(field = field_1), ignore(field = field_2))]

Or they can be wrapped in a derive attribute to allow for multiple conversions:

#[mapper(derive(try_from, ty = OtherType, ignore(field = field_1)))]
#[mapper(derive(into, ty = YetAnotherType))]

If multiple conversions are involved, both variant and field level attributes can also be wrapped in a when attribute and must set the ty they refer to:

#[mapper(when(ty = OtherType, try_with = with::try_remove_option))]
#[mapper(when(ty = YetAnotherType, skip))]

Example

pub enum Model {
    Empty,
    Text(String),
    Data {
        id: i64,
        text: String,
        status: Option<i32>,
        internal: bool,
    },
    Unknown,
}

#[derive(Default, Mapper)]
#[mapper(try_from, into, ty = Model, ignore(field = Unknown))]
pub enum Entity {
    Empty,
    #[mapper(rename = Text)]
    Message(String),
    #[mapper(ignore(field = internal))]
    Data {
        id: i64,
        #[mapper(rename = "text")]
        message: String,
        #[mapper(with = with::option)]
        #[mapper(try_with = with::try_option)]
        status: Option<i16>,
        #[mapper(skip, default = true)]
        random: bool,
    },
    #[mapper(skip, default = Model::Unknown)]
    #[default]
    Error,
}

The macro expansion above would generate something like:

impl From<Entity> for Model {
    fn from(other: Entity) -> Self {
        match other {
            Entity::Empty => Model::Empty,
            Entity::Message(m) => Model::Text(Into::into(m)),
            Entity::Data {
                id: id,
                message: message,
                status: status,
                random: _,
            } => Model::Data {
                id: Into::into(id),
                text: Into::into(message),
                status: with::option(status),
                internal: Default::default(),
            },
            Entity::Error => Model::Unknown,
        }
    }
}
impl TryFrom<Model> for Entity {
    type Error = ::anyhow::Error;
    fn try_from(other: Model) -> Result<Self, <Self as TryFrom<Model>>::Error> {
        Ok(match other {
            Model::Empty => Entity::Empty,
            Model::Text(t) => Entity::Message(TryInto::try_into(t)?),
            Model::Data {
                id: id,
                text: message,
                status: status,
                internal: _,
            } => Entity::Data {
                id: TryInto::try_into(id)?,
                message: TryInto::try_into(message)?,
                status: with::try_option(status)?,
                random: true,
            },
            Model::Unknown => Default::default(),
        })
    }
}

There are more examples available in the integration tests.

Dependencies

~0.9–1.7MB
~35K SLoC