cyclone-runtime-rust 1.0.1

Rust reference runtime for the Cyclone binary wire format: Writer, Reader, DecodeError.
Documentation
//! The [`Encode`] / [`Decode`] traits and the two helpers built on them.
//!
//! The impls here are written by hand on purpose: the runtime cannot tell a
//! hand-written codec from a derived one or from a CLI-generated file, and the
//! bytes must be the same either way.

use cyclone_runtime::{from_bytes, to_bytes, Decode, DecodeError, Encode, Reader, Writer};

#[derive(Debug, PartialEq)]
struct Vector3 {
    x: f32,
    y: f32,
    z: f32,
}

impl Encode for Vector3 {
    fn encode(&self, writer: &mut Writer) {
        writer.write_f32(self.x);
        writer.write_f32(self.y);
        writer.write_f32(self.z);
    }
}

impl Decode for Vector3 {
    fn decode(reader: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Vector3 {
            x: reader.read_f32()?,
            y: reader.read_f32()?,
            z: reader.read_f32()?,
        })
    }
}

/// A Model holding a nested Model, a String and an Array - inlined, no
/// delimiters, exactly as RFC-0002 ยง15 specifies.
#[derive(Debug, PartialEq)]
struct GameMessage {
    player_id: u32,
    player_name: String,
    position: Vector3,
    health: u32,
    is_alive: bool,
}

impl Encode for GameMessage {
    fn encode(&self, writer: &mut Writer) {
        writer.write_u32(self.player_id);
        writer.write_string(&self.player_name);
        self.position.encode(writer);
        writer.write_u32(self.health);
        writer.write_bool(self.is_alive);
    }
}

impl Decode for GameMessage {
    fn decode(reader: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(GameMessage {
            player_id: reader.read_u32()?,
            player_name: reader.read_string()?,
            position: Vector3::decode(reader)?,
            health: reader.read_u32()?,
            is_alive: reader.read_bool()?,
        })
    }
}

fn sample() -> GameMessage {
    GameMessage {
        player_id: 42,
        player_name: "Knight".to_owned(),
        position: Vector3 { x: 10.5, y: 20.3, z: -5.1 },
        health: 100,
        is_alive: true,
    }
}

const SAMPLE_BYTES: &[u8] = &[
    0x2A, 0x00, 0x00, 0x00, //
    0x06, 0x00, 0x00, 0x00, 0x4B, 0x6E, 0x69, 0x67, 0x68, 0x74, //
    0x00, 0x00, 0x28, 0x41, 0x66, 0x66, 0xA2, 0x41, 0x33, 0x33, 0xA3, 0xC0, //
    0x64, 0x00, 0x00, 0x00, //
    0x01,
];

#[test]
fn to_bytes_matches_the_specification() {
    assert_eq!(to_bytes(&sample()), SAMPLE_BYTES);
}

#[test]
fn from_bytes_recovers_the_value() {
    assert_eq!(from_bytes::<GameMessage>(SAMPLE_BYTES).expect("decode"), sample());
}

#[test]
fn round_trip_through_the_helpers() {
    let bytes = to_bytes(&sample());
    let value: GameMessage = from_bytes(&bytes).expect("decode");
    assert_eq!(value, sample());
    assert_eq!(to_bytes(&value), bytes);
}

/// A nested Model is inlined: encoding it through the trait produces the same
/// bytes as writing its fields into the outer buffer by hand.
#[test]
fn nested_model_is_inlined() {
    let position = Vector3 { x: 1.5, y: 2.5, z: 3.5 };

    let mut through_trait = Writer::new();
    position.encode(&mut through_trait);

    let mut by_hand = Writer::new();
    by_hand.write_f32(1.5);
    by_hand.write_f32(2.5);
    by_hand.write_f32(3.5);

    assert_eq!(through_trait.as_slice(), by_hand.as_slice());
    assert_eq!(through_trait.len(), 12);
}

#[test]
fn from_bytes_propagates_decode_errors() {
    assert_eq!(
        from_bytes::<Vector3>(&[0x00, 0x00, 0x00]),
        Err(DecodeError::UnexpectedEof { needed: 4, remaining: 3 })
    );
}

/// `from_bytes` decodes one value and returns it; leftover bytes are the
/// caller's business. A caller that needs the stream to end exactly at the
/// value drives a `Reader` and checks `is_empty` itself.
#[test]
fn from_bytes_ignores_trailing_bytes() {
    let mut bytes = SAMPLE_BYTES.to_vec();
    bytes.extend_from_slice(&[0xAA, 0xBB]);
    assert_eq!(from_bytes::<GameMessage>(&bytes).expect("decode"), sample());

    let mut reader = Reader::new(&bytes);
    GameMessage::decode(&mut reader).expect("decode");
    assert_eq!(reader.remaining(), 2);
}

/// Encoding through the helper is deterministic, and matches driving a
/// `Writer` directly - the helper adds no framing of its own.
#[test]
fn to_bytes_adds_nothing() {
    let value = sample();

    let mut writer = Writer::new();
    value.encode(&mut writer);

    assert_eq!(to_bytes(&value), writer.into_bytes());
    assert_eq!(to_bytes(&value), to_bytes(&value));
}