Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make serialization non-dependent on serde #107

Open
plaidfinch opened this issue May 7, 2021 · 5 comments
Open

Make serialization non-dependent on serde #107

plaidfinch opened this issue May 7, 2021 · 5 comments
Assignees
Labels
enhancement New feature or request

Comments

@plaidfinch
Copy link
Contributor

Currently, dialectic-tokio-serde requires precisely a Serialize + DeserializeOwned bound. This can be made more generic, to enable, for instance, custom serialization approaches or whitelists of allowed types.

At present, the Serializer and Deserializer traits are:

pub trait Serializer {
    type Error;
    type Output;
    fn serialize<T: ?Sized + Serialize>(&mut self, item: &T) -> Result<Self::Output, Self::Error>;
}

pub trait Deserializer<Input> {
    type Error;
    fn deserialize<T: for<'a> Deserialize<'a>>(&mut self, src: &Input) -> Result<T, Self::Error>;
}

This could be changed to:

pub trait Serializer {
    type Error;
    type Output;
}

pub trait SerializerFor<T>: Serializer {
    fn serialize(&mut self, item: &T) -> Result<Self::Output, Self::Error>;
}

pub trait Deserializer<Input> {
    type Error;
}

pub trait DeserializerFor<Input, T>: Deserializer<Input> {
    fn deserialize(&mut self, src: &Input) -> Result<T, Self::Error>;
}

This would separate the crate entirely from a dependency on Serde, allowing downstream crates to depend on it if necessary, but also to use their own serialization strategies while still sharing the framing/encoding logic in this crate. Additionally, it allows for the creation of wrapper Serializer/Deserializer structs which wrap an existing such Serializer/Deserializer but restrict its instance further, for instance to enforce a whitelist of permitted types.

@plaidfinch
Copy link
Contributor Author

plaidfinch commented Jun 1, 2021

One useful thing this would allow: a parameterizeable allowlist serializer.

First, this setup would let you define a new marker type and enumerate a specific set of types which are allowed on the "allow list" denoted by that type.

/// A marker type standing in for allowing every type to be serialized/deserialized.
pub struct AllowAll;

/// A marker trait that indicates whether or not the type `T` belongs to the allow-list `List`.
pub trait Allowed<List = AllowAll> {}

/// The `AllowAll` marker type allows all types.
impl<T> Allowed<AllowAll> for T {}

You would use it like so:

mod sealed {
    pub struct OnlyByteAndBool;
}

impl Allowed<sealed::OnlyByteAndBool> for u8 {}
impl Allowed<sealed::OnlyByteAndBool> for bool {}

The sealed module being non-exported means nobody can add to the allow-list outside the crate, and in this example, only u8 and bool would ever belong to that list.

Then, this abstraction can be used to implement a general restriction mechanism for serializers/deserializers:

#[repr(transparent)]
struct Restricted<T, List = AllowAll>{
    inner: T,
    list: PhantomData<fn() -> List>,
}

impl<S: Serializer, List>  Serializer for Restricted<S, List> {
    type Error = S::Error;
    type Output = S::Output;
}

/// The where-clause on this impl restricts serialized types
/// to the allow-list specified in the phantom type
impl<List, T, S: SerializeFor<T>> SerializerFor<T> for Restricted<S, List>
where
    T: Allowed<List>
{
    #[inline(always)]
    fn serialize(&mut self, item: &T) -> Result<Self::Output, Self::Error> {
        self.inner.serialize(item)
    }
}

impl<D: Deserializer, List> Deserializer for Restricted<D, List> {
    type Error = S::Error;
}

/// The where-clause on this impl restricts deserialized types
/// to the allow-list specified in the phantom type
impl<List, Input, T, D: DeserializeFor<Input, T>> DeserializerFor<Input, T> for D
where
    T: Allowed<List>
{
    #[inline(always)]
    fn deserialize(&mut self, src: &Input) -> Result<T, Self::Error> {
        self.inner.deserialize(src)
    }
}

@plaidfinch
Copy link
Contributor Author

Also, with the unstable negative_impls feature, you could use the same technique to construct a deny-list!

@plaidfinch
Copy link
Contributor Author

An alternate design would be to build the allow-list functionality directly into the SerializeFor and DeserializeFor traits, making them take an extra defaulted-to-AllowAll type parameter. This would mean that serializers and deserializers would not need to be wrapped to grant them this functionality.

@plaidfinch
Copy link
Contributor Author

As a separate issue, if this functionality is merged, the name of the crate is slightly misleading: dialectic_tokio_codec would be more appropriate. Should we deprecate the former name, keep it, something else?

@plaidfinch
Copy link
Contributor Author

Oh! A better design would be to use the allow-list construct to restrict arbitrary senders/receivers! Make a wrapper around a sender or receiver that uses the same technique, and it's more general than merely for serialization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant