lxmf-wire 0.2.0

Core LXMF wire format, message primitives, and identity helpers for LXMF-rs.
Documentation
use crate::constants::FIELD_COMMANDS;
use crate::LxmfError;
use alloc::collections::BTreeMap;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandEntry {
    pub command_id: u8,
    pub payload: Vec<u8>,
}

impl CommandEntry {
    pub fn from_text(command_id: u8, payload: &str) -> Self {
        Self { command_id, payload: payload.as_bytes().to_vec() }
    }

    pub fn from_bytes(command_id: u8, payload: Vec<u8>) -> Self {
        Self { command_id, payload }
    }
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct WireFields {
    entries: BTreeMap<u8, rmpv::Value>,
}

impl WireFields {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn insert_field(&mut self, field_id: u8, value: rmpv::Value) -> &mut Self {
        self.entries.insert(field_id, value);
        self
    }

    pub fn set_commands<I>(&mut self, commands: I) -> &mut Self
    where
        I: IntoIterator<Item = CommandEntry>,
    {
        let mut out = Vec::new();
        for command in commands {
            let entry = rmpv::Value::Map(vec![(
                rmpv::Value::Integer((command.command_id as i64).into()),
                rmpv::Value::Binary(command.payload),
            )]);
            out.push(entry);
        }
        self.entries.insert(FIELD_COMMANDS, rmpv::Value::Array(out));
        self
    }

    pub fn to_rmpv(&self) -> rmpv::Value {
        let mut entries = Vec::with_capacity(self.entries.len());
        for (field_id, value) in &self.entries {
            entries.push((rmpv::Value::Integer((*field_id as i64).into()), value.clone()));
        }
        rmpv::Value::Map(entries)
    }

    pub fn encode_msgpack(&self) -> Result<Vec<u8>, LxmfError> {
        rmp_serde::to_vec(&self.to_rmpv()).map_err(|err| LxmfError::Encode(err.to_string()))
    }
}

#[cfg(test)]
mod tests {
    use super::{CommandEntry, WireFields};
    use crate::constants::FIELD_COMMANDS;

    #[test]
    fn commands_encode_with_integer_field_ids() {
        let mut fields = WireFields::new();
        fields.set_commands(vec![
            CommandEntry::from_text(0x01, "ping"),
            CommandEntry::from_bytes(0x02, vec![0xAA, 0xBB]),
        ]);

        let rmpv::Value::Map(entries) = fields.to_rmpv() else { panic!("expected map") };
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].0.as_i64(), Some(FIELD_COMMANDS as i64));
        let Some(cmds) = entries[0].1.as_array().cloned() else {
            panic!("commands array expected")
        };
        assert_eq!(cmds.len(), 2);
    }
}