spiral-contract 0.1.10

Language-neutral service wire values
Documentation
//! Lossless JSON handling for 64-bit integers consumed by JavaScript clients.

use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;

use serde::de::{Error as DeserializeError, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};

const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;

/// Serde adapter for a signed 64-bit integer.
pub mod i64 {
    use super::*;

    /// Serialize integers as lossless decimal strings.
    pub fn serialize<S: Serializer>(value: &i64, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&value.to_string())
    }

    /// Accept decimal strings or JSON numbers within JavaScript's safe range.
    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i64, D::Error> {
        struct SignedVisitor;

        impl Visitor<'_> for SignedVisitor {
            type Value = i64;

            fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
                formatter.write_str("a decimal int64 string or a safe JSON integer")
            }

            fn visit_i64<E: DeserializeError>(self, value: i64) -> Result<Self::Value, E> {
                (value.unsigned_abs() <= MAX_SAFE_INTEGER)
                    .then_some(value)
                    .ok_or_else(|| E::custom("JSON integer exceeds the safe range"))
            }

            fn visit_u64<E: DeserializeError>(self, value: u64) -> Result<Self::Value, E> {
                i64::try_from(value)
                    .ok()
                    .filter(|value| value.unsigned_abs() <= MAX_SAFE_INTEGER)
                    .ok_or_else(|| E::custom("JSON integer exceeds the safe range"))
            }

            fn visit_str<E: DeserializeError>(self, value: &str) -> Result<Self::Value, E> {
                let digits = value.strip_prefix('-').unwrap_or(value);
                if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
                    return Err(E::custom("int64 string must contain decimal digits"));
                }
                value.parse().map_err(E::custom)
            }
        }

        deserializer.deserialize_any(SignedVisitor)
    }
}

/// Serde adapter for an unsigned 64-bit integer.
pub mod u64 {
    use super::*;

    /// Serialize integers as lossless decimal strings.
    pub fn serialize<S: Serializer>(value: &u64, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&value.to_string())
    }

    /// Accept decimal strings or JSON numbers within JavaScript's safe range.
    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
        struct UnsignedVisitor;

        impl Visitor<'_> for UnsignedVisitor {
            type Value = u64;

            fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
                formatter.write_str("a decimal uint64 string or a safe JSON integer")
            }

            fn visit_u64<E: DeserializeError>(self, value: u64) -> Result<Self::Value, E> {
                (value <= MAX_SAFE_INTEGER)
                    .then_some(value)
                    .ok_or_else(|| E::custom("JSON integer exceeds the safe range"))
            }

            fn visit_i64<E: DeserializeError>(self, value: i64) -> Result<Self::Value, E> {
                u64::try_from(value)
                    .map_err(E::custom)
                    .and_then(|value| self.visit_u64(value))
            }

            fn visit_str<E: DeserializeError>(self, value: &str) -> Result<Self::Value, E> {
                if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
                    return Err(E::custom("uint64 string must contain decimal digits"));
                }
                value.parse().map_err(E::custom)
            }
        }

        deserializer.deserialize_any(UnsignedVisitor)
    }
}

/// A signed 64-bit integer encoded losslessly for every supported language.
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct SafeLong(#[serde(with = "i64")] pub i64);

/// An unsigned 64-bit integer encoded losslessly for every supported language.
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct SafeUnsignedLong(#[serde(with = "u64")] pub u64);

impl From<i64> for SafeLong {
    fn from(value: i64) -> Self {
        Self(value)
    }
}

impl From<SafeLong> for i64 {
    fn from(value: SafeLong) -> Self {
        value.0
    }
}

impl From<u64> for SafeUnsignedLong {
    fn from(value: u64) -> Self {
        Self(value)
    }
}

impl From<SafeUnsignedLong> for u64 {
    fn from(value: SafeUnsignedLong) -> Self {
        value.0
    }
}

/// A decimal string outside the wire integer grammar or range.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SafeLongParseError;

impl Display for SafeLongParseError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        formatter.write_str("expected a decimal integer within the wire type's range")
    }
}
impl Error for SafeLongParseError {}

impl Display for SafeLong {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        self.0.fmt(formatter)
    }
}
impl Display for SafeUnsignedLong {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        self.0.fmt(formatter)
    }
}

impl FromStr for SafeLong {
    type Err = SafeLongParseError;
    fn from_str(value: &str) -> Result<Self, Self::Err> {
        let digits = value.strip_prefix('-').unwrap_or(value);
        if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
            return Err(SafeLongParseError);
        }
        value.parse().map(Self).map_err(|_| SafeLongParseError)
    }
}
impl FromStr for SafeUnsignedLong {
    type Err = SafeLongParseError;
    fn from_str(value: &str) -> Result<Self, Self::Err> {
        if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
            return Err(SafeLongParseError);
        }
        value.parse().map(Self).map_err(|_| SafeLongParseError)
    }
}

#[cfg(test)]
mod tests {
    use serde::{Deserialize, Serialize};
    use serde_json::json;

    #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
    struct Counts {
        #[serde(with = "super::i64")]
        signed: i64,
        #[serde(with = "super::u64")]
        unsigned: u64,
    }

    #[test]
    fn integers_use_decimal_strings_and_unsafe_numbers_fail() {
        assert_eq!(
            serde_json::to_value(super::SafeLong(1)).unwrap(),
            json!("1")
        );
        assert_eq!(
            serde_json::to_value(super::SafeUnsignedLong(1)).unwrap(),
            json!("1")
        );
        for invalid in ["+1", " 1", "1_000", "1", "9223372036854775808"] {
            assert!(invalid.parse::<super::SafeLong>().is_err());
        }
        let counts = Counts {
            signed: i64::MIN,
            unsigned: u64::MAX,
        };
        let encoded = serde_json::to_value(&counts).expect("encode counts");
        assert_eq!(
            encoded,
            json!({"signed":"-9223372036854775808","unsigned":"18446744073709551615"})
        );
        assert_eq!(
            serde_json::from_value::<Counts>(encoded).expect("decode counts"),
            counts
        );
        assert!(
            serde_json::from_value::<Counts>(json!({"signed":9007199254740992_i64,"unsigned":1}))
                .is_err()
        );
        assert!(
            serde_json::from_value::<Counts>(json!({"signed":1,"unsigned":9007199254740992_u64}))
                .is_err()
        );
    }
}