Artem-Romanenia / o2o

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

Showcase how I use this crate to crate a mapping between (secured) business object and serializable dto #21

Closed tisonkun closed 1 month ago

tisonkun commented 2 months ago
use derivative::Derivative;
use secrecy::ExposeSecret;
use secrecy::SecretString;
use serde::Deserialize;

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct TableProps {
    pub storage: Option<StorageProps>,
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StorageProps {
    Redacted_0(Redacted_0Props),
    Redacted_1(Redacted_1Props),
}

#[derive(Debug, Clone, Derivative, Deserialize)]
#[derivative(PartialEq)]
pub struct Redacted_0Props {
    pub normal_field: String,
    pub another_normal_field: String,
    #[derivative(PartialEq(compare_with = "compare_secret_string"))]
    pub secured_field: SecretString,
    #[derivative(PartialEq(compare_with = "compare_secret_string"))]
    pub another_secured_field: SecretString,
}

impl Eq for Redacted_0Props {}

#[derive(Debug, Clone, Derivative, Deserialize)]
#[derivative(PartialEq)]
pub struct Redacted_1Props {
    pub normal_field: String,
    pub another_normal_field: String,
    #[derivative(PartialEq(compare_with = "compare_secret_string"))]
    pub secured_field: SecretString,
    #[derivative(PartialEq(compare_with = "compare_secret_string"))]
    pub access_key_secret: SecretString,
}

fn compare_secret_string(a: &SecretString, b: &SecretString) -> bool {
    a.expose_secret() == b.expose_secret()
}

impl Eq for Redacted_1Props {}

/// Expose secret strings in props for serde. Struct defs must be kept in sync.
pub mod storage_exposed {
    use secrecy::ExposeSecret;
    use serde::Deserialize;
    use serde::Serialize;

    use crate::props::Redacted_1Props;
    use crate::props::Redacted_0Props;
    use crate::props::StorageProps;
    use crate::props::TableProps;

    fn secret_to_string(secret: secrecy::SecretString) -> String {
        secret.expose_secret().to_string()
    }

    #[derive(Serialize, Deserialize, o2o::o2o)]
    #[map_owned(TableProps)]
    pub struct TablePropsExposed {
        #[map(~.map(|props| props.into()))]
        pub storage: Option<StoragePropsExposed>,
    }

    #[derive(Serialize, Deserialize, o2o::o2o)]
    #[map_owned(StorageProps)]
    #[serde(tag = "type", rename_all = "snake_case")]
    pub enum StoragePropsExposed {
        Redacted_0(#[map(~.into())] Redacted_0PropsExposed),
        Redacted_1(#[map(~.into())] Redacted_1PropsExposed),
    }

    #[derive(Serialize, Deserialize, o2o::o2o)]
    #[map_owned(Redacted_0Props)]
    pub struct Redacted_0PropsExposed {
        pub normal_field: String,
        pub another_normal_field: String,

        #[from(secret_to_string(~))]
        #[into(~.into())]
        pub secured_field: String,

        #[from(secret_to_string(~))]
        #[into(~.into())]
        pub another_secured_field: String,
    }

    #[derive(Serialize, Deserialize, o2o::o2o)]
    #[map_owned(Redacted_1Props)]
    pub struct Redacted_1PropsExposed {
        pub normal_field: String,
        pub another_normal_field: String,

        #[from(secret_to_string(~))]
        #[into(~.into())]
        pub secured_field: String,

        #[from(secret_to_string(~))]
        #[into(~.into())]
        pub access_key_secret: String,
    }
}

Works perfectly so far.

I may have some further ideas like use mapstruct to keep fields update in sync. But with o2o the compiler can guard the convert bound. And I'd like a replacement for derivative which seems to be unmaintained but its current code works well. educe is weird and drive_more doesn't support PartialEq. Anyway, these seems out of the scope of this crate. Just share some related thoughts.

Thanks for this excellent crate. I'll adopt it further if it reaches 1.0 and stable API with considered.

tisonkun commented 2 months ago

The only wart I found is that #[map] implements From and Into both on TablePropsExposed. So there is no From<TablePropsExposed> for TableProps implemented for TableProps.

For all where I previously write TableProps::from(props /* TablePropsExposed */) I should now write props.into(). But this is a reasonable trade-off.

Artem-Romanenia commented 1 month ago

Hi! Thank you for good words!

The only wart I found is that #[map] implements From and Into both on TablePropsExposed. So there is no From<TablePropsExposed> for TableProps implemented for TableProps.

That was deliberate. I think in this case, it is more intuitive when, if a proc macro is applied on struct A, it produces impl ... for A.

As an alternative, instead of applying map_owned on one side, you can apply 'from_owned' on both sides:

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, o2o::o2o)]
#[from_owned(TablePropsExposed)]
pub struct TableProps { ... }

#[derive(Serialize, Deserialize, o2o::o2o)]
#[from_owned(TableProps)]
pub struct TablePropsExposed { ... }

It is wordier, but in some cases may even result in more readable code.

Artem-Romanenia commented 1 month ago

Please reopen if you have any other questions or suggestions.