Thoughts and advice on my use of Traits and Associative Types

I would love for some people to share their thoughts on my code here: Rust Playground

Basically I have a StandardClaims<C: Claim> struct that functions as a sort of container for claims about a person. I want to use this container both for requesting certain claims, as well as for holding the actual claim values.

That means that all the values in an StandardClaims instance need to be either of type Option<ClaimRequest<T>> OR of Option<ClaimValue<T>>. There can be no mixture of the two types in one single instance.

I feel that the way I've used the traits and associative types works quite well. However, I am wondering whether I'm using some bad practices here. Also, one thing I don't like is that I am using the Claim trait only for ClaimValue<T> and ClaimRequest<T>, and it actually should not be implemented at all for any other type. StandardClaims<ClaimValue> and StandardClaims<ClaimRequest> should be the only variations. Perhaps I should just use an enum instead of the Claim trait, but then I will loose the benefits of enforcing that all the claims in an StandardClaims instance are all either for 'requests' or 'values'.

Any thoughts are welcome!

Since all thoughts are welcome, my first thought is that it would help me answer your question if you could reduce the example to its essentials.

My second thought is that it seems like you're using generic associated types to specify a generic "wrapper" type that can be used in type definitions. You could also consider generating multiple specific types through a declarative macro. You could also specify multiple non-generic associative types for each of the values (String, i64, Address, ...) inside of the containers if those are finite.

I've reduced your example and applied some changes. Particularly, I've simplified the Claim trait to only specify the container and I was able to move the trait bounds from the data type to the generated Serialize and Deserialize implementations. This was inspired by @dtolnay 's advice from 2017.

pub trait Claim {
    type Container<T>;
}

pub struct ClaimRequest;

impl Claim for ClaimRequest {
    type Container<T> = ClaimRequestObject<T>;
}

pub struct ClaimValue;

impl Claim for ClaimValue {
    type Container<T> = T;
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ClaimRequestObject<T> {
    pub essential: Option<bool>,
    pub value: Option<T>,
    pub values: Option<Vec<T>>,
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct StandardClaims<C: Claim> {
    #[serde(bound(serialize = "C::Container<i64>: Serialize", deserialize = "C::Container<i64>: Deserialize<'de>"))]
    pub updated_at: Option<C::Container<i64>>,
    #[serde(bound(serialize = "C::Container<String>: Serialize", deserialize = "C::Container<String>: Deserialize<'de>"))]
    pub email: Option<C::Container<String>>,
    #[serde(bound(serialize = "C::Container<bool>: Serialize", deserialize = "C::Container<bool>: Deserialize<'de>"))]
    pub email_verified: Option<C::Container<bool>>,
    #[serde(bound(serialize = "C::Container<Address<C>>: Serialize", deserialize = "C::Container<Address<C>>: Deserialize<'de>"))]
    pub address: Option<C::Container<Address<C>>>,
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Address<C: Claim> {
    #[serde(bound(serialize = "C::Container<String>: Serialize", deserialize = "C::Container<String>: Deserialize<'de>"))]
    pub formatted: Option<C::Container<String>>,
}

pub type StandardClaimsRequests = StandardClaims<ClaimRequest>;
pub type StandardClaimsValues = StandardClaims<ClaimValue>;

I think what you're asking for is sealed traits.
I found an article that might be able to help you
https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/

Apologies for not being very concise (I will keep that in mind next time), and thanks a lot for sharing your solution. It is exactly what I was aiming for, but in a much more elegant way :clap:

Thank you, this is indeed very helpful. Your help together with what @mickvangelderen answered is exactly what I was hoping to find :+1:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.