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 feature query messages.

use crate::capnp_messages::{read_feature, write_feature};
use crate::features_query::{
    FeatureRef, GetFeaturesRequest, GetFeaturesResponse, QueryFeatureKind,
};

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

use std::io::Cursor;

// Enum conversion helpers

fn query_feature_kind_to_capnp(
    kind: QueryFeatureKind,
) -> crate::features_query_capnp::QueryFeatureKind {
    match kind {
        QueryFeatureKind::Route => crate::features_query_capnp::QueryFeatureKind::Route,
        QueryFeatureKind::Shape => crate::features_query_capnp::QueryFeatureKind::Shape,
    }
}

fn query_feature_kind_from_capnp(
    kind: crate::features_query_capnp::QueryFeatureKind,
) -> QueryFeatureKind {
    match kind {
        crate::features_query_capnp::QueryFeatureKind::Route => QueryFeatureKind::Route,
        crate::features_query_capnp::QueryFeatureKind::Shape => QueryFeatureKind::Shape,
    }
}

// Write helpers

fn write_feature_ref(
    builder: &mut crate::features_query_capnp::feature_ref::Builder,
    feature_ref: &FeatureRef,
) {
    builder.set_id(&feature_ref.id);
    builder.set_kind(query_feature_kind_to_capnp(feature_ref.kind));
}

// Read helpers

fn read_feature_ref(
    reader: crate::features_query_capnp::feature_ref::Reader,
) -> Result<FeatureRef, ::capnp::Error> {
    Ok(FeatureRef {
        id: reader.get_id()?.to_str()?.to_string(),
        kind: query_feature_kind_from_capnp(reader.get_kind()?),
    })
}

// Public serialization API

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

    let mut refs_builder = builder
        .reborrow()
        .init_feature_refs(request.feature_refs.len() as u32);
    for (i, feature_ref) in request.feature_refs.iter().enumerate() {
        let mut ref_builder = refs_builder.reborrow().get(i as u32);
        write_feature_ref(&mut ref_builder, feature_ref);
    }

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

/// Deserialize a GetFeaturesRequest from Cap'n Proto bytes
pub fn deserialize_get_features_request(data: &[u8]) -> Result<GetFeaturesRequest, ::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::features_query_capnp::get_features_request::Reader>()?;

    let refs_reader = reader.get_feature_refs()?;
    let mut feature_refs = Vec::with_capacity(refs_reader.len() as usize);
    for i in 0..refs_reader.len() {
        feature_refs.push(read_feature_ref(refs_reader.get(i))?);
    }

    Ok(GetFeaturesRequest { feature_refs })
}

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

    let mut items_builder = builder.reborrow().init_items(response.items.len() as u32);
    for (i, item) in response.items.iter().enumerate() {
        let mut item_builder = items_builder.reborrow().get(i as u32);
        write_feature(&mut item_builder, item);
    }

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

/// Deserialize a GetFeaturesResponse from Cap'n Proto bytes
pub fn deserialize_get_features_response(
    data: &[u8],
) -> Result<GetFeaturesResponse, ::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::features_query_capnp::get_features_response::Reader>()?;

    let items_reader = reader.get_items()?;
    let mut items = Vec::with_capacity(items_reader.len() as usize);
    for i in 0..items_reader.len() {
        items.push(read_feature(items_reader.get(i))?);
    }

    Ok(GetFeaturesResponse { items })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core_model::{AssetIdentity, Feature, LayerRole, ScopeType};
    use crate::spatial_types::{
        GeoPoint, GeoPose, SpatialData, Wgs84PolygonData, Wgs84Route, Wgs84Shape,
    };

    #[test]
    fn test_get_features_request_roundtrip() {
        let original = GetFeaturesRequest {
            feature_refs: vec![
                FeatureRef {
                    id: "Route-123".to_string(),
                    kind: QueryFeatureKind::Route,
                },
                FeatureRef {
                    id: "geom-456".to_string(),
                    kind: QueryFeatureKind::Shape,
                },
            ],
        };

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

        assert_eq!(original, deserialized);
    }

    #[test]
    fn test_get_features_response_roundtrip() {
        let original = GetFeaturesResponse {
            items: vec![
                Feature {
                    identity: AssetIdentity {
                        id: "route-123".to_string(),
                        name: "Test Route".to_string(),
                        rev: 1,
                        hash: "abc123".to_string(),
                    },
                    layer_role: LayerRole::Target,
                    scope: ScopeType::Fleet,
                    data: SpatialData::Wgs84Route(Wgs84Route::Path(vec![
                        GeoPose {
                            lat_deg: 37.7749,
                            lon_deg: -122.4194,
                            altitude_m: 10.0,
                            heading_deg: 45.0,
                        },
                        GeoPose {
                            lat_deg: 37.7750,
                            lon_deg: -122.4195,
                            altitude_m: 10.0,
                            heading_deg: 90.0,
                        },
                    ])),
                    expires_at_ms: 0,
                    min_z_m: 0.0,
                    max_z_m: 0.0,
                    level_id: 0,
                },
                Feature {
                    identity: AssetIdentity {
                        id: "shape-456".to_string(),
                        name: "Test Shape".to_string(),
                        rev: 2,
                        hash: "def456".to_string(),
                    },
                    layer_role: LayerRole::WorkArea,
                    scope: ScopeType::Fleet,
                    data: SpatialData::Wgs84Shape(Wgs84Shape::Polygon(Wgs84PolygonData {
                        exterior: vec![
                            GeoPoint {
                                lon: -122.4194,
                                lat: 37.7749,
                            },
                            GeoPoint {
                                lon: -122.4195,
                                lat: 37.7750,
                            },
                            GeoPoint {
                                lon: -122.4196,
                                lat: 37.7749,
                            },
                        ],
                        holes: vec![],
                    })),
                    expires_at_ms: 0,
                    min_z_m: 0.0,
                    max_z_m: 100.0,
                    level_id: 0,
                },
            ],
        };

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

        assert_eq!(original, deserialized);
    }

    #[test]
    fn test_empty_request_roundtrip() {
        let original = GetFeaturesRequest {
            feature_refs: vec![],
        };

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

        assert_eq!(original, deserialized);
    }

    #[test]
    fn test_empty_response_roundtrip() {
        let original = GetFeaturesResponse { items: vec![] };

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

        assert_eq!(original, deserialized);
    }
}