fadedbee
fadedbee

Reputation: 44807

How to just use custom serialisation for "stringy" serialisation?

I've recently got to grips with custom serialisation/deserialisation: https://stackoverflow.com/a/63846824/129805

I want to use this custom "stringy" serialisation (and des.) only for JSON and RON, while using the #[derive(Serialisation, ... for all the binary serialisations, such as bincode. (Inflating a two-byte (100, 200) to seven or more bytes of "100:200" is pointlessly wasteful.)

I need to do this within a single executable, as server/server comms will be bincode or protobufs, while client/server comms will be JSON.

Both server/server and client/server comms will use the same serialisable structs. i.e. I want a single set of structs for all comms, but they should use custom serialisation for JSON/RON but derived serialisation for bin/protobufs.

How can I do this?


Update:

Here is working code with tests which pass:

use serde::{Serialize, Serializer, Deserialize, Deserializer};
use serde::de::{self, Visitor, Unexpected};
use std::fmt;
use std::str::FromStr;
use regex::Regex;

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct DerivedIncline {
    rise: u8,
    distance: u8,
}

impl DerivedIncline {
    pub fn new(rise: u8, distance: u8) -> DerivedIncline {
        DerivedIncline {rise, distance}
    }
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct StringyIncline {
    rise: u8,
    distance: u8,
}

impl StringyIncline {
    pub fn new(rise: u8, distance: u8) -> StringyIncline {
        StringyIncline {rise, distance}
    }
}

impl Serialize for StringyIncline {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&format!("{}:{}", self.rise, self.distance))
    }
}

struct StringyInclineVisitor;

impl<'de> Visitor<'de> for StringyInclineVisitor {
    type Value = StringyIncline;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("a colon-separated pair of integers between 0 and 255")
    }

    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        let re = Regex::new(r"(\d+):(\d+)").unwrap(); // PERF: move this into a lazy_static!
        if let Some(nums) = re.captures_iter(s).next() {
            if let Ok(rise) = u8::from_str(&nums[1]) { // nums[0] is the whole match, so we must skip that
                if let Ok(distance) = u8::from_str(&nums[2]) {
                    Ok(StringyIncline::new(rise, distance))
                } else {
                    Err(de::Error::invalid_value(Unexpected::Str(s), &self))
                }
            } else {
                Err(de::Error::invalid_value(Unexpected::Str(s), &self))
            }
        } else {
            Err(de::Error::invalid_value(Unexpected::Str(s), &self))
        }
    }

}

impl<'de> Deserialize<'de> for StringyIncline {
    fn deserialize<D>(deserializer: D) -> Result<StringyIncline, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_string(StringyInclineVisitor)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serialisation() {
        let stringy_incline = StringyIncline::new(4, 3);
        let derived_incline = DerivedIncline::new(4, 3);

        let json = serde_json::to_string(&stringy_incline).unwrap();
        assert_eq!(json, "\"4:3\"");
    
        let bin = bincode::serialize(&derived_incline).unwrap();
        assert_eq!(bin, [4u8, 3u8]);
    }

    #[test]
    fn deserialisation() {
        let json = "\"4:3\"";
        let bin = [4u8, 3u8];

        let deserialised_json: StringyIncline = serde_json::from_str(&json).unwrap();
        let deserialised_bin: DerivedIncline = bincode::deserialize(&bin).unwrap();

        assert_eq!(deserialised_json, StringyIncline::new(4, 3));
        assert_eq!(deserialised_bin, DerivedIncline::new(4, 3));
    }
}

I want to have a single Incline struct which acts like StringlyIncline when serialised to JSON or as DerivedIncline when serialised to bincode.

Upvotes: 0

Views: 339

Answers (1)

Michael Anderson
Michael Anderson

Reputation: 73590

If you're using nightly and are willing to turn on the specialization feature you can write a function that will tell you if the generic parameter S is a serde_json::Serializer

trait IsJsonSerializer {
  fn is_json_serializer() -> bool;
}

impl<T> IsJsonSerializer for T {
  default fn is_json_serializer() -> bool {
     false
  }
}

impl<W,F> IsJsonSerializer for &mut serde_json::Serializer<W,F> {
  fn is_json_serializer() -> bool {
     true
  }
}

Then you can write if S::is_json_serializer() {...}. Using this your serialization function can be written:

#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
struct RawIncline {
    rise: u8,
    distance: u8,
}

impl Serialize for Incline {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        if S::is_json_serializer() {
            serializer.serialize_str(&format!("{}:{}", self.rise, self.distance))
        } else {
            RawIncline{rise:self.rise, distance:self.distance}.serialize(serializer)
        }
    }
}

You can then do something similar for deserialization.

I can't think of a way to get something like this to work without the specialization feature, so it limited to nightly for now - but I'd love to see if it is possible somehow.

Upvotes: 1

Related Questions