bounded-integer icon indicating copy to clipboard operation
bounded-integer copied to clipboard

Choose the underlying deserialize implementation

Open tommasoclini opened this issue 1 year ago • 3 comments

I'd like to be able to choose between new and new_saturating for the implementation of deserialize, how could this be implemented?

tommasoclini avatar Jan 19 '25 16:01 tommasoclini

#[serde(deserialize_with = "deserialize_saturating")]
field: YourBoundedInteger,

and

fn deserialize_saturating<D: Deserializer<'de>>(deserializer: D) -> Result<YourBoundedInteger, D::Error> {
    // replace `u32` with the repr of your bounded integer
    u32::deserialize(deserializer).map(YourBoundedInteger::new_saturating)
}

Kestrer avatar Jan 19 '25 21:01 Kestrer

Thx, would a change to the crate to make this possible out-of-the-box require a new major version to be released? Or can it be achieved with a minor?

tommasoclini avatar Jan 19 '25 21:01 tommasoclini

Hmm. There are multiple possible designs here, not mutually exclusive, and I’m not sure which would be best:

Option 1: direct support for macro-generated integers only

bounded_integer! {
    #[deserialize_saturating]
    struct YourInteger(-5, 5);
}

As I write this, I reälize that our current approach of simply forwarding all the attributes that exist in the original declaration to the output is unsound. We’ll have to remove that. Anyway, in the macro we can just look for a #[deserialize_saturating], set some deserialize_saturating: bool in the BoundedInteger struct and affect the generated code accordingly.

Option 2: opt-out for macro-generated integers

thus allowing people to write their own implementation of Deserialize

bounded_integer! {
    #[no_serde] // maybe #[no_deserialize]/#[no_deserialize] as well?
    // or maybe it should be spelt #[derive(!Deserialize)]? 🤔 
    struct YourInteger(-5, 5);
}

then people can write:

impl<'de> Deserialize<'de> for YourInteger {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        u32::deserialize(deserializer).map(Self::new_saturating)
    }
}

Option 3: provide some kind of serde helper

This version works for both macro-generated and const-generic bounded integers.

// This trait duplicates, but does not supplant, the inherent functionality on bounded integers;
// it can’t supplant it because we don’t want people to have to import the trait to use bounded
// integers.
pub trait BoundedInteger: Ord + /* bunch of stuff */ + private::Sealed {
    type Inner;
    const MIN: Self;
    const MAX: Self;
    unsafe fn new_unchecked(inner: Self::Inner) -> Self { unsafe { transmute(inner) } }
    fn new_saturating(inner: Self::Inner) -> Self { /* … */ }
}
pub mod serde {
    pub fn de_saturating<T: BoundedInteger, D: Deserializer<'de>>(deserializer: D) -> Result<T, D::Error> {
        Deserialize::deserialize(deserializer).map(T::new_saturating)
    }
}
pub use serde::*;

which you can then use with #[serde(deserialize_with = "bounded_integer::de_saturating")].


I would probably accept a PR for any of these approaches. I think they’re all good to have and could co-exist.

Kestrer avatar Jan 19 '25 21:01 Kestrer