1use synapse_parser::ast::{
2 ArraySuffix, Attribute, BaseType, ConstDecl, Item, Literal, MessageDef, PrimitiveType,
3 PacketKind, StructDef, SynFile, TypeExpr,
4};
5
6pub const PREAMBLE: &str = "\
10#pragma once
11#include \"cfe.h\"
12
13";
14
15pub struct RustOptions<'a> {
17 pub cfs_module: &'a str,
21 pub tlm_header: &'a str,
23 pub cmd_header: &'a str,
25}
26
27impl Default for RustOptions<'_> {
28 fn default() -> Self {
29 RustOptions {
30 cfs_module: "cfs_sys",
31 tlm_header: "CFE_MSG_TelemetryHeader_t",
32 cmd_header: "CFE_MSG_CommandHeader_t",
33 }
34 }
35}
36
37pub fn generate_c(file: &SynFile) -> String {
39 let mut out = String::from(PREAMBLE);
40 emit_c_imports(file, &mut out);
41 emit_items(file, &mut out);
42 out
43}
44
45pub fn generate_rust(file: &SynFile, opts: &RustOptions) -> String {
51 let mut out = String::new();
52 emit_rust_imports(file, &mut out);
53 emit_rust_items(file, opts, &mut out);
54 out
55}
56
57fn emit_c_imports(file: &SynFile, out: &mut String) {
60 let mut emitted = false;
61 for item in &file.items {
62 if let Item::Import(import) = item {
63 out.push_str(&format!("#include \"{}\"\n", import_c_header(&import.path)));
64 emitted = true;
65 }
66 }
67 if emitted {
68 out.push('\n');
69 }
70}
71
72fn emit_rust_imports(file: &SynFile, out: &mut String) {
73 let mut emitted = false;
74 for item in &file.items {
75 if let Item::Import(import) = item {
76 out.push_str(&format!("use crate::{};\n", import_rust_module(&import.path)));
77 emitted = true;
78 }
79 }
80 if emitted {
81 out.push('\n');
82 }
83}
84
85fn emit_items(file: &SynFile, out: &mut String) {
86 let mut has_mids = false;
88 for item in &file.items {
89 if let Some(m) = packet_item(item) {
90 if let Some(mid) = find_mid_attr(&m.attrs) {
91 if !has_mids {
92 out.push_str("/* Message IDs */\n");
93 has_mids = true;
94 }
95 let define_name = to_screaming_snake(&m.name);
96 let mid_str = literal_mid_str(mid);
97 out.push_str(&format!("#define {}_MID {}\n", define_name, mid_str));
98 }
99 }
100 }
101 if has_mids { out.push('\n'); }
102
103 let mut namespace = Vec::new();
105 for item in &file.items {
106 match item {
107 Item::Namespace(ns) => namespace = ns.name.clone(),
108 Item::Import(_) | Item::Enum(_) => {}
109 Item::Const(c) => emit_const(out, c),
110 Item::Struct(s) | Item::Table(s) => emit_struct(out, s, &namespace),
111 Item::Command(m) | Item::Telemetry(m) | Item::Message(m) => emit_message(out, m, &namespace),
112 }
113 }
114}
115
116fn emit_const(out: &mut String, c: &ConstDecl) {
119 let val = literal_str(&c.value);
120 out.push_str(&format!("#define {} {}\n\n", c.name, val));
121}
122
123fn emit_struct(out: &mut String, s: &StructDef, namespace: &[String]) {
126 for line in &s.doc {
127 if line.is_empty() { out.push_str("///\n"); } else { out.push_str(&format!("/// {line}\n")); }
128 }
129 out.push_str("typedef struct {\n");
130 for f in &s.fields {
131 emit_c_field(out, f, namespace);
132 }
133 out.push_str(&format!("}} {};\n\n", c_decl_type_name(&s.name, namespace)));
134}
135
136fn emit_message(out: &mut String, m: &MessageDef, namespace: &[String]) {
139 let header_type = if packet_is_command(m) {
140 "CFE_MSG_CommandHeader_t"
141 } else {
142 "CFE_MSG_TelemetryHeader_t"
143 };
144
145 for line in &m.doc {
146 if line.is_empty() {
147 out.push_str("///\n");
148 } else {
149 out.push_str(&format!("/// {line}\n"));
150 }
151 }
152
153 out.push_str(&format!("typedef struct {{\n"));
154 out.push_str(&format!(" {} Header;\n", header_type));
155 for f in &m.fields {
156 emit_c_field(out, f, namespace);
157 }
158 out.push_str(&format!("}} {};\n\n", c_decl_type_name(&m.name, namespace)));
159}
160
161fn emit_rust_items(file: &SynFile, opts: &RustOptions, out: &mut String) {
164 let mut has_mids = false;
166 for item in &file.items {
167 if let Some(m) = packet_item(item) {
168 if let Some(mid) = find_mid_attr(&m.attrs) {
169 if !has_mids {
170 out.push_str("// Message IDs\n");
171 has_mids = true;
172 }
173 let const_name = format!("{}_MID", to_screaming_snake(&m.name));
174 let val = rust_mid_str(mid);
175 out.push_str(&format!("pub const {}: u16 = {};\n", const_name, val));
176 }
177 }
178 }
179 if has_mids { out.push('\n'); }
180
181 for item in &file.items {
183 match item {
184 Item::Namespace(_) | Item::Import(_) | Item::Enum(_) => {}
185 Item::Const(c) => emit_rust_const(out, c),
186 Item::Struct(s) | Item::Table(s) => emit_rust_struct(out, s),
187 Item::Command(m) | Item::Telemetry(m) | Item::Message(m) => emit_rust_message(out, m, opts),
188 }
189 }
190}
191
192fn emit_rust_const(out: &mut String, c: &ConstDecl) {
193 let val = rust_literal_str(&c.value);
194 let ty = rust_field_type_str(&c.ty);
195 out.push_str(&format!("pub const {}: {} = {};\n\n", c.name, ty, val));
196}
197
198fn emit_rust_struct(out: &mut String, s: &StructDef) {
199 for line in &s.doc {
200 if line.is_empty() { out.push_str("///\n"); } else { out.push_str(&format!("/// {line}\n")); }
201 }
202 out.push_str("#[repr(C)]\n");
203 out.push_str(&format!("pub struct {} {{\n", s.name));
204 for f in &s.fields {
205 out.push_str(&format!(" pub {}: {},\n", f.name, rust_field_type_str(&f.ty)));
206 }
207 out.push_str("}\n\n");
208}
209
210fn emit_rust_message(out: &mut String, m: &MessageDef, opts: &RustOptions) {
211 let header_type = if packet_is_command(m) { opts.cmd_header } else { opts.tlm_header };
212 let qualified = if opts.cfs_module.is_empty() {
213 header_type.to_string()
214 } else {
215 format!("{}::{}", opts.cfs_module, header_type)
216 };
217
218 for line in &m.doc {
219 if line.is_empty() {
220 out.push_str("///\n");
221 } else {
222 out.push_str(&format!("/// {line}\n"));
223 }
224 }
225
226 out.push_str("#[repr(C)]\n");
227 out.push_str(&format!("pub struct {} {{\n", m.name));
228 out.push_str(&format!(" pub cfs_header: {},\n", qualified));
229 for f in &m.fields {
230 let ty = rust_field_type_str(&f.ty);
231 out.push_str(&format!(" pub {}: {},\n", f.name, ty));
232 }
233 out.push_str("}\n\n");
234}
235
236fn rust_field_type_str(ty: &TypeExpr) -> String {
237 if ty.base == BaseType::String {
238 return match &ty.array {
239 None | Some(ArraySuffix::Dynamic) => "*const u8".to_string(),
240 Some(ArraySuffix::Fixed(n)) | Some(ArraySuffix::Bounded(n)) => {
241 format!("[u8; {}]", n)
242 }
243 };
244 }
245
246 let base = rust_base_type_str(&ty.base);
247 match &ty.array {
248 None => base,
249 Some(ArraySuffix::Fixed(n)) => format!("[{}; {}]", base, n),
250 Some(ArraySuffix::Dynamic) => format!("*const {}", base),
252 Some(ArraySuffix::Bounded(n)) => format!("*const {} /* max {} */", base, n),
253 }
254}
255
256fn rust_base_type_str(base: &BaseType) -> String {
257 match base {
258 BaseType::String => "*const u8".to_string(),
259 BaseType::Primitive(p) => rust_primitive_str(*p).to_string(),
260 BaseType::Ref(segments) => segments.join("::"),
261 }
262}
263
264fn rust_primitive_str(p: PrimitiveType) -> &'static str {
265 match p {
266 PrimitiveType::F32 => "f32",
267 PrimitiveType::F64 => "f64",
268 PrimitiveType::I8 => "i8",
269 PrimitiveType::I16 => "i16",
270 PrimitiveType::I32 => "i32",
271 PrimitiveType::I64 => "i64",
272 PrimitiveType::U8 => "u8",
273 PrimitiveType::U16 => "u16",
274 PrimitiveType::U32 => "u32",
275 PrimitiveType::U64 => "u64",
276 PrimitiveType::Bool => "bool",
277 PrimitiveType::Bytes => "*const u8",
278 }
279}
280
281fn rust_mid_str(lit: &Literal) -> String {
282 match lit {
283 Literal::Hex(n) => format!("0x{:04X}", n),
284 Literal::Int(n) => n.to_string(),
285 Literal::Ident(segs) => segs.join("::"),
286 other => rust_literal_str(other),
287 }
288}
289
290fn rust_literal_str(lit: &Literal) -> String {
291 match lit {
292 Literal::Hex(n) => format!("0x{:X}", n),
293 Literal::Int(n) => n.to_string(),
294 Literal::Bool(b) => b.to_string(),
295 Literal::Float(f) => {
296 let s = format!("{}", f);
297 if s.contains('.') || s.contains('e') { s } else { format!("{}.0", s) }
298 }
299 Literal::Str(s) => format!("{:?}", s),
300 Literal::Ident(segments) => segments.join("::"),
301 }
302}
303
304fn find_mid_attr(attrs: &[Attribute]) -> Option<&Literal> {
308 attrs.iter().find(|a| a.name == "mid").map(|a| &a.value)
309}
310
311fn packet_item(item: &Item) -> Option<&MessageDef> {
312 match item {
313 Item::Command(m) | Item::Telemetry(m) | Item::Message(m) => Some(m),
314 _ => None,
315 }
316}
317
318fn packet_is_command(m: &MessageDef) -> bool {
320 match m.kind {
321 PacketKind::Command => return true,
322 PacketKind::Telemetry => return false,
323 PacketKind::Message => {}
324 }
325
326 if m.attrs.iter().any(|a| a.name == "cmd" && a.value != Literal::Bool(false)) {
327 return true;
328 }
329 if let Some(mid) = find_mid_attr(&m.attrs) {
330 if let Some(n) = literal_to_u64(mid) {
331 return (n & 0x1000) != 0;
332 }
333 }
334 false
335}
336
337fn literal_to_u64(lit: &Literal) -> Option<u64> {
338 match lit {
339 Literal::Hex(n) => Some(*n),
340 Literal::Int(n) if *n >= 0 => Some(*n as u64),
341 _ => None,
342 }
343}
344
345fn literal_mid_str(lit: &Literal) -> String {
347 match lit {
348 Literal::Hex(n) => format!("0x{:04X}U", n),
349 Literal::Int(n) => format!("{}U", n),
350 Literal::Ident(segs) => segs.join("::"),
351 other => literal_str(other),
352 }
353}
354
355fn literal_str(lit: &Literal) -> String {
356 match lit {
357 Literal::Float(f) => {
358 let s = format!("{}", f);
359 if s.contains('.') || s.contains('e') { s } else { format!("{}.0", s) }
360 }
361 Literal::Int(n) => n.to_string(),
362 Literal::Hex(n) => format!("0x{:X}U", n),
363 Literal::Bool(b) => if *b { "1".to_string() } else { "0".to_string() },
364 Literal::Str(s) => format!("{:?}", s),
365 Literal::Ident(segments) => segments.join("::"),
366 }
367}
368
369fn non_fixed_type_str(ty: &TypeExpr, namespace: &[String]) -> String {
370 if ty.base == BaseType::String {
371 return match &ty.array {
372 None | Some(ArraySuffix::Dynamic) => "const char*".to_string(),
373 Some(ArraySuffix::Fixed(_)) => unreachable!("handled by emit_c_field"),
374 Some(ArraySuffix::Bounded(n)) => format!("char[{}]", n),
375 };
376 }
377
378 let base = base_type_str(&ty.base, namespace);
379 match &ty.array {
380 None => base,
381 Some(ArraySuffix::Fixed(_)) => unreachable!("handled by caller"),
382 Some(ArraySuffix::Dynamic) => format!("CFE_Span_t /* {} */", base),
383 Some(ArraySuffix::Bounded(n)) => format!("CFE_Span_t /* {} max {} */", base, n),
384 }
385}
386
387fn base_type_str(base: &BaseType, namespace: &[String]) -> String {
388 match base {
389 BaseType::String => "const char*".to_string(),
390 BaseType::Primitive(p) => primitive_str(*p).to_string(),
391 BaseType::Ref(segments) => c_ref_type_name(segments, namespace),
392 }
393}
394
395fn emit_c_field(out: &mut String, f: &synapse_parser::ast::FieldDef, namespace: &[String]) {
396 match (&f.ty.base, &f.ty.array) {
397 (BaseType::String, Some(ArraySuffix::Fixed(n) | ArraySuffix::Bounded(n))) => {
398 out.push_str(&format!(" char {}[{}];\n", f.name, n));
399 }
400 (_, Some(ArraySuffix::Fixed(n))) => {
401 out.push_str(&format!(" {} {}[{}];\n", base_type_str(&f.ty.base, namespace), f.name, n));
402 }
403 _ => {
404 out.push_str(&format!(" {} {};\n", non_fixed_type_str(&f.ty, namespace), f.name));
405 }
406 }
407}
408
409fn c_decl_type_name(name: &str, namespace: &[String]) -> String {
410 let mut segments = namespace.to_vec();
411 segments.push(name.to_string());
412 format!("{}_t", segments.join("_"))
413}
414
415fn c_ref_type_name(segments: &[String], namespace: &[String]) -> String {
416 let resolved = if segments.len() == 1 && !namespace.is_empty() {
417 let mut resolved = namespace.to_vec();
418 resolved.push(segments[0].clone());
419 resolved
420 } else {
421 segments.to_vec()
422 };
423 if resolved.is_empty() {
424 return "_t".to_string();
425 }
426 format!("{}_t", resolved.join("_"))
427}
428
429fn import_c_header(path: &str) -> String {
430 replace_extension(path, "h")
431}
432
433fn import_rust_module(path: &str) -> String {
434 let header = path.rsplit('/').next().unwrap_or(path);
435 replace_extension(header, "")
436}
437
438fn replace_extension(path: &str, ext: &str) -> String {
439 match path.rsplit_once('.') {
440 Some((stem, _)) if ext.is_empty() => stem.to_string(),
441 Some((stem, _)) => format!("{stem}.{ext}"),
442 None if ext.is_empty() => path.to_string(),
443 None => format!("{path}.{ext}"),
444 }
445}
446
447fn primitive_str(p: PrimitiveType) -> &'static str {
448 match p {
449 PrimitiveType::F32 => "float",
450 PrimitiveType::F64 => "double",
451 PrimitiveType::I8 => "int8_t",
452 PrimitiveType::I16 => "int16_t",
453 PrimitiveType::I32 => "int32_t",
454 PrimitiveType::I64 => "int64_t",
455 PrimitiveType::U8 => "uint8_t",
456 PrimitiveType::U16 => "uint16_t",
457 PrimitiveType::U32 => "uint32_t",
458 PrimitiveType::U64 => "uint64_t",
459 PrimitiveType::Bool => "bool",
460 PrimitiveType::Bytes => "uint8_t*",
461 }
462}
463
464fn to_screaming_snake(name: &str) -> String {
466 let mut out = String::new();
467 for (i, ch) in name.chars().enumerate() {
468 if ch.is_uppercase() && i > 0 {
469 out.push('_');
470 }
471 out.push(ch.to_ascii_uppercase());
472 }
473 out
474}
475
476#[cfg(test)]
479mod tests {
480 use super::*;
481 use synapse_parser::ast::parse;
482
483 fn codegen(src: &str) -> String { generate_c(&parse(src).unwrap()) }
484
485 #[test]
486 fn tlm_message_with_hex_mid() {
487 let out = codegen("@mid(0x0801)\nmessage NavTlm { x: f64 y: f64 }");
488 assert!(out.contains("#define NAV_TLM_MID 0x0801U"));
489 assert!(out.contains("CFE_MSG_TelemetryHeader_t Header;"));
490 assert!(out.contains("typedef struct {"));
491 assert!(out.contains("} NavTlm_t;"));
492 assert!(out.contains(" double x;"));
493 assert!(out.contains(" double y;"));
494 }
495
496 #[test]
497 fn cmd_message_detected_by_mid_bit12() {
498 let out = codegen("@mid(0x1880)\nmessage NavCmd { seq: u16 }");
499 assert!(out.contains("#define NAV_CMD_MID 0x1880U"));
500 assert!(out.contains("CFE_MSG_CommandHeader_t Header;"));
501 assert!(out.contains("} NavCmd_t;"));
502 }
503
504 #[test]
505 fn command_uses_command_header() {
506 let out = codegen("@mid(0x0801)\ncommand SetMode { mode: u8 }");
507 assert!(out.contains("#define SET_MODE_MID 0x0801U"));
508 assert!(out.contains("CFE_MSG_CommandHeader_t Header;"));
509 assert!(!out.contains("CFE_MSG_TelemetryHeader_t Header;"));
510 }
511
512 #[test]
513 fn telemetry_uses_telemetry_header() {
514 let out = codegen("@mid(0x1880)\ntelemetry NavState { x: f64 }");
515 assert!(out.contains("#define NAV_STATE_MID 0x1880U"));
516 assert!(out.contains("CFE_MSG_TelemetryHeader_t Header;"));
517 assert!(!out.contains("CFE_MSG_CommandHeader_t Header;"));
518 }
519
520 #[test]
521 fn table_is_plain_data_without_bus_header() {
522 let out = codegen("table NavConfig { max_speed: f64 enabled: bool }");
523 assert!(out.contains("} NavConfig_t;"));
524 assert!(out.contains(" double max_speed;"));
525 assert!(!out.contains("CFE_MSG_CommandHeader_t Header;"));
526 assert!(!out.contains("CFE_MSG_TelemetryHeader_t Header;"));
527 }
528
529 #[test]
530 fn message_without_mid_no_define() {
531 let out = codegen("message Bare { x: f32 }");
532 assert!(!out.contains("#define"));
533 assert!(out.contains("typedef struct {"));
534 assert!(out.contains("CFE_MSG_TelemetryHeader_t Header;"));
535 }
536
537 #[test]
538 fn const_emits_define() {
539 let out = codegen("const NAV_TLM_MID: u16 = 0x0801");
540 assert!(out.contains("#define NAV_TLM_MID 0x801U"));
541 }
542
543 #[test]
544 fn fixed_array_field() {
545 let out = codegen("@mid(0x0802)\nmessage Imu { covariance: f64[9] }");
546 assert!(out.contains(" double covariance[9];"));
547 }
548
549 #[test]
550 fn c_refs_use_declared_typedef_names() {
551 let out = codegen("struct Point { x: f64 }\nmessage Pose { point: Point }");
552 assert!(out.contains("} Point_t;"));
553 assert!(out.contains(" Point_t point;"));
554 }
555
556 #[test]
557 fn c_qualified_refs_use_declared_typedef_names() {
558 let out = codegen("message Stamped { header: std_msgs::Header }");
559 assert!(out.contains(" std_msgs_Header_t header;"));
560 }
561
562 #[test]
563 fn c_bounded_string_uses_inline_storage() {
564 let out = codegen("struct Label { name: string[<=64] }");
565 assert!(out.contains(" char name[64];"));
566 }
567
568 #[test]
569 fn c_imports_emit_header_includes() {
570 let out = codegen(r#"import "std_msgs.syn""#);
571 assert!(out.contains("#include \"std_msgs.h\""));
572 }
573
574 fn rust_codegen(src: &str) -> String {
577 generate_rust(&parse(src).unwrap(), &RustOptions::default())
578 }
579
580 #[test]
581 fn rust_tlm_struct() {
582 let out = rust_codegen("@mid(0x0801)\nmessage NavTlm { x: f64 y: f64 }");
583 assert!(out.contains("pub const NAV_TLM_MID: u16 = 0x0801;"));
584 assert!(out.contains("#[repr(C)]"));
585 assert!(out.contains("pub struct NavTlm {"));
586 assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_TelemetryHeader_t,"));
587 assert!(out.contains(" pub x: f64,"));
588 assert!(out.contains(" pub y: f64,"));
589 }
590
591 #[test]
592 fn rust_cmd_struct() {
593 let out = rust_codegen("@mid(0x1880)\nmessage NavCmd { seq: u16 }");
594 assert!(out.contains("pub const NAV_CMD_MID: u16 = 0x1880;"));
595 assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_CommandHeader_t,"));
596 }
597
598 #[test]
599 fn rust_command_uses_command_header() {
600 let out = rust_codegen("@mid(0x0801)\ncommand SetMode { mode: u8 }");
601 assert!(out.contains("pub const SET_MODE_MID: u16 = 0x0801;"));
602 assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_CommandHeader_t,"));
603 assert!(!out.contains("CFE_MSG_TelemetryHeader_t"));
604 }
605
606 #[test]
607 fn rust_telemetry_uses_telemetry_header() {
608 let out = rust_codegen("@mid(0x1880)\ntelemetry NavState { x: f64 }");
609 assert!(out.contains("pub const NAV_STATE_MID: u16 = 0x1880;"));
610 assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_TelemetryHeader_t,"));
611 assert!(!out.contains("CFE_MSG_CommandHeader_t"));
612 }
613
614 #[test]
615 fn rust_table_is_plain_data_without_bus_header() {
616 let out = rust_codegen("table NavConfig { max_speed: f64 enabled: bool }");
617 assert!(out.contains("pub struct NavConfig {"));
618 assert!(out.contains(" pub max_speed: f64,"));
619 assert!(!out.contains("cfs_header"));
620 }
621
622 #[test]
623 fn rust_fixed_array() {
624 let out = rust_codegen("@mid(0x0802)\nmessage Imu { covariance: f64[9] }");
625 assert!(out.contains(" pub covariance: [f64; 9],"));
626 }
627
628 #[test]
629 fn rust_custom_module() {
630 let opts = RustOptions { cfs_module: "my_cfs", ..Default::default() };
631 let out = generate_rust(&parse("@mid(0x0801)\nmessage T { x: f32 }").unwrap(), &opts);
632 assert!(out.contains("my_cfs::CFE_MSG_TelemetryHeader_t"));
633 }
634
635 #[test]
636 fn rust_bare_module() {
637 let opts = RustOptions { cfs_module: "", ..Default::default() };
638 let out = generate_rust(&parse("@mid(0x0801)\nmessage T { x: f32 }").unwrap(), &opts);
639 assert!(out.contains(" pub cfs_header: CFE_MSG_TelemetryHeader_t,"));
640 assert!(!out.contains("::CFE_MSG_TelemetryHeader_t"));
641 }
642
643 #[test]
644 fn rust_message_can_have_payload_header_field() {
645 let out = rust_codegen("@mid(0x0801)\nmessage Stamped { header: std_msgs::Header }");
646 assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_TelemetryHeader_t,"));
647 assert!(out.contains(" pub header: std_msgs::Header,"));
648 }
649
650 #[test]
651 fn rust_const_uses_declared_type() {
652 let out = rust_codegen("const PI: f64 = 3.14\nconst ENABLED: bool = true");
653 assert!(out.contains("pub const PI: f64 = 3.14;"));
654 assert!(out.contains("pub const ENABLED: bool = true;"));
655 }
656
657 #[test]
658 fn rust_bounded_string_uses_inline_storage() {
659 let out = rust_codegen("struct Label { name: string[<=64] }");
660 assert!(out.contains(" pub name: [u8; 64],"));
661 }
662
663 #[test]
664 fn rust_imports_emit_crate_uses() {
665 let out = rust_codegen(r#"import "std_msgs.syn""#);
666 assert!(out.contains("use crate::std_msgs;"));
667 }
668
669 #[test]
670 fn screaming_snake_conversion() {
671 assert_eq!(to_screaming_snake("NavTelemetry"), "NAV_TELEMETRY");
672 assert_eq!(to_screaming_snake("PoseStamped"), "POSE_STAMPED");
673 assert_eq!(to_screaming_snake("Foo"), "FOO");
674 }
675}