Skip to main content

hiroz_schema/
lib.rs

1//! ROS 2 Type Description Schema
2//!
3//! This crate provides the canonical schema types for ROS 2 type descriptions,
4//! used by hiroz for:
5//! - Type hash (RIHS01) computation
6//! - Type description service wire format
7//! - Dynamic message schema conversion
8//! - Python binding type ID mapping
9//!
10//! The `TypeDescription` type matches the ROS 2 `type_description_interfaces` exactly,
11//! making it the single source of truth for type information after parsing.
12
13mod hash;
14mod type_description;
15mod type_id;
16
17pub use hash::{calculate_hash, to_ros2_json};
18pub use type_description::{
19    FieldDescription, FieldTypeDescription, TypeDescription, TypeDescriptionMsg, to_hash_version,
20};
21pub use type_id::TypeId;
22
23/// RIHS01 type hash (32 bytes)
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct TypeHash(pub [u8; 32]);
26
27impl TypeHash {
28    /// Convert type hash to RIHS01 string format
29    pub fn to_rihs_string(&self) -> String {
30        format!("RIHS01_{}", hex::encode(self.0))
31    }
32
33    /// Parse RIHS01 string format to type hash
34    pub fn from_rihs_string(s: &str) -> Result<Self, String> {
35        if !s.starts_with("RIHS01_") {
36            return Err("Invalid RIHS01 format: must start with 'RIHS01_'".to_string());
37        }
38
39        let hex_part = &s[7..];
40        let bytes = hex::decode(hex_part).map_err(|e| format!("Invalid hex encoding: {}", e))?;
41
42        if bytes.len() != 32 {
43            return Err(format!("Hash must be 32 bytes, got {}", bytes.len()));
44        }
45
46        let mut hash = [0u8; 32];
47        hash.copy_from_slice(&bytes);
48        Ok(TypeHash(hash))
49    }
50
51    /// Create a zero (placeholder) type hash for Humble compatibility
52    pub fn zero() -> Self {
53        TypeHash([0u8; 32])
54    }
55}
56
57impl std::fmt::Display for TypeHash {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        write!(f, "{}", self.to_rihs_string())
60    }
61}
62
63impl Default for TypeHash {
64    fn default() -> Self {
65        Self::zero()
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn test_type_hash_roundtrip() {
75        let hash = TypeHash([
76            0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
77            0x77, 0x88, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
78            0x66, 0x77, 0x88, 0x99,
79        ]);
80
81        let s = hash.to_rihs_string();
82        assert!(s.starts_with("RIHS01_"));
83        assert_eq!(s.len(), 7 + 64); // "RIHS01_" + 64 hex chars
84
85        let decoded = TypeHash::from_rihs_string(&s).unwrap();
86        assert_eq!(hash, decoded);
87    }
88
89    #[test]
90    fn test_type_hash_invalid_prefix() {
91        let result = TypeHash::from_rihs_string("INVALID_1234");
92        assert!(result.is_err());
93    }
94
95    #[test]
96    fn test_type_hash_invalid_length() {
97        let result = TypeHash::from_rihs_string("RIHS01_1234");
98        assert!(result.is_err());
99    }
100}