wireband-edge 0.4.0

Lightweight Wire.Band client — semantic data middleware for any domain (IoT, AI/ML, DeFi, legal, geospatial, supply chain, and more)
Documentation
//! Theta frame encoding and decoding.

use serde_json::Value;

use crate::error::{Result, WireBandError};

pub const FRAME_PREFIX_LEN: usize = 2;

/// Encode a value as a framed compact JSON payload.
pub fn encode(symbol: u16, topic: &str, data: &Value) -> Vec<u8> {
    let body = serde_json::to_vec(&serde_json::json!({ "t": topic, "d": data }))
        .unwrap_or_default();
    let mut frame = Vec::with_capacity(FRAME_PREFIX_LEN + body.len());
    frame.push((symbol >> 8) as u8);
    frame.push((symbol & 0xFF) as u8);
    frame.extend_from_slice(&body);
    frame
}

/// Decode a theta-prefixed frame into (symbol, payload).
pub fn decode(raw: &[u8]) -> Result<(u16, Value)> {
    if raw.len() < FRAME_PREFIX_LEN {
        return Err(WireBandError::FrameTooShort(raw.len()));
    }
    let symbol = ((raw[0] as u16) << 8) | raw[1] as u16;
    let payload: Value = serde_json::from_slice(&raw[FRAME_PREFIX_LEN..])?;
    Ok((symbol, payload))
}

/// Hex-encode frame bytes for JSON transport.
#[inline]
pub fn to_hex(frame: &[u8]) -> String {
    frame.iter().map(|b| format!("{b:02x}")).collect()
}

/// Decode a hex string back to frame bytes.
pub fn from_hex(hex: &str) -> Option<Vec<u8>> {
    if hex.len() % 2 != 0 {
        return None;
    }
    (0..hex.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok())
        .collect()
}

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

    #[test]
    fn round_trip() {
        let data = serde_json::json!({ "v": 22.5 });
        let frame = encode(SENSOR_TEMP, "sensors/temp", &data);
        let (sym, payload) = decode(&frame).unwrap();
        assert_eq!(sym, SENSOR_TEMP);
        assert_eq!(payload["t"], "sensors/temp");
        assert_eq!(payload["d"]["v"], 22.5);
    }

    #[test]
    fn prefix_bytes() {
        let frame = encode(0xFC60, "t", &serde_json::json!({}));
        assert_eq!(frame[0], 0xFC);
        assert_eq!(frame[1], 0x60);
    }

    #[test]
    fn compact_no_whitespace() {
        let frame = encode(SENSOR_TEMP, "t", &serde_json::json!({ "v": 1 }));
        let body = std::str::from_utf8(&frame[2..]).unwrap();
        assert!(!body.contains(' '));
    }

    #[test]
    fn hex_round_trip() {
        let frame = encode(SENSOR_TEMP, "t", &serde_json::json!({ "v": 1 }));
        let hex = to_hex(&frame);
        let decoded = from_hex(&hex).unwrap();
        assert_eq!(frame, decoded);
    }

    #[test]
    fn decode_too_short() {
        assert!(decode(&[0xFC]).is_err());
        assert!(decode(&[]).is_err());
    }
}