synapse-proto 0.0.2

Protocol Buffers message definitions for Synapse RPC
Documentation
//! Synapse Protocol Buffer Definitions
//!
//! This crate contains the core protobuf definitions for the Synapse RPC protocol.
//! All messages use the `SynapseMessage` envelope with a discriminator for message type.

pub use bytes::Bytes;

/// Serde helpers for protobuf types
pub mod serde_helpers {
    use bytes::Bytes;
    use serde::{Deserialize, Deserializer, Serializer};

    /// Serialize request_id (16 bytes) as UUID string
    pub mod uuid_string {
        use super::*;
        use serde::Serialize;

        pub fn serialize<S>(bytes: &Bytes, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            if bytes.len() == 16 {
                let mut uuid_bytes = [0u8; 16];
                uuid_bytes.copy_from_slice(&bytes[..]);

                // Format as UUID string: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                let uuid_str = format!(
                    "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
                    uuid_bytes[0],
                    uuid_bytes[1],
                    uuid_bytes[2],
                    uuid_bytes[3],
                    uuid_bytes[4],
                    uuid_bytes[5],
                    uuid_bytes[6],
                    uuid_bytes[7],
                    uuid_bytes[8],
                    uuid_bytes[9],
                    uuid_bytes[10],
                    uuid_bytes[11],
                    uuid_bytes[12],
                    uuid_bytes[13],
                    uuid_bytes[14],
                    uuid_bytes[15]
                );
                uuid_str.serialize(serializer)
            } else {
                // Fallback to base64 for non-UUID bytes
                base64::Engine::encode(&base64::engine::general_purpose::STANDARD, bytes)
                    .serialize(serializer)
            }
        }

        pub fn deserialize<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
        where
            D: Deserializer<'de>,
        {
            use serde::de::Error;
            let s = String::deserialize(deserializer)?;

            // Try to parse as UUID first (with or without hyphens)
            let cleaned = s.replace("-", "");
            if cleaned.len() == 32 {
                // Parse hex string as UUID
                let mut bytes = Vec::with_capacity(16);
                for i in 0..16 {
                    let byte_str = &cleaned[i * 2..i * 2 + 2];
                    if let Ok(byte) = u8::from_str_radix(byte_str, 16) {
                        bytes.push(byte);
                    } else {
                        return Err(D::Error::custom("Invalid UUID hex"));
                    }
                }
                Ok(Bytes::from(bytes))
            } else {
                // Fallback to base64 decode
                base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.as_bytes())
                    .map(Bytes::from)
                    .map_err(D::Error::custom)
            }
        }
    }

    /// Serialize payload - supports both plain text/JSON and base64
    pub mod payload_flexible {
        use super::*;
        use serde::Serialize;

        pub fn serialize<S>(bytes: &Bytes, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            // Try to interpret as UTF-8 string first
            if let Ok(s) = std::str::from_utf8(bytes) {
                // If it's valid UTF-8, serialize as string
                s.serialize(serializer)
            } else {
                // Otherwise use base64
                base64::Engine::encode(&base64::engine::general_purpose::STANDARD, bytes)
                    .serialize(serializer)
            }
        }

        pub fn deserialize<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
        where
            D: Deserializer<'de>,
        {
            let s = String::deserialize(deserializer)?;

            // Try base64 decode first
            if let Ok(decoded) =
                base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.as_bytes())
            {
                // If it decodes successfully and is different from the original, use it
                if decoded != s.as_bytes() {
                    return Ok(Bytes::from(decoded));
                }
            }

            // Otherwise treat as plain text/JSON
            Ok(Bytes::from(s.into_bytes()))
        }
    }

    /// Serialize Bytes as base64 string (for other byte fields)
    pub mod bytes_base64 {
        use super::*;

        pub fn serialize<S>(bytes: &Bytes, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            use serde::Serialize;
            base64::Engine::encode(&base64::engine::general_purpose::STANDARD, bytes)
                .serialize(serializer)
        }

        pub fn deserialize<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
        where
            D: Deserializer<'de>,
        {
            use serde::de::Error;
            let s = String::deserialize(deserializer)?;
            base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.as_bytes())
                .map(Bytes::from)
                .map_err(D::Error::custom)
        }
    }
}

// Include generated code
include!(concat!(env!("OUT_DIR"), "/synapse.rs"));

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

    // Helper: roundtrip through JSON using a wrapper struct
    #[derive(serde::Serialize, serde::Deserialize, Debug)]
    struct UuidWrapper {
        #[serde(with = "uuid_string")]
        id: Bytes,
    }

    #[derive(serde::Serialize, serde::Deserialize, Debug)]
    struct PayloadWrapper {
        #[serde(with = "payload_flexible")]
        data: Bytes,
    }

    #[derive(serde::Serialize, serde::Deserialize, Debug)]
    struct Base64Wrapper {
        #[serde(with = "bytes_base64")]
        data: Bytes,
    }

    // ========== uuid_string ==========

    #[test]
    fn test_uuid_serialize_16_bytes() {
        let bytes = Bytes::from(vec![
            0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44,
            0x00, 0x00,
        ]);
        let wrapper = UuidWrapper { id: bytes };
        let json = serde_json::to_string(&wrapper).unwrap();
        assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
    }

    #[test]
    fn test_uuid_roundtrip() {
        let original = Bytes::from(vec![
            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
            0x0f, 0x10,
        ]);
        let wrapper = UuidWrapper {
            id: original.clone(),
        };
        let json = serde_json::to_string(&wrapper).unwrap();
        let decoded: UuidWrapper = serde_json::from_str(&json).unwrap();
        assert_eq!(original, decoded.id);
    }

    #[test]
    fn test_uuid_non_16_bytes_falls_back_to_base64() {
        let bytes = Bytes::from(vec![0x01, 0x02, 0x03]); // Not 16 bytes
        let wrapper = UuidWrapper { id: bytes };
        let json = serde_json::to_string(&wrapper).unwrap();
        // Should not contain UUID format hyphens in the expected pattern
        assert!(!json.contains('-'));
    }

    #[test]
    fn test_uuid_deserialize_with_hyphens() {
        let json = r#"{"id":"550e8400-e29b-41d4-a716-446655440000"}"#;
        let wrapper: UuidWrapper = serde_json::from_str(json).unwrap();
        assert_eq!(wrapper.id.len(), 16);
        assert_eq!(wrapper.id[0], 0x55);
        assert_eq!(wrapper.id[15], 0x00);
    }

    #[test]
    fn test_uuid_deserialize_without_hyphens() {
        let json = r#"{"id":"550e8400e29b41d4a716446655440000"}"#;
        let wrapper: UuidWrapper = serde_json::from_str(json).unwrap();
        assert_eq!(wrapper.id.len(), 16);
        assert_eq!(wrapper.id[0], 0x55);
    }

    // ========== payload_flexible ==========

    #[test]
    fn test_payload_utf8_serializes_as_string() {
        let wrapper = PayloadWrapper {
            data: Bytes::from("hello world"),
        };
        let json = serde_json::to_string(&wrapper).unwrap();
        assert!(json.contains("hello world"));
    }

    #[test]
    fn test_payload_binary_serializes_as_base64() {
        let wrapper = PayloadWrapper {
            data: Bytes::from(vec![0xFF, 0xFE, 0x00, 0x01]),
        };
        let json = serde_json::to_string(&wrapper).unwrap();
        // Should not contain raw binary, should be base64 encoded
        assert!(!json.contains('\u{ffff}'));
    }

    #[test]
    fn test_payload_plain_text_roundtrip() {
        // Plain text that is NOT valid base64 should survive roundtrip
        let wrapper = PayloadWrapper {
            data: Bytes::from("{\"key\":\"value\"}"),
        };
        let json = serde_json::to_string(&wrapper).unwrap();
        let decoded: PayloadWrapper = serde_json::from_str(&json).unwrap();
        assert_eq!(decoded.data, Bytes::from("{\"key\":\"value\"}"));
    }

    // ========== bytes_base64 ==========

    #[test]
    fn test_base64_roundtrip() {
        let original = Bytes::from(vec![0x00, 0xFF, 0x42, 0x13, 0x37]);
        let wrapper = Base64Wrapper {
            data: original.clone(),
        };
        let json = serde_json::to_string(&wrapper).unwrap();
        let decoded: Base64Wrapper = serde_json::from_str(&json).unwrap();
        assert_eq!(original, decoded.data);
    }

    #[test]
    fn test_base64_invalid_input() {
        let json = r#"{"data":"not valid base64!!@@"}"#;
        let result: Result<Base64Wrapper, _> = serde_json::from_str(json);
        assert!(result.is_err());
    }
}