1#![forbid(unsafe_code)]
50#![warn(missing_docs)]
51#![allow(
52 clippy::manual_pattern_char_comparison,
53 clippy::if_same_then_else,
54 clippy::collapsible_if,
55 clippy::useless_conversion,
56 clippy::approx_constant
57)]
58
59pub(crate) mod amqp;
60pub(crate) mod bitset;
61pub mod c_mode;
62pub(crate) mod corba_traits;
63pub mod dcps;
64pub mod emitter;
65pub mod error;
66pub mod psm_cxx;
67pub mod qos;
68pub mod rpc;
69pub mod status;
70pub mod type_map;
71pub(crate) mod verbatim;
72
73pub use c_mode::{CGenOptions, generate_c_header};
74pub use error::CppGenError;
75pub use psm_cxx::{
76 emit_condition_skeleton, emit_core_basics, emit_exception_hierarchy,
77 emit_full_psm_cxx_skeleton, emit_listener_skeleton, emit_psm_cxx_includes,
78 emit_reference_value_pattern,
79};
80
81use zerodds_idl::ast::Specification;
82
83#[derive(Debug, Clone)]
85pub struct CppGenOptions {
86 pub namespace_prefix: Option<String>,
89 pub include_guard_prefix: Option<String>,
93 pub indent_width: usize,
95 pub emit_amqp_helpers: bool,
101 pub emit_corba_traits: bool,
106}
107
108impl Default for CppGenOptions {
109 fn default() -> Self {
110 Self {
111 namespace_prefix: None,
112 include_guard_prefix: None,
113 indent_width: 4,
114 emit_amqp_helpers: false,
115 emit_corba_traits: false,
116 }
117 }
118}
119
120pub(crate) const TIME_DURATION_TYPES: &[(&str, &str)] = &[
125 ("Time_t", "DDS::Time_t"),
126 ("Duration_t", "DDS::Duration_t"),
127 ("Time", "DDS::Time_t"),
128 ("Duration", "DDS::Duration_t"),
129];
130
131pub fn generate_cpp_header(
142 ast: &Specification,
143 opts: &CppGenOptions,
144) -> Result<String, CppGenError> {
145 let mut out = emitter::emit_header(ast, opts)?;
146 if opts.emit_amqp_helpers {
147 amqp::emit_amqp_helpers(&mut out, ast)?;
148 }
149 if opts.emit_corba_traits {
150 corba_traits::emit_corba_traits(&mut out, ast)?;
151 }
152 Ok(out)
153}
154
155pub fn generate_cpp_header_with_corba_traits(
163 ast: &Specification,
164 opts: &CppGenOptions,
165) -> Result<String, CppGenError> {
166 let opts = CppGenOptions {
167 emit_corba_traits: true,
168 ..opts.clone()
169 };
170 generate_cpp_header(ast, &opts)
171}
172
173pub fn generate_cpp_header_with_amqp(
182 ast: &Specification,
183 opts: &CppGenOptions,
184) -> Result<String, CppGenError> {
185 let opts = CppGenOptions {
186 emit_amqp_helpers: true,
187 ..opts.clone()
188 };
189 generate_cpp_header(ast, &opts)
190}
191
192#[cfg(test)]
193mod tests {
194 #![allow(clippy::expect_used, clippy::panic)]
195 use super::*;
196 use zerodds_idl::config::ParserConfig;
197
198 fn gen_cpp(src: &str) -> String {
199 let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
200 generate_cpp_header(&ast, &CppGenOptions::default()).expect("gen must succeed")
201 }
202
203 #[test]
204 fn empty_source_emits_only_preamble() {
205 let cpp = gen_cpp("");
206 assert!(cpp.contains("#pragma once"));
207 assert!(cpp.contains("Generated by zerodds idl-cpp"));
208 assert!(!cpp.contains("namespace M {"));
210 }
211
212 #[test]
213 fn empty_module_emits_namespace() {
214 let cpp = gen_cpp("module M {};");
215 assert!(cpp.contains("namespace M {"));
216 assert!(cpp.contains("} // namespace M"));
217 }
218
219 #[test]
220 fn three_level_modules_nest() {
221 let cpp = gen_cpp("module A { module B { module C {}; }; };");
222 assert!(cpp.contains("namespace A {"));
223 assert!(cpp.contains("namespace B {"));
224 assert!(cpp.contains("namespace C {"));
225 assert!(cpp.contains("} // namespace C"));
226 assert!(cpp.contains("} // namespace B"));
227 assert!(cpp.contains("} // namespace A"));
228 }
229
230 #[test]
231 fn primitive_struct_member_uses_correct_cpp_types() {
232 let cpp = gen_cpp(
233 "struct S { boolean b; octet o; short s; long l; long long ll; \
234 unsigned short us; unsigned long ul; unsigned long long ull; \
235 float f; double d; };",
236 );
237 assert!(cpp.contains("bool b_;"));
238 assert!(cpp.contains("uint8_t o_;"));
239 assert!(cpp.contains("int16_t s_;"));
240 assert!(cpp.contains("int32_t l_;"));
241 assert!(cpp.contains("int64_t ll_;"));
242 assert!(cpp.contains("uint16_t us_;"));
243 assert!(cpp.contains("uint32_t ul_;"));
244 assert!(cpp.contains("uint64_t ull_;"));
245 assert!(cpp.contains("float f_;"));
246 assert!(cpp.contains("double d_;"));
247 }
248
249 #[test]
250 fn string_member_requires_string_include() {
251 let cpp = gen_cpp("struct S { string name; };");
252 assert!(cpp.contains("#include <string>"));
253 assert!(cpp.contains("std::string name_;"));
254 }
255
256 #[test]
257 fn sequence_member_uses_vector() {
258 let cpp = gen_cpp("struct S { sequence<long> data; };");
259 assert!(cpp.contains("#include <vector>"));
260 assert!(cpp.contains("std::vector<int32_t> data_;"));
261 }
262
263 #[test]
264 fn array_member_uses_std_array() {
265 let cpp = gen_cpp("struct S { long matrix[3][4]; };");
266 assert!(cpp.contains("#include <array>"));
267 assert!(cpp.contains("std::array<std::array<int32_t, 4>, 3>"));
268 }
269
270 #[test]
271 fn enum_emits_enum_class_int32_t() {
272 let cpp = gen_cpp("enum Color { RED, GREEN, BLUE };");
273 assert!(cpp.contains("enum class Color : int32_t"));
274 assert!(cpp.contains("RED,"));
275 assert!(cpp.contains("BLUE,"));
276 }
277
278 #[test]
279 fn typedef_emits_using_alias() {
280 let cpp = gen_cpp("typedef long MyInt;");
281 assert!(cpp.contains("using MyInt = int32_t;"));
282 }
283
284 #[test]
285 fn inheritance_emits_public_base() {
286 let cpp = gen_cpp("struct Parent { long x; }; struct Child : Parent { long y; };");
287 assert!(cpp.contains("class Child : public ::Parent"));
290 }
291
292 #[test]
296 fn ngva_typedef_serialized_and_nested_qualified() {
297 let cpp = gen_cpp(
298 "module nga { \
299 typedef double CurrentInAmpsType; \
300 enum CoordinateSystemType { COORDINATE_SYSTEM_TYPE__BNG }; \
301 struct LinearVelocity2DType { double xComponent; double yComponent; }; \
302 struct Navigation_Resource_Specification { \
303 @key string<32> vehicleId; \
304 CurrentInAmpsType batteryCurrent; \
305 CoordinateSystemType coordinateSystem; \
306 LinearVelocity2DType velocity; \
307 }; };",
308 );
309 assert!(
311 !cpp.contains("not supported (skip)"),
312 "typedef/nested members must not be skipped (Bug F)"
313 );
314 assert!(
316 cpp.contains("::nga::LinearVelocity2DType"),
317 "nested struct must be absolutely qualified (Bug B)"
318 );
319 assert!(
320 cpp.contains("::nga::CoordinateSystemType"),
321 "nested enum must be absolutely qualified (Bug B)"
322 );
323 }
324
325 #[test]
332 fn recursive_struct_generates_without_overflow_and_no_skip() {
333 let cpp = gen_cpp(
334 "module conf { \
335 struct TreeNode { long value; sequence<TreeNode> children; }; \
336 };",
337 );
338 assert!(
339 cpp.contains("std::vector<::conf::TreeNode>"),
340 "recursion must be stored behind a vector (heap indirection)"
341 );
342 assert!(
343 !cpp.contains("not supported (skip)"),
344 "recursive member must be serialized, not dropped (no wire data loss)"
345 );
346 assert!(
349 cpp.contains("topic_type_support<::conf::TreeNode>"),
350 "recursive member must splice via its own topic_type_support"
351 );
352 }
353
354 #[test]
358 fn forward_declared_recursive_struct_generates() {
359 let cpp = gen_cpp(
360 "module conf { \
361 struct Node; \
362 struct Node { long v; sequence<Node> next; }; \
363 };",
364 );
365 assert!(cpp.contains("std::vector<::conf::Node>"));
366 assert!(!cpp.contains("not supported (skip)"));
367 }
368
369 #[test]
370 fn keyed_struct_marker_appears() {
371 let cpp = gen_cpp("struct S { @key long id; long val; };");
372 assert!(cpp.contains("// @key"));
373 }
374
375 #[test]
376 fn optional_member_uses_std_optional() {
377 let cpp = gen_cpp("struct S { @optional long maybe; };");
378 assert!(cpp.contains("#include <optional>"));
379 assert!(cpp.contains("std::optional<int32_t>"));
380 }
381
382 #[test]
383 fn exception_inherits_std_exception() {
384 let cpp = gen_cpp("exception NotFound { string what_; };");
385 assert!(cpp.contains("#include <exception>"));
386 assert!(cpp.contains("class NotFound : public std::exception"));
387 }
388
389 #[test]
390 fn union_uses_std_variant() {
391 let cpp = gen_cpp(
392 "union U switch (long) { case 1: long a; case 2: double b; default: octet c; };",
393 );
394 assert!(cpp.contains("#include <variant>"));
395 assert!(cpp.contains("std::variant<"));
396 assert!(cpp.contains("// case default"));
397 }
398
399 #[test]
400 fn time_t_member_maps_to_dds_time_t() {
401 let cpp = gen_cpp("struct S { Time_t t; };");
402 assert!(cpp.contains("DDS::Time_t"));
403 }
404
405 #[test]
406 fn duration_t_member_maps_to_dds_duration_t() {
407 let cpp = gen_cpp("struct S { Duration_t d; };");
408 assert!(cpp.contains("DDS::Duration_t"));
409 }
410
411 #[test]
412 fn reserved_field_name_is_rejected() {
413 let ast = zerodds_idl::parse("struct S { long class_field; };", &ParserConfig::default())
414 .expect("parse");
415 let res = type_map::check_identifier("class");
419 assert!(matches!(res, Err(CppGenError::InvalidName { .. })));
420 let _ = ast; }
422
423 #[test]
424 fn empty_source_includes_cstdint() {
425 let cpp = gen_cpp("");
426 assert!(cpp.contains("#include <cstdint>"));
427 }
428
429 #[test]
430 fn header_starts_with_generated_marker() {
431 let cpp = gen_cpp("");
432 assert!(cpp.starts_with("// Generated by zerodds idl-cpp."));
433 }
434
435 #[test]
436 fn pragma_once_appears_exactly_once() {
437 let cpp = gen_cpp("module M { struct S { long x; }; };");
438 let count = cpp.matches("#pragma once").count();
439 assert_eq!(count, 1);
440 }
441
442 #[test]
443 fn struct_has_default_constructor() {
444 let cpp = gen_cpp("struct S { long x; };");
445 assert!(cpp.contains("S() = default;"));
446 assert!(cpp.contains("~S() = default;"));
447 }
448
449 #[test]
450 fn struct_has_mutable_and_const_getter() {
451 let cpp = gen_cpp("struct S { long x; };");
452 assert!(cpp.contains("int32_t& x()"));
454 assert!(cpp.contains("const int32_t& x() const"));
455 }
456
457 #[test]
458 fn struct_has_setter() {
459 let cpp = gen_cpp("struct S { long x; };");
460 assert!(cpp.contains("void x(const int32_t& value)"));
461 }
462
463 #[test]
464 fn struct_has_field_order_constructor() {
465 let cpp = gen_cpp("struct S { long celsius; string sensor_id; };");
468 assert!(
469 cpp.contains("S(int32_t celsius, std::string sensor_id)"),
470 "field-order ctor signature missing:\n{cpp}"
471 );
472 assert!(
473 cpp.contains(": celsius_(std::move(celsius)), sensor_id_(std::move(sensor_id)) {}"),
474 "member-init list missing:\n{cpp}"
475 );
476 assert!(cpp.contains("S() = default;"));
478 }
479
480 #[test]
481 fn zero_field_struct_has_no_field_order_constructor() {
482 let cpp = gen_cpp("struct Empty { };");
485 assert!(cpp.contains("Empty() = default;"));
486 assert_eq!(
489 cpp.matches("Empty(").count(),
490 2, "unexpected extra constructor for zero-field struct:\n{cpp}"
492 );
493 assert!(!cpp.contains("std::move"));
494 }
495
496 #[test]
497 fn namespace_prefix_option_wraps_output() {
498 let ast =
499 zerodds_idl::parse("struct S { long x; };", &ParserConfig::default()).expect("parse");
500 let opts = CppGenOptions {
501 namespace_prefix: Some("zerodds".into()),
502 ..Default::default()
503 };
504 let cpp = generate_cpp_header(&ast, &opts).expect("gen");
505 assert!(cpp.contains("namespace zerodds {"));
506 assert!(cpp.contains("} // namespace zerodds"));
507 }
508
509 #[test]
510 fn non_service_interface_emits_pure_virtual_class() {
511 let ast = zerodds_idl::parse("interface I { void op(); };", &ParserConfig::default())
512 .expect("parse");
513 let cpp = generate_cpp_header(&ast, &CppGenOptions::default()).expect("ok");
514 assert!(cpp.contains("class I"));
515 assert!(cpp.contains("virtual ~I()"));
516 assert!(cpp.contains("= 0;"));
517 }
518
519 #[test]
520 fn const_decl_emits_constexpr() {
521 let cpp = gen_cpp("const long MAX = 100;");
522 assert!(cpp.contains("constexpr int32_t MAX = 100;"));
523 }
524
525 #[test]
526 fn options_have_sensible_defaults() {
527 let o = CppGenOptions::default();
528 assert_eq!(o.indent_width, 4);
529 assert!(o.namespace_prefix.is_none());
530 assert!(o.include_guard_prefix.is_none());
531 }
532
533 #[test]
534 fn options_clone_works() {
535 let o = CppGenOptions {
536 namespace_prefix: Some("foo".into()),
537 include_guard_prefix: Some("FOO_".into()),
538 indent_width: 2,
539 emit_amqp_helpers: false,
540 emit_corba_traits: false,
541 };
542 let cloned = o.clone();
543 assert_eq!(cloned.indent_width, 2);
544 assert_eq!(cloned.namespace_prefix.as_deref(), Some("foo"));
545 }
546}