Skip to main content

ros2_types/
types.rs

1//! Type description data structures
2//!
3//! These structures match the ROS2 type_description_interfaces
4
5use serde::{Deserialize, Serialize};
6
7/// Complete type description message including referenced types
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct TypeDescriptionMsg {
10    /// The main type being described
11    pub type_description: IndividualTypeDescription,
12    /// All types referenced by the main type
13    pub referenced_type_descriptions: Vec<IndividualTypeDescription>,
14}
15
16/// Description of a single type
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct IndividualTypeDescription {
19    /// Fully qualified type name (e.g., "std_msgs/msg/Header")
20    pub type_name: String,
21    /// Fields in this type
22    pub fields: Vec<Field>,
23}
24
25/// Description of a field in a type
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Field {
28    /// Field name
29    pub name: String,
30    /// Field type information
31    #[serde(rename = "type")]
32    pub field_type: FieldType,
33    /// Default value (empty string if none)
34    pub default_value: String,
35}
36
37/// Type information for a field
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct FieldType {
40    /// Type ID from FieldType.msg constants
41    pub type_id: u8,
42    /// Array/sequence capacity (0 if not applicable)
43    pub capacity: u64,
44    /// String capacity (0 if not applicable)
45    pub string_capacity: u64,
46    /// Nested type name (empty if not a nested type)
47    pub nested_type_name: String,
48}
49
50// Field type constants matching type_description_interfaces/msg/FieldType.msg
51pub const FIELD_TYPE_NOT_SET: u8 = 0;
52pub const FIELD_TYPE_NESTED_TYPE: u8 = 1;
53pub const FIELD_TYPE_INT8: u8 = 2;
54pub const FIELD_TYPE_UINT8: u8 = 3;
55pub const FIELD_TYPE_INT16: u8 = 4;
56pub const FIELD_TYPE_UINT16: u8 = 5;
57pub const FIELD_TYPE_INT32: u8 = 6;
58pub const FIELD_TYPE_UINT32: u8 = 7;
59pub const FIELD_TYPE_INT64: u8 = 8;
60pub const FIELD_TYPE_UINT64: u8 = 9;
61pub const FIELD_TYPE_FLOAT: u8 = 10;
62pub const FIELD_TYPE_DOUBLE: u8 = 11;
63pub const FIELD_TYPE_LONG_DOUBLE: u8 = 12;
64pub const FIELD_TYPE_CHAR: u8 = 13;
65pub const FIELD_TYPE_WCHAR: u8 = 14;
66pub const FIELD_TYPE_BOOLEAN: u8 = 15;
67pub const FIELD_TYPE_BYTE: u8 = 16;
68pub const FIELD_TYPE_STRING: u8 = 17;
69pub const FIELD_TYPE_WSTRING: u8 = 18;
70pub const FIELD_TYPE_FIXED_STRING: u8 = 19;
71pub const FIELD_TYPE_FIXED_WSTRING: u8 = 20;
72pub const FIELD_TYPE_BOUNDED_STRING: u8 = 21;
73pub const FIELD_TYPE_BOUNDED_WSTRING: u8 = 22;
74
75// Fixed-size arrays (49-96)
76pub const FIELD_TYPE_NESTED_TYPE_ARRAY: u8 = 49;
77pub const FIELD_TYPE_INT8_ARRAY: u8 = 50;
78pub const FIELD_TYPE_UINT8_ARRAY: u8 = 51;
79pub const FIELD_TYPE_INT16_ARRAY: u8 = 52;
80pub const FIELD_TYPE_UINT16_ARRAY: u8 = 53;
81pub const FIELD_TYPE_INT32_ARRAY: u8 = 54;
82pub const FIELD_TYPE_UINT32_ARRAY: u8 = 55;
83pub const FIELD_TYPE_INT64_ARRAY: u8 = 56;
84pub const FIELD_TYPE_UINT64_ARRAY: u8 = 57;
85pub const FIELD_TYPE_FLOAT_ARRAY: u8 = 58;
86pub const FIELD_TYPE_DOUBLE_ARRAY: u8 = 59;
87pub const FIELD_TYPE_LONG_DOUBLE_ARRAY: u8 = 60;
88pub const FIELD_TYPE_CHAR_ARRAY: u8 = 61;
89pub const FIELD_TYPE_WCHAR_ARRAY: u8 = 62;
90pub const FIELD_TYPE_BOOLEAN_ARRAY: u8 = 63;
91pub const FIELD_TYPE_BYTE_ARRAY: u8 = 64;
92pub const FIELD_TYPE_STRING_ARRAY: u8 = 65;
93pub const FIELD_TYPE_WSTRING_ARRAY: u8 = 66;
94
95// Bounded sequences (97-144)
96pub const FIELD_TYPE_NESTED_TYPE_BOUNDED_SEQUENCE: u8 = 97;
97pub const FIELD_TYPE_INT8_BOUNDED_SEQUENCE: u8 = 98;
98pub const FIELD_TYPE_UINT8_BOUNDED_SEQUENCE: u8 = 99;
99pub const FIELD_TYPE_INT16_BOUNDED_SEQUENCE: u8 = 100;
100pub const FIELD_TYPE_UINT16_BOUNDED_SEQUENCE: u8 = 101;
101pub const FIELD_TYPE_INT32_BOUNDED_SEQUENCE: u8 = 102;
102pub const FIELD_TYPE_UINT32_BOUNDED_SEQUENCE: u8 = 103;
103pub const FIELD_TYPE_INT64_BOUNDED_SEQUENCE: u8 = 104;
104pub const FIELD_TYPE_UINT64_BOUNDED_SEQUENCE: u8 = 105;
105pub const FIELD_TYPE_FLOAT_BOUNDED_SEQUENCE: u8 = 106;
106pub const FIELD_TYPE_DOUBLE_BOUNDED_SEQUENCE: u8 = 107;
107pub const FIELD_TYPE_LONG_DOUBLE_BOUNDED_SEQUENCE: u8 = 108;
108pub const FIELD_TYPE_CHAR_BOUNDED_SEQUENCE: u8 = 109;
109pub const FIELD_TYPE_WCHAR_BOUNDED_SEQUENCE: u8 = 110;
110pub const FIELD_TYPE_BOOLEAN_BOUNDED_SEQUENCE: u8 = 111;
111pub const FIELD_TYPE_BYTE_BOUNDED_SEQUENCE: u8 = 112;
112pub const FIELD_TYPE_STRING_BOUNDED_SEQUENCE: u8 = 113;
113pub const FIELD_TYPE_WSTRING_BOUNDED_SEQUENCE: u8 = 114;
114
115// Unbounded sequences (145-192)
116pub const FIELD_TYPE_NESTED_TYPE_UNBOUNDED_SEQUENCE: u8 = 145;
117pub const FIELD_TYPE_INT8_UNBOUNDED_SEQUENCE: u8 = 146;
118pub const FIELD_TYPE_UINT8_UNBOUNDED_SEQUENCE: u8 = 147;
119pub const FIELD_TYPE_INT16_UNBOUNDED_SEQUENCE: u8 = 148;
120pub const FIELD_TYPE_UINT16_UNBOUNDED_SEQUENCE: u8 = 149;
121pub const FIELD_TYPE_INT32_UNBOUNDED_SEQUENCE: u8 = 150;
122pub const FIELD_TYPE_UINT32_UNBOUNDED_SEQUENCE: u8 = 151;
123pub const FIELD_TYPE_INT64_UNBOUNDED_SEQUENCE: u8 = 152;
124pub const FIELD_TYPE_UINT64_UNBOUNDED_SEQUENCE: u8 = 153;
125pub const FIELD_TYPE_FLOAT_UNBOUNDED_SEQUENCE: u8 = 154;
126pub const FIELD_TYPE_DOUBLE_UNBOUNDED_SEQUENCE: u8 = 155;
127pub const FIELD_TYPE_LONG_DOUBLE_UNBOUNDED_SEQUENCE: u8 = 156;
128pub const FIELD_TYPE_CHAR_UNBOUNDED_SEQUENCE: u8 = 157;
129pub const FIELD_TYPE_WCHAR_UNBOUNDED_SEQUENCE: u8 = 158;
130pub const FIELD_TYPE_BOOLEAN_UNBOUNDED_SEQUENCE: u8 = 159;
131pub const FIELD_TYPE_BYTE_UNBOUNDED_SEQUENCE: u8 = 160;
132pub const FIELD_TYPE_STRING_UNBOUNDED_SEQUENCE: u8 = 161;
133pub const FIELD_TYPE_WSTRING_UNBOUNDED_SEQUENCE: u8 = 162;
134
135impl FieldType {
136    /// Create a primitive field type
137    pub fn primitive(type_id: u8) -> Self {
138        Self {
139            type_id,
140            capacity: 0,
141            string_capacity: 0,
142            nested_type_name: String::new(),
143        }
144    }
145
146    /// Create a nested type field
147    pub fn nested(type_name: impl Into<String>) -> Self {
148        Self {
149            type_id: FIELD_TYPE_NESTED_TYPE,
150            capacity: 0,
151            string_capacity: 0,
152            nested_type_name: type_name.into(),
153        }
154    }
155
156    /// Create an unbounded sequence of nested types (Vec<NestedType>)
157    pub fn nested_sequence(type_name: impl Into<String>) -> Self {
158        Self {
159            type_id: FIELD_TYPE_NESTED_TYPE_UNBOUNDED_SEQUENCE,
160            capacity: 0,
161            string_capacity: 0,
162            nested_type_name: type_name.into(),
163        }
164    }
165
166    /// Create a fixed-size array field type (e.g., [T; N])
167    /// For nested types: [NestedType; N]
168    /// For primitives: [primitive; N]
169    pub fn array(base_type_id: u8, capacity: u64) -> Self {
170        let array_type_id = match base_type_id {
171            FIELD_TYPE_INT8 => FIELD_TYPE_INT8_ARRAY,
172            FIELD_TYPE_UINT8 => FIELD_TYPE_UINT8_ARRAY,
173            FIELD_TYPE_INT16 => FIELD_TYPE_INT16_ARRAY,
174            FIELD_TYPE_UINT16 => FIELD_TYPE_UINT16_ARRAY,
175            FIELD_TYPE_INT32 => FIELD_TYPE_INT32_ARRAY,
176            FIELD_TYPE_UINT32 => FIELD_TYPE_UINT32_ARRAY,
177            FIELD_TYPE_INT64 => FIELD_TYPE_INT64_ARRAY,
178            FIELD_TYPE_UINT64 => FIELD_TYPE_UINT64_ARRAY,
179            FIELD_TYPE_FLOAT => FIELD_TYPE_FLOAT_ARRAY,
180            FIELD_TYPE_DOUBLE => FIELD_TYPE_DOUBLE_ARRAY,
181            FIELD_TYPE_LONG_DOUBLE => FIELD_TYPE_LONG_DOUBLE_ARRAY,
182            FIELD_TYPE_CHAR => FIELD_TYPE_CHAR_ARRAY,
183            FIELD_TYPE_WCHAR => FIELD_TYPE_WCHAR_ARRAY,
184            FIELD_TYPE_BOOLEAN => FIELD_TYPE_BOOLEAN_ARRAY,
185            FIELD_TYPE_BYTE => FIELD_TYPE_BYTE_ARRAY,
186            FIELD_TYPE_STRING => FIELD_TYPE_STRING_ARRAY,
187            FIELD_TYPE_WSTRING => FIELD_TYPE_WSTRING_ARRAY,
188            _ => base_type_id, // Fallback
189        };
190        Self {
191            type_id: array_type_id,
192            capacity,
193            string_capacity: 0,
194            nested_type_name: String::new(),
195        }
196    }
197
198    /// Create a fixed-size array of nested types
199    pub fn nested_array(type_name: impl Into<String>, capacity: u64) -> Self {
200        Self {
201            type_id: FIELD_TYPE_NESTED_TYPE_ARRAY,
202            capacity,
203            string_capacity: 0,
204            nested_type_name: type_name.into(),
205        }
206    }
207
208    /// Create an unbounded sequence (Vec) of primitives
209    pub fn sequence(base_type_id: u8) -> Self {
210        let sequence_type_id = match base_type_id {
211            FIELD_TYPE_INT8 => FIELD_TYPE_INT8_UNBOUNDED_SEQUENCE,
212            FIELD_TYPE_UINT8 => FIELD_TYPE_UINT8_UNBOUNDED_SEQUENCE,
213            FIELD_TYPE_INT16 => FIELD_TYPE_INT16_UNBOUNDED_SEQUENCE,
214            FIELD_TYPE_UINT16 => FIELD_TYPE_UINT16_UNBOUNDED_SEQUENCE,
215            FIELD_TYPE_INT32 => FIELD_TYPE_INT32_UNBOUNDED_SEQUENCE,
216            FIELD_TYPE_UINT32 => FIELD_TYPE_UINT32_UNBOUNDED_SEQUENCE,
217            FIELD_TYPE_INT64 => FIELD_TYPE_INT64_UNBOUNDED_SEQUENCE,
218            FIELD_TYPE_UINT64 => FIELD_TYPE_UINT64_UNBOUNDED_SEQUENCE,
219            FIELD_TYPE_FLOAT => FIELD_TYPE_FLOAT_UNBOUNDED_SEQUENCE,
220            FIELD_TYPE_DOUBLE => FIELD_TYPE_DOUBLE_UNBOUNDED_SEQUENCE,
221            FIELD_TYPE_LONG_DOUBLE => FIELD_TYPE_LONG_DOUBLE_UNBOUNDED_SEQUENCE,
222            FIELD_TYPE_CHAR => FIELD_TYPE_CHAR_UNBOUNDED_SEQUENCE,
223            FIELD_TYPE_WCHAR => FIELD_TYPE_WCHAR_UNBOUNDED_SEQUENCE,
224            FIELD_TYPE_BOOLEAN => FIELD_TYPE_BOOLEAN_UNBOUNDED_SEQUENCE,
225            FIELD_TYPE_BYTE => FIELD_TYPE_BYTE_UNBOUNDED_SEQUENCE,
226            FIELD_TYPE_STRING => FIELD_TYPE_STRING_UNBOUNDED_SEQUENCE,
227            FIELD_TYPE_WSTRING => FIELD_TYPE_WSTRING_UNBOUNDED_SEQUENCE,
228            _ => base_type_id, // Fallback
229        };
230        Self {
231            type_id: sequence_type_id,
232            capacity: 0,
233            string_capacity: 0,
234            nested_type_name: String::new(),
235        }
236    }
237
238    /// Create a string field type with capacity
239    pub fn string_with_capacity(type_id: u8, string_capacity: u64) -> Self {
240        Self {
241            type_id,
242            capacity: 0,
243            string_capacity,
244            nested_type_name: String::new(),
245        }
246    }
247
248    /// Create a bounded string (string with maximum size)
249    pub fn bounded_string(string_capacity: u64) -> Self {
250        Self {
251            type_id: FIELD_TYPE_BOUNDED_STRING,
252            capacity: 0,
253            string_capacity,
254            nested_type_name: String::new(),
255        }
256    }
257
258    /// Create a bounded wstring (wide string with maximum size)
259    pub fn bounded_wstring(string_capacity: u64) -> Self {
260        Self {
261            type_id: FIELD_TYPE_BOUNDED_WSTRING,
262            capacity: 0,
263            string_capacity,
264            nested_type_name: String::new(),
265        }
266    }
267
268    /// Create a bounded sequence of primitives
269    pub fn bounded_sequence(base_type_id: u8, capacity: u64) -> Self {
270        let sequence_type_id = match base_type_id {
271            FIELD_TYPE_INT8 => FIELD_TYPE_INT8_BOUNDED_SEQUENCE,
272            FIELD_TYPE_UINT8 => FIELD_TYPE_UINT8_BOUNDED_SEQUENCE,
273            FIELD_TYPE_INT16 => FIELD_TYPE_INT16_BOUNDED_SEQUENCE,
274            FIELD_TYPE_UINT16 => FIELD_TYPE_UINT16_BOUNDED_SEQUENCE,
275            FIELD_TYPE_INT32 => FIELD_TYPE_INT32_BOUNDED_SEQUENCE,
276            FIELD_TYPE_UINT32 => FIELD_TYPE_UINT32_BOUNDED_SEQUENCE,
277            FIELD_TYPE_INT64 => FIELD_TYPE_INT64_BOUNDED_SEQUENCE,
278            FIELD_TYPE_UINT64 => FIELD_TYPE_UINT64_BOUNDED_SEQUENCE,
279            FIELD_TYPE_FLOAT => FIELD_TYPE_FLOAT_BOUNDED_SEQUENCE,
280            FIELD_TYPE_DOUBLE => FIELD_TYPE_DOUBLE_BOUNDED_SEQUENCE,
281            FIELD_TYPE_LONG_DOUBLE => FIELD_TYPE_LONG_DOUBLE_BOUNDED_SEQUENCE,
282            FIELD_TYPE_CHAR => FIELD_TYPE_CHAR_BOUNDED_SEQUENCE,
283            FIELD_TYPE_WCHAR => FIELD_TYPE_WCHAR_BOUNDED_SEQUENCE,
284            FIELD_TYPE_BOOLEAN => FIELD_TYPE_BOOLEAN_BOUNDED_SEQUENCE,
285            FIELD_TYPE_BYTE => FIELD_TYPE_BYTE_BOUNDED_SEQUENCE,
286            FIELD_TYPE_STRING => FIELD_TYPE_STRING_BOUNDED_SEQUENCE,
287            FIELD_TYPE_WSTRING => FIELD_TYPE_WSTRING_BOUNDED_SEQUENCE,
288            _ => base_type_id, // Fallback
289        };
290        Self {
291            type_id: sequence_type_id,
292            capacity,
293            string_capacity: 0,
294            nested_type_name: String::new(),
295        }
296    }
297
298    /// Create a bounded sequence with string_capacity for bounded string elements
299    /// Used for sequence<string<M>, N> where M is string_capacity and N is capacity
300    pub fn bounded_sequence_with_string_capacity(
301        base_type_id: u8,
302        capacity: u64,
303        string_capacity: u64,
304    ) -> Self {
305        let mut ft = Self::bounded_sequence(base_type_id, capacity);
306        ft.string_capacity = string_capacity;
307        ft
308    }
309
310    /// Create an unbounded sequence with string_capacity for bounded string elements
311    /// Used for sequence<string<M>> where M is string_capacity
312    pub fn sequence_with_string_capacity(base_type_id: u8, string_capacity: u64) -> Self {
313        let mut ft = Self::sequence(base_type_id);
314        ft.string_capacity = string_capacity;
315        ft
316    }
317
318    /// Create a bounded sequence of nested types
319    pub fn nested_bounded_sequence(type_name: impl Into<String>, capacity: u64) -> Self {
320        Self {
321            type_id: FIELD_TYPE_NESTED_TYPE_BOUNDED_SEQUENCE,
322            capacity,
323            string_capacity: 0,
324            nested_type_name: type_name.into(),
325        }
326    }
327}
328
329impl Field {
330    /// Create a new field
331    pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
332        Self {
333            name: name.into(),
334            field_type,
335            default_value: String::new(),
336        }
337    }
338
339    /// Create a new field with a default value
340    pub fn with_default(
341        name: impl Into<String>,
342        field_type: FieldType,
343        default_value: impl Into<String>,
344    ) -> Self {
345        Self {
346            name: name.into(),
347            field_type,
348            default_value: default_value.into(),
349        }
350    }
351}
352
353impl IndividualTypeDescription {
354    /// Create a new type description
355    pub fn new(type_name: impl Into<String>, fields: Vec<Field>) -> Self {
356        Self {
357            type_name: type_name.into(),
358            fields,
359        }
360    }
361}
362
363impl TypeDescriptionMsg {
364    /// Create a new type description message
365    pub fn new(
366        type_description: IndividualTypeDescription,
367        referenced_type_descriptions: Vec<IndividualTypeDescription>,
368    ) -> Self {
369        Self {
370            type_description,
371            referenced_type_descriptions,
372        }
373    }
374}
375
376// ============================================================================
377// IDL and MSG definition generation
378// ============================================================================
379
380use std::collections::BTreeSet;
381use std::fmt::Write;
382
383/// Extract base type_id from array/sequence type_id.
384/// Returns (base_type_id, kind) where kind is 'p' (plain), 'a' (array),
385/// 'b' (bounded sequence), 'u' (unbounded sequence).
386fn decompose_type_id(type_id: u8) -> (u8, char) {
387    match type_id {
388        0..=48 => (type_id, 'p'),
389        49..=96 => (type_id - 48, 'a'),
390        97..=144 => (type_id - 96, 'b'),
391        145..=192 => (type_id - 144, 'u'),
392        _ => (type_id, 'p'),
393    }
394}
395
396/// Map a base type_id to its IDL primitive type name.
397fn base_type_to_idl(base_type_id: u8) -> &'static str {
398    match base_type_id {
399        FIELD_TYPE_NESTED_TYPE => "", // handled separately
400        FIELD_TYPE_INT8 => "int8",
401        FIELD_TYPE_UINT8 => "uint8",
402        FIELD_TYPE_INT16 => "int16",
403        FIELD_TYPE_UINT16 => "uint16",
404        FIELD_TYPE_INT32 => "int32",
405        FIELD_TYPE_UINT32 => "uint32",
406        FIELD_TYPE_INT64 => "int64",
407        FIELD_TYPE_UINT64 => "uint64",
408        FIELD_TYPE_FLOAT => "float",
409        FIELD_TYPE_DOUBLE => "double",
410        FIELD_TYPE_LONG_DOUBLE => "long double",
411        FIELD_TYPE_CHAR => "uint8", // ROS2 char = uint8
412        FIELD_TYPE_WCHAR => "wchar",
413        FIELD_TYPE_BOOLEAN => "boolean",
414        FIELD_TYPE_BYTE => "octet",
415        FIELD_TYPE_STRING => "string",
416        FIELD_TYPE_WSTRING => "wstring",
417        FIELD_TYPE_FIXED_STRING => "string",
418        FIELD_TYPE_FIXED_WSTRING => "wstring",
419        FIELD_TYPE_BOUNDED_STRING => "string",
420        FIELD_TYPE_BOUNDED_WSTRING => "wstring",
421        _ => "unknown",
422    }
423}
424
425/// Map a base type_id to its .msg primitive type name.
426fn base_type_to_msg(base_type_id: u8) -> &'static str {
427    match base_type_id {
428        FIELD_TYPE_NESTED_TYPE => "", // handled separately
429        FIELD_TYPE_INT8 => "int8",
430        FIELD_TYPE_UINT8 => "uint8",
431        FIELD_TYPE_INT16 => "int16",
432        FIELD_TYPE_UINT16 => "uint16",
433        FIELD_TYPE_INT32 => "int32",
434        FIELD_TYPE_UINT32 => "uint32",
435        FIELD_TYPE_INT64 => "int64",
436        FIELD_TYPE_UINT64 => "uint64",
437        FIELD_TYPE_FLOAT => "float32",
438        FIELD_TYPE_DOUBLE => "float64",
439        FIELD_TYPE_LONG_DOUBLE => "float64",
440        FIELD_TYPE_CHAR => "char",
441        FIELD_TYPE_WCHAR => "char",
442        FIELD_TYPE_BOOLEAN => "bool",
443        FIELD_TYPE_BYTE => "byte",
444        FIELD_TYPE_STRING => "string",
445        FIELD_TYPE_WSTRING => "wstring",
446        FIELD_TYPE_FIXED_STRING => "string",
447        FIELD_TYPE_FIXED_WSTRING => "wstring",
448        FIELD_TYPE_BOUNDED_STRING => "string",
449        FIELD_TYPE_BOUNDED_WSTRING => "wstring",
450        _ => "unknown",
451    }
452}
453
454/// Split a fully-qualified type name like "pkg/msg/TypeName" into (pkg, msg_type, name).
455fn split_type_name(fqn: &str) -> (&str, &str, &str) {
456    let parts: Vec<&str> = fqn.splitn(3, '/').collect();
457    match parts.len() {
458        3 => (parts[0], parts[1], parts[2]),
459        2 => (parts[0], "msg", parts[1]),
460        _ => ("", "msg", fqn),
461    }
462}
463
464/// Convert a "pkg/msg/TypeName" to IDL module-qualified form "pkg::msg::TypeName".
465fn type_name_to_idl_qualified(fqn: &str) -> String {
466    fqn.replace('/', "::")
467}
468
469impl TypeDescriptionMsg {
470    /// Generate an OMG IDL representation of this type description.
471    ///
472    /// All referenced types are inlined into a single IDL string.
473    /// This is suitable for MCAP schema data with `schema_encoding = "ros2idl"`.
474    pub fn to_idl(&self) -> String {
475        let mut output = String::new();
476        let mut typedefs_needed = BTreeSet::new();
477
478        // Emit referenced types first (dependencies before dependents)
479        for ref_type in &self.referenced_type_descriptions {
480            Self::emit_idl_struct(&mut output, ref_type, &mut typedefs_needed);
481            output.push('\n');
482        }
483
484        // Emit main type
485        Self::emit_idl_struct(&mut output, &self.type_description, &mut typedefs_needed);
486        output
487    }
488
489    fn emit_idl_struct(
490        output: &mut String,
491        desc: &IndividualTypeDescription,
492        typedefs_needed: &mut BTreeSet<String>,
493    ) {
494        let (pkg, msg_type, name) = split_type_name(&desc.type_name);
495
496        // Collect typedefs for fixed-size arrays in this struct
497        let mut local_typedefs = Vec::new();
498        for field in &desc.fields {
499            let (base_id, kind) = decompose_type_id(field.field_type.type_id);
500            if kind == 'a' {
501                let idl_base = if base_id == FIELD_TYPE_NESTED_TYPE {
502                    type_name_to_idl_qualified(&field.field_type.nested_type_name)
503                } else {
504                    base_type_to_idl(base_id).to_string()
505                };
506                let cap = field.field_type.capacity;
507                let typedef_name = format!(
508                    "{}__{}",
509                    idl_base.replace("::", "__").replace(' ', "_"),
510                    cap
511                );
512                if typedefs_needed.insert(typedef_name.clone()) {
513                    local_typedefs.push((idl_base, typedef_name, cap));
514                }
515            }
516        }
517
518        // Open modules
519        if !pkg.is_empty() {
520            let _ = writeln!(output, "module {pkg} {{");
521            let _ = writeln!(output, "  module {msg_type} {{");
522        }
523
524        let indent = if pkg.is_empty() { "" } else { "    " };
525
526        // Emit typedefs
527        for (base, alias, cap) in &local_typedefs {
528            let _ = writeln!(output, "{indent}typedef {base} {alias}[{cap}];");
529        }
530        if !local_typedefs.is_empty() {
531            output.push('\n');
532        }
533
534        // Emit struct
535        let _ = writeln!(output, "{indent}struct {name} {{");
536        for field in &desc.fields {
537            let idl_type = Self::field_type_to_idl(&field.field_type, typedefs_needed);
538            let _ = writeln!(output, "{indent}  {idl_type} {name_};", name_ = field.name);
539        }
540        let _ = writeln!(output, "{indent}}};");
541
542        // Close modules
543        if !pkg.is_empty() {
544            let _ = writeln!(output, "  }};");
545            let _ = writeln!(output, "}};");
546        }
547    }
548
549    fn field_type_to_idl(ft: &FieldType, typedefs_needed: &mut BTreeSet<String>) -> String {
550        let (base_id, kind) = decompose_type_id(ft.type_id);
551
552        let base_name = if base_id == FIELD_TYPE_NESTED_TYPE {
553            type_name_to_idl_qualified(&ft.nested_type_name)
554        } else {
555            let prim = base_type_to_idl(base_id);
556            // Handle string capacity on base type
557            if ft.string_capacity > 0
558                && matches!(
559                    base_id,
560                    FIELD_TYPE_STRING
561                        | FIELD_TYPE_WSTRING
562                        | FIELD_TYPE_BOUNDED_STRING
563                        | FIELD_TYPE_BOUNDED_WSTRING
564                        | FIELD_TYPE_FIXED_STRING
565                        | FIELD_TYPE_FIXED_WSTRING
566                )
567            {
568                format!("{prim}<{}>", ft.string_capacity)
569            } else {
570                prim.to_string()
571            }
572        };
573
574        match kind {
575            'a' => {
576                // Fixed-size array — use typedef name
577                let typedef_name = format!(
578                    "{}__{}",
579                    base_name.replace("::", "__").replace(' ', "_"),
580                    ft.capacity
581                );
582                typedefs_needed.insert(typedef_name.clone());
583                typedef_name
584            }
585            'b' => {
586                // Bounded sequence
587                format!("sequence<{base_name}, {}>", ft.capacity)
588            }
589            'u' => {
590                // Unbounded sequence
591                format!("sequence<{base_name}>")
592            }
593            _ => base_name,
594        }
595    }
596
597    /// Generate a `.msg` format definition of this type description.
598    ///
599    /// Referenced types are appended after a `===` separator line (Foxglove convention),
600    /// making this suitable for MCAP schema data with `schema_encoding = "ros2msg"`.
601    pub fn to_msg_definition(&self) -> String {
602        let mut output = String::new();
603
604        // Main type definition
605        Self::emit_msg_struct(&mut output, &self.type_description);
606
607        // Referenced types separated by ===
608        for ref_type in &self.referenced_type_descriptions {
609            let _ = writeln!(
610                output,
611                "\n================================================================================"
612            );
613            let _ = writeln!(output, "MSG: {}", ref_type.type_name);
614            Self::emit_msg_struct(&mut output, ref_type);
615        }
616
617        output
618    }
619
620    fn emit_msg_struct(output: &mut String, desc: &IndividualTypeDescription) {
621        for field in &desc.fields {
622            let msg_type = Self::field_type_to_msg(&field.field_type);
623            let _ = writeln!(output, "{msg_type} {}", field.name);
624        }
625    }
626
627    fn field_type_to_msg(ft: &FieldType) -> String {
628        let (base_id, kind) = decompose_type_id(ft.type_id);
629
630        let base_name = if base_id == FIELD_TYPE_NESTED_TYPE {
631            // For nested types in .msg format, use the fqn as-is (e.g., "std_msgs/Header")
632            // Remove the middle segment if it's "msg" to match .msg convention
633            let (pkg, _msg_type, name) = split_type_name(&ft.nested_type_name);
634            if pkg.is_empty() {
635                name.to_string()
636            } else {
637                format!("{pkg}/{name}")
638            }
639        } else {
640            let prim = base_type_to_msg(base_id);
641            // Handle bounded strings
642            if ft.string_capacity > 0
643                && matches!(
644                    base_id,
645                    FIELD_TYPE_BOUNDED_STRING | FIELD_TYPE_BOUNDED_WSTRING
646                )
647            {
648                format!("{prim}<={}>", ft.string_capacity)
649            } else {
650                prim.to_string()
651            }
652        };
653
654        match kind {
655            'a' => {
656                // Fixed-size array
657                format!("{base_name}[{}]", ft.capacity)
658            }
659            'b' => {
660                // Bounded sequence
661                format!("{base_name}[<={}]", ft.capacity)
662            }
663            'u' => {
664                // Unbounded sequence
665                format!("{base_name}[]")
666            }
667            _ => base_name,
668        }
669    }
670}