mecha10-core 0.8.1

Mecha10 core runtime and foundational types
Documentation
//! Serde helper for transporting binary payloads (e.g. raw camera frame bytes)
//! efficiently over JSON-based message bus topics.
//!
//! JSON has no native "bytes" type, so `serde_json`'s default `Vec<u8>` handling
//! (and even `#[serde(with = "serde_bytes")]`, which only helps for binary formats
//! like bincode/CBOR/MessagePack) both serialize a `Vec<u8>` as a JSON array of
//! decimal numbers — e.g. `[137, 80, 78, ...]`. For large, high-frequency payloads
//! like uncompressed camera frames, that JSON-array-of-numbers encoding is ~3-4x
//! larger than the raw bytes and expensive to encode/decode, which was the primary
//! throughput amplifier behind LAB-2024's video frame drops.
//!
//! [`base64_bytes`] instead serializes a `Vec<u8>` as a single base64-encoded JSON
//! string, which is ~33% larger than the raw bytes (vs. ~300-400% for the decimal
//! array) and much cheaper to (de)serialize. Use it via `#[serde(with = "...")]`
//! on any `Vec<u8>` field that crosses a JSON-serialized message bus boundary.
//!
//! # Example
//!
//! ```
//! use mecha10_core::messages::binary::base64_bytes;
//!
//! #[derive(serde::Serialize, serde::Deserialize)]
//! struct Frame {
//!     #[serde(with = "base64_bytes")]
//!     image_bytes: Vec<u8>,
//! }
//!
//! let frame = Frame { image_bytes: vec![0, 1, 2, 255] };
//! let json = serde_json::to_string(&frame).unwrap();
//! assert_eq!(json, r#"{"image_bytes":"AAEC/w=="}"#);
//!
//! let round_tripped: Frame = serde_json::from_str(&json).unwrap();
//! assert_eq!(round_tripped.image_bytes, frame.image_bytes);
//! ```

pub mod base64_bytes {
    use base64::engine::general_purpose::STANDARD;
    use base64::Engine as _;
    use serde::{Deserialize, Deserializer, Serialize, Serializer};

    /// Serialize `bytes` as a base64-encoded string.
    pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        STANDARD.encode(bytes).serialize(serializer)
    }

    /// Deserialize a base64-encoded string into `Vec<u8>`.
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let encoded = String::deserialize(deserializer)?;
        STANDARD.decode(encoded.as_bytes()).map_err(serde::de::Error::custom)
    }
}

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

    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
    struct Wrapper {
        #[serde(with = "base64_bytes")]
        data: Vec<u8>,
    }

    #[test]
    fn round_trips_through_json() {
        let original = Wrapper {
            data: vec![0, 1, 2, 3, 254, 255],
        };
        let json = serde_json::to_string(&original).unwrap();
        let decoded: Wrapper = serde_json::from_str(&json).unwrap();
        assert_eq!(decoded, original);
    }

    #[test]
    fn serializes_as_compact_base64_string_not_number_array() {
        let wrapper = Wrapper {
            data: vec![0, 1, 2, 255],
        };
        let json = serde_json::to_string(&wrapper).unwrap();
        assert_eq!(json, r#"{"data":"AAEC/w=="}"#);
        assert!(!json.contains('['));
    }

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