rover_nexus_core 0.2.0

Wire-format message types (Cap'n Proto + JSON) for communication between robotic vehicles and a robot orchestration server: Rover Nexus
Documentation
// Copyright 2026 Rottinghaus Dynamics
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Cap'n Proto message serialization for settings query messages.

use crate::core_model::Value;
use crate::settings_query::{GetSettingsRequest, GetSettingsResponse, SettingItem};

use ::capnp::message::Builder;
use ::capnp::serialize;

use std::io::Cursor;

// Helper functions for Value enum serialization

fn write_value(builder: &mut crate::messages_capnp::value::Builder, value: &Value) {
    match value {
        Value::String(s) => builder.set_string(s),
        Value::Int(i) => builder.set_int(*i),
        Value::Number(n) => builder.set_number(*n),
        Value::Bool(b) => builder.set_bool(*b),
        Value::Enum(s) => builder.set_enum(s),
    }
}

fn read_value(reader: crate::messages_capnp::value::Reader) -> Result<Value, ::capnp::Error> {
    match reader.which()? {
        crate::messages_capnp::value::String(Ok(s)) => Ok(Value::String(s.to_str()?.to_string())),
        crate::messages_capnp::value::String(Err(e)) => Err(e),
        crate::messages_capnp::value::Int(i) => Ok(Value::Int(i)),
        crate::messages_capnp::value::Number(n) => Ok(Value::Number(n)),
        crate::messages_capnp::value::Bool(b) => Ok(Value::Bool(b)),
        crate::messages_capnp::value::Enum(Ok(s)) => Ok(Value::Enum(s.to_str()?.to_string())),
        crate::messages_capnp::value::Enum(Err(e)) => Err(e),
    }
}

// Write helpers

fn write_setting_item(
    builder: &mut crate::settings_query_capnp::setting_item::Builder,
    item: &SettingItem,
) {
    builder.set_key(&item.key);
    let mut value_builder = builder.reborrow().init_value();
    write_value(&mut value_builder, &item.value);
    builder.set_updated_at_ms(item.updated_at_ms);
    builder.set_updated_by(&item.updated_by);
}

// Read helpers

fn read_setting_item(
    reader: crate::settings_query_capnp::setting_item::Reader,
) -> Result<SettingItem, ::capnp::Error> {
    Ok(SettingItem {
        key: reader.get_key()?.to_str()?.to_string(),
        value: read_value(reader.get_value()?)?,
        updated_at_ms: reader.get_updated_at_ms(),
        updated_by: reader.get_updated_by()?.to_str()?.to_string(),
    })
}

// Public serialization API

/// Serialize a GetSettingsRequest to Cap'n Proto bytes
pub fn serialize_get_settings_request(
    request: &GetSettingsRequest,
) -> Result<Vec<u8>, ::capnp::Error> {
    let mut message = Builder::new_default();
    let mut builder =
        message.init_root::<crate::settings_query_capnp::get_settings_request::Builder>();

    let mut keys_builder = builder.reborrow().init_keys(request.keys.len() as u32);
    for (i, key) in request.keys.iter().enumerate() {
        keys_builder.set(i as u32, key);
    }

    let mut buf = Vec::new();
    serialize::write_message(&mut buf, &message)?;
    Ok(buf)
}

/// Deserialize a GetSettingsRequest from Cap'n Proto bytes
pub fn deserialize_get_settings_request(data: &[u8]) -> Result<GetSettingsRequest, ::capnp::Error> {
    let cursor = Cursor::new(data);
    let message_reader = serialize::read_message(cursor, ::capnp::message::ReaderOptions::new())?;
    let reader =
        message_reader.get_root::<crate::settings_query_capnp::get_settings_request::Reader>()?;

    let keys_reader = reader.get_keys()?;
    let mut keys = Vec::with_capacity(keys_reader.len() as usize);
    for i in 0..keys_reader.len() {
        keys.push(keys_reader.get(i)?.to_str()?.to_string());
    }

    Ok(GetSettingsRequest { keys })
}

/// Serialize a GetSettingsResponse to Cap'n Proto bytes
pub fn serialize_get_settings_response(
    response: &GetSettingsResponse,
) -> Result<Vec<u8>, ::capnp::Error> {
    let mut message = Builder::new_default();
    let mut builder =
        message.init_root::<crate::settings_query_capnp::get_settings_response::Builder>();

    let mut settings_builder = builder
        .reborrow()
        .init_settings(response.settings.len() as u32);
    for (i, item) in response.settings.iter().enumerate() {
        let mut item_builder = settings_builder.reborrow().get(i as u32);
        write_setting_item(&mut item_builder, item);
    }

    let mut not_found_builder = builder
        .reborrow()
        .init_not_found(response.not_found.len() as u32);
    for (i, key) in response.not_found.iter().enumerate() {
        not_found_builder.set(i as u32, key);
    }

    let mut buf = Vec::new();
    serialize::write_message(&mut buf, &message)?;
    Ok(buf)
}

/// Deserialize a GetSettingsResponse from Cap'n Proto bytes
pub fn deserialize_get_settings_response(
    data: &[u8],
) -> Result<GetSettingsResponse, ::capnp::Error> {
    let cursor = Cursor::new(data);
    let message_reader = serialize::read_message(cursor, ::capnp::message::ReaderOptions::new())?;
    let reader =
        message_reader.get_root::<crate::settings_query_capnp::get_settings_response::Reader>()?;

    let settings_reader = reader.get_settings()?;
    let mut settings = Vec::with_capacity(settings_reader.len() as usize);
    for i in 0..settings_reader.len() {
        settings.push(read_setting_item(settings_reader.get(i))?);
    }

    let not_found_reader = reader.get_not_found()?;
    let mut not_found = Vec::with_capacity(not_found_reader.len() as usize);
    for i in 0..not_found_reader.len() {
        not_found.push(not_found_reader.get(i)?.to_str()?.to_string());
    }

    Ok(GetSettingsResponse {
        settings,
        not_found,
    })
}

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

    #[test]
    fn test_get_settings_request_roundtrip() {
        let original = GetSettingsRequest {
            keys: vec![
                "robot.speed_limit".to_string(),
                "robot.name".to_string(),
                "debug.enabled".to_string(),
            ],
        };

        let serialized = serialize_get_settings_request(&original).expect("Failed to serialize");
        let deserialized =
            deserialize_get_settings_request(&serialized).expect("Failed to deserialize");

        assert_eq!(original, deserialized);
    }

    #[test]
    fn test_get_settings_request_empty_roundtrip() {
        let original = GetSettingsRequest { keys: vec![] };

        let serialized = serialize_get_settings_request(&original).expect("Failed to serialize");
        let deserialized =
            deserialize_get_settings_request(&serialized).expect("Failed to deserialize");

        assert_eq!(original, deserialized);
    }

    #[test]
    fn test_get_settings_response_roundtrip() {
        let original = GetSettingsResponse {
            settings: vec![
                SettingItem {
                    key: "robot.speed_limit".to_string(),
                    value: Value::Number(1.5),
                    updated_at_ms: 1706800000000,
                    updated_by: "fleet_manager".to_string(),
                },
                SettingItem {
                    key: "robot.name".to_string(),
                    value: Value::String("Robot-42".to_string()),
                    updated_at_ms: 1706799000000,
                    updated_by: "".to_string(),
                },
                SettingItem {
                    key: "debug.enabled".to_string(),
                    value: Value::Bool(true),
                    updated_at_ms: 1706798000000,
                    updated_by: "admin".to_string(),
                },
                SettingItem {
                    key: "robot.max_retries".to_string(),
                    value: Value::Int(3),
                    updated_at_ms: 1706797000000,
                    updated_by: "robot".to_string(),
                },
                SettingItem {
                    key: "robot.mode".to_string(),
                    value: Value::Enum("autonomous".to_string()),
                    updated_at_ms: 1706796000000,
                    updated_by: "operator".to_string(),
                },
            ],
            not_found: vec!["nonexistent.key".to_string(), "other.missing".to_string()],
        };

        let serialized = serialize_get_settings_response(&original).expect("Failed to serialize");
        let deserialized =
            deserialize_get_settings_response(&serialized).expect("Failed to deserialize");

        assert_eq!(original, deserialized);
    }

    #[test]
    fn test_get_settings_response_empty_roundtrip() {
        let original = GetSettingsResponse {
            settings: vec![],
            not_found: vec![],
        };

        let serialized = serialize_get_settings_response(&original).expect("Failed to serialize");
        let deserialized =
            deserialize_get_settings_response(&serialized).expect("Failed to deserialize");

        assert_eq!(original, deserialized);
    }

    #[test]
    fn test_get_settings_response_no_not_found_roundtrip() {
        let original = GetSettingsResponse {
            settings: vec![SettingItem {
                key: "test.setting".to_string(),
                value: Value::String("test_value".to_string()),
                updated_at_ms: 1706800000000,
                updated_by: "test".to_string(),
            }],
            not_found: vec![],
        };

        let serialized = serialize_get_settings_response(&original).expect("Failed to serialize");
        let deserialized =
            deserialize_get_settings_response(&serialized).expect("Failed to deserialize");

        assert_eq!(original, deserialized);
    }
}