Artem-Romanenia / o2o

Object to Object mapper for Rust
Apache License 2.0
31 stars 2 forks source link

Flattening without Default #15

Closed utomdht closed 10 hours ago

utomdht commented 3 months ago

Hi,

Thanks for this great project, it save me a lot of time mapping DTOs/BOs !

I was wondering if there could be a way to generate Into with struct flattening without relying on Default, ex:

// Source, built from my API, and is missing some ids extracted from auth or other sources
struct EntityNew {
    name: String
    description: String
}

// this struct is built within the Domain layer just for o2o mapping, wrapping the API object and extracted fields
#[owned_into(EntityNewBO)]
struct EntityNewWrapper {
   #[flatten]
    wrapped: EntityNew
    id: Uuid
    user_id: Uuid    
    computed_field: Int
}

// Target object. A default on this doesn't make much sense and bear the risk of missing compilation errors when adding fields
struct EntityNewBO {
    id: Uuid
    user_id: Uuid    
    computed_field: Int
    name: String
    description: String
}
Artem-Romanenia commented 3 months ago

Hi, thanks for such high evaluation of o2o!

The problem with theoretic #[flatten] attribute is that there is no way for proc macro to know what fields wrapped: EntityNew has. It may even not be a struct. So you have to somehow let o2o know about those fields. Since you need to do Into, you can do this:

#[owned_into(EntityNewBO)]
#[ghosts(name: { @.wrapped.name }, description: { @.wrapped.description })]
struct EntityNewWrapper {
    #[ghost]
    wrapped: EntityNew,
    id: Uuid,
    user_id: Uuid,  
    computed_field: Int,
}

If there is an option to put o2o macro on the other side of a mapping, you can also do this:

#[from_owned(EntityNewWrapper)]
struct EntityNewBO {
    id: Uuid,
    user_id: Uuid, 
    computed_field: Int,
    #[child(wrapped)] name: String,
    #[child(wrapped)] description: String,
}

Please post here or create new issue if you have any other questions!

Artem-Romanenia commented 3 months ago

Actually I had an idea that can make things a little better in such cases, so I`ll keep this one open)

Artem-Romanenia commented 10 hours ago

Hi @utomdht, it took me a long time to implement this, but there's finally a version v0.5.0 that can help in your case.

struct EntityNew {
    name: String,
    description: String
}

// this struct is built within the Domain layer just for o2o mapping, wrapping the API object and extracted fields
#[derive(o2o::o2o)]
#[map_owned(EntityNewBO)]
struct EntityNewWrapper {
    #[parent(name, description)] // <--- parent instruction here does what you want
    wrapped: EntityNew,
    id: Uuid,
    user_id: Uuid,
    computed_field: i32
}

// Target object. A default on this doesn't make much sense and bear the risk of missing compilation errors when adding fields
struct EntityNewBO {
    id: Uuid,
    user_id: Uuid,
    computed_field: i32,
    name: String,
    description: String
}

This example will generate following code:

impl ::core::convert::From<EntityNewBO> for EntityNewWrapper {
    fn from(value: EntityNewBO) -> EntityNewWrapper {
        EntityNewWrapper {
            wrapped: EntityNew { name: value.name, description: value.description },
            id: value.id,
            user_id: value.user_id,
            computed_field: value.computed_field,
        }
    }
}
impl ::core::convert::Into<EntityNewBO> for EntityNewWrapper {
    fn into(self) -> EntityNewBO {
        EntityNewBO {
            name: self.wrapped.name,
            description: self.wrapped.description,
            id: self.id,
            user_id: self.user_id,
            computed_field: self.computed_field,
        }
    }
}

You can check out this section of readme for a little bit more info.