Skip to main content

zerodds_idl_cpp/
psm_cxx.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! C5.2: DDS-PSM-CXX 1.0 header skeleton layer.
4//!
5//! Instead of a full codegen (which would be a Rust→C++ cross-compile),
6//! this layer provides **static header templates** for the `dds::core::*`
7//! namespace, plus codegen helpers that emit these templates for a concrete
8//! topic IDL.
9//!
10//! Spec reference: OMG DDS-PSM-CXX 1.0 (formal/22-08-04).
11//!
12//! # Components
13//! - `dds::core::Reference<T>`/`dds::core::Value<T>` pattern (Spec §7.1.6).
14//! - Exception hierarchy (Spec §7.1.7).
15//! - Listener / Status / Condition / WaitSet (Spec §8.3).
16//! - Domain/Topic/Pub/Sub classes (Spec §8.1).
17//!
18//! # API
19//! - [`emit_psm_cxx_includes`]: produces the `#include` block for a
20//!   concrete topic name.
21//! - [`emit_reference_value_pattern`]: emits the Reference/Value
22//!   templates (`Reference<T>`, `Value<T>`).
23//! - [`emit_listener_skeleton`]: emits the listener classes with the
24//!   13 status callbacks from Block F.
25//! - [`emit_exception_hierarchy`]: emits the DDS exception classes.
26//!
27//! The templates live statically under `templates/dds-psm-cxx/*.hpp.tmpl`
28//! and are embedded at build time via `include_str!`.
29
30use core::fmt::Write;
31
32use crate::error::CppGenError;
33
34/// Static templates (embedded via `include_str!`).
35const TPL_CORE_HPP: &str = include_str!("../templates/dds-psm-cxx/core.hpp.tmpl");
36const TPL_REFERENCE_HPP: &str = include_str!("../templates/dds-psm-cxx/reference.hpp.tmpl");
37const TPL_EXCEPTIONS_HPP: &str = include_str!("../templates/dds-psm-cxx/exceptions.hpp.tmpl");
38const TPL_LISTENER_HPP: &str = include_str!("../templates/dds-psm-cxx/listener.hpp.tmpl");
39const TPL_CONDITION_HPP: &str = include_str!("../templates/dds-psm-cxx/condition.hpp.tmpl");
40
41/// Produces the `#include` block for a concrete topic.
42///
43/// The block contains both the `dds::core::*` headers (Reference, Value,
44/// Status, QoS, Entity) and the standard-library includes needed for the
45/// topic bindings.
46///
47/// # Arguments
48/// - `participant_name`: name of the DomainParticipant header (without
49///   the `.hpp` suffix). Emitted as `#include "<name>.hpp"`.
50///
51/// # Errors
52/// Returns [`CppGenError::InvalidName`] if `participant_name` is empty
53/// or contains unsafe characters (`/`, `\`, `..`).
54pub fn emit_psm_cxx_includes(participant_name: &str) -> Result<String, CppGenError> {
55    if participant_name.is_empty() {
56        return Err(CppGenError::InvalidName {
57            name: participant_name.to_string(),
58            reason: "participant header name must not be empty".into(),
59        });
60    }
61    // Defensive validation: no path-traversal characters.
62    if participant_name.contains('/')
63        || participant_name.contains('\\')
64        || participant_name.contains("..")
65    {
66        return Err(CppGenError::InvalidName {
67            name: participant_name.to_string(),
68            reason: "participant header name must not contain path separators".into(),
69        });
70    }
71
72    let mut out = String::new();
73    writeln!(out, "// dds-psm-cxx-1.0 includes (generated).").map_err(fmt_err)?;
74    writeln!(out, "#include <cstdint>").map_err(fmt_err)?;
75    writeln!(out, "#include <memory>").map_err(fmt_err)?;
76    writeln!(out, "#include <string>").map_err(fmt_err)?;
77    writeln!(out, "#include <vector>").map_err(fmt_err)?;
78    writeln!(out, "#include <exception>").map_err(fmt_err)?;
79    writeln!(out, "#include <utility>").map_err(fmt_err)?;
80    writeln!(out, "#include <dds/core/core.hpp>").map_err(fmt_err)?;
81    writeln!(out, "#include <dds/core/reference.hpp>").map_err(fmt_err)?;
82    writeln!(out, "#include <dds/core/exceptions.hpp>").map_err(fmt_err)?;
83    writeln!(out, "#include <dds/core/listener.hpp>").map_err(fmt_err)?;
84    writeln!(out, "#include <dds/core/condition.hpp>").map_err(fmt_err)?;
85    writeln!(out, "#include \"{participant_name}.hpp\"").map_err(fmt_err)?;
86    Ok(out)
87}
88
89/// Emits the Reference/Value pattern from Spec §7.1.6.
90///
91/// `dds::core::Reference<T>` is a nullable smart-pointer wrapper,
92/// `dds::core::Value<T>` is a by-value wrapper. Both are emitted as
93/// template classes in the `dds::core` namespace.
94///
95/// # Errors
96/// Returns [`CppGenError::Internal`] if writing into the
97/// `String` buffer fails.
98pub fn emit_reference_value_pattern(out: &mut String) -> Result<(), CppGenError> {
99    out.push_str(TPL_REFERENCE_HPP);
100    if !TPL_REFERENCE_HPP.ends_with('\n') {
101        out.push('\n');
102    }
103    Ok(())
104}
105
106/// Emits the DDS exception hierarchy from Spec §7.1.7.
107///
108/// # Errors
109/// Returns [`CppGenError::Internal`] if writing fails.
110pub fn emit_exception_hierarchy(out: &mut String) -> Result<(), CppGenError> {
111    out.push_str(TPL_EXCEPTIONS_HPP);
112    if !TPL_EXCEPTIONS_HPP.ends_with('\n') {
113        out.push('\n');
114    }
115    Ok(())
116}
117
118/// Emits the listener skeleton from Spec §8.3.
119///
120/// Provides listener classes with all 13 status callbacks from Block F as
121/// virtual methods with the default implementation `{}`.
122///
123/// # Errors
124/// Returns [`CppGenError::Internal`] if writing fails.
125pub fn emit_listener_skeleton(out: &mut String) -> Result<(), CppGenError> {
126    out.push_str(TPL_LISTENER_HPP);
127    if !TPL_LISTENER_HPP.ends_with('\n') {
128        out.push('\n');
129    }
130    Ok(())
131}
132
133/// Emits the Condition/WaitSet classes from Spec §8.3.
134///
135/// # Errors
136/// Returns [`CppGenError::Internal`] if writing fails.
137pub fn emit_condition_skeleton(out: &mut String) -> Result<(), CppGenError> {
138    out.push_str(TPL_CONDITION_HPP);
139    if !TPL_CONDITION_HPP.ends_with('\n') {
140        out.push('\n');
141    }
142    Ok(())
143}
144
145/// Emits the `dds::core` base headers (Time, Duration, InstanceHandle,
146/// Sample<T>).
147///
148/// # Errors
149/// Returns [`CppGenError::Internal`] if writing fails.
150pub fn emit_core_basics(out: &mut String) -> Result<(), CppGenError> {
151    out.push_str(TPL_CORE_HPP);
152    if !TPL_CORE_HPP.ends_with('\n') {
153        out.push('\n');
154    }
155    Ok(())
156}
157
158/// Produces a complete DDS-PSM-CXX skeleton header including all
159/// static templates. In the fixture tests it is checked merged against the
160/// existing C5.1-a outputs.
161///
162/// # Errors
163/// Returns [`CppGenError::Internal`] if writing fails.
164pub fn emit_full_psm_cxx_skeleton() -> Result<String, CppGenError> {
165    let mut out = String::new();
166    writeln!(
167        out,
168        "// Generated dds-psm-cxx-1.0 skeleton (zerodds idl-cpp C5.2)."
169    )
170    .map_err(fmt_err)?;
171    writeln!(out, "#pragma once").map_err(fmt_err)?;
172    writeln!(out).map_err(fmt_err)?;
173    writeln!(out, "#include <cstdint>").map_err(fmt_err)?;
174    writeln!(out, "#include <memory>").map_err(fmt_err)?;
175    writeln!(out, "#include <string>").map_err(fmt_err)?;
176    writeln!(out, "#include <vector>").map_err(fmt_err)?;
177    writeln!(out, "#include <exception>").map_err(fmt_err)?;
178    writeln!(out, "#include <utility>").map_err(fmt_err)?;
179    writeln!(out).map_err(fmt_err)?;
180    emit_core_basics(&mut out)?;
181    emit_reference_value_pattern(&mut out)?;
182    emit_exception_hierarchy(&mut out)?;
183    emit_listener_skeleton(&mut out)?;
184    emit_condition_skeleton(&mut out)?;
185    Ok(out)
186}
187
188fn fmt_err(_: core::fmt::Error) -> CppGenError {
189    CppGenError::Internal("string formatting failed".into())
190}
191
192#[cfg(test)]
193mod tests {
194    #![allow(clippy::expect_used, clippy::panic)]
195    use super::*;
196
197    #[test]
198    fn includes_with_valid_name() {
199        let s = emit_psm_cxx_includes("MyParticipant").expect("ok");
200        assert!(s.contains("#include <dds/core/core.hpp>"));
201        assert!(s.contains("#include <dds/core/reference.hpp>"));
202        assert!(s.contains("#include \"MyParticipant.hpp\""));
203    }
204
205    #[test]
206    fn includes_rejects_empty() {
207        let res = emit_psm_cxx_includes("");
208        assert!(matches!(res, Err(CppGenError::InvalidName { .. })));
209    }
210
211    #[test]
212    fn includes_rejects_path_traversal() {
213        let res = emit_psm_cxx_includes("../etc/passwd");
214        assert!(matches!(res, Err(CppGenError::InvalidName { .. })));
215        let res2 = emit_psm_cxx_includes("a/b");
216        assert!(matches!(res2, Err(CppGenError::InvalidName { .. })));
217        let res3 = emit_psm_cxx_includes("a\\b");
218        assert!(matches!(res3, Err(CppGenError::InvalidName { .. })));
219    }
220
221    #[test]
222    fn reference_pattern_emits_reference_template() {
223        let mut s = String::new();
224        emit_reference_value_pattern(&mut s).expect("ok");
225        assert!(s.contains("namespace dds { namespace core {"));
226        assert!(s.contains("class Reference {"));
227        assert!(s.contains("class Value {"));
228    }
229
230    #[test]
231    fn exception_hierarchy_emits_dds_exception_classes() {
232        let mut s = String::new();
233        emit_exception_hierarchy(&mut s).expect("ok");
234        assert!(s.contains("class Exception"));
235        assert!(s.contains("class PreconditionNotMetError"));
236        assert!(s.contains("class NotEnabledError"));
237        assert!(s.contains("class OutOfResourcesError"));
238        assert!(s.contains("class IllegalOperationError"));
239    }
240
241    #[test]
242    fn listener_skeleton_has_13_callbacks() {
243        let mut s = String::new();
244        emit_listener_skeleton(&mut s).expect("ok");
245        // Listener callbacks for all 13 status classes (DCPS Spec):
246        let callbacks = [
247            "on_inconsistent_topic",
248            "on_sample_lost",
249            "on_sample_rejected",
250            "on_liveliness_changed",
251            "on_requested_deadline_missed",
252            "on_requested_incompatible_qos",
253            "on_offered_deadline_missed",
254            "on_offered_incompatible_qos",
255            "on_liveliness_lost",
256            "on_publication_matched",
257            "on_subscription_matched",
258            "on_data_available",
259            "on_data_on_readers",
260        ];
261        for cb in callbacks {
262            assert!(s.contains(cb), "missing callback: {cb}");
263        }
264    }
265
266    #[test]
267    fn condition_skeleton_has_waitset() {
268        let mut s = String::new();
269        emit_condition_skeleton(&mut s).expect("ok");
270        assert!(s.contains("class Condition"));
271        assert!(s.contains("class WaitSet"));
272        assert!(s.contains("class GuardCondition"));
273        assert!(s.contains("class StatusCondition"));
274        assert!(s.contains("class ReadCondition"));
275    }
276
277    #[test]
278    fn core_basics_define_time_duration_handle() {
279        let mut s = String::new();
280        emit_core_basics(&mut s).expect("ok");
281        assert!(s.contains("class Time"));
282        assert!(s.contains("class Duration"));
283        assert!(s.contains("class InstanceHandle"));
284        assert!(s.contains("Sample"));
285    }
286
287    #[test]
288    fn full_skeleton_combines_all_blocks() {
289        let s = emit_full_psm_cxx_skeleton().expect("ok");
290        assert!(s.contains("#pragma once"));
291        assert!(s.contains("class Time"));
292        assert!(s.contains("class Reference"));
293        assert!(s.contains("class Exception"));
294        assert!(s.contains("on_data_available"));
295        assert!(s.contains("class WaitSet"));
296    }
297
298    #[test]
299    fn full_skeleton_namespaces_are_dds_core() {
300        let s = emit_full_psm_cxx_skeleton().expect("ok");
301        // At least three occurrences of "namespace dds":
302        let count = s.matches("namespace dds").count();
303        assert!(count >= 3, "expected >=3 namespace dds blocks, got {count}");
304    }
305}