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//! Statt eines vollen Codegens (der Rust→C++-Cross-Compile waere) liefert
6//! diese Schicht **statische Header-Templates** fuer den `dds::core::*`-
7//! Namespace, plus Codegen-Helper, die diese Templates fuer ein konkretes
8//! Topic-IDL emittieren.
9//!
10//! Spec-Referenz: OMG DDS-PSM-CXX 1.0 (formal/22-08-04).
11//!
12//! # Bestandteile
13//! - `dds::core::Reference<T>`/`dds::core::Value<T>`-Pattern (Spec §7.1.6).
14//! - Exception-Hierarchie (Spec §7.1.7).
15//! - Listener / Status / Condition / WaitSet (Spec §8.3).
16//! - Domain/Topic/Pub/Sub-Klassen (Spec §8.1).
17//!
18//! # API
19//! - [`emit_psm_cxx_includes`]: erzeugt `#include`-Block fuer einen
20//!   konkreten Topic-Namen.
21//! - [`emit_reference_value_pattern`]: emittiert die Reference/Value-
22//!   Templates (`Reference<T>`, `Value<T>`).
23//! - [`emit_listener_skeleton`]: emittiert die Listener-Klassen mit den
24//!   13 Status-Callbacks aus Block F.
25//! - [`emit_exception_hierarchy`]: emittiert die DDS-Exception-Klassen.
26//!
27//! Die Templates liegen statisch unter `templates/dds-psm-cxx/*.hpp.tmpl`
28//! und werden via `include_str!` zur Build-Zeit eingebettet.
29
30use core::fmt::Write;
31
32use crate::error::CppGenError;
33
34/// Statische Templates (eingebettet 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/// Erzeugt den `#include`-Block fuer einen konkreten Topic.
42///
43/// Der Block enthaelt sowohl die `dds::core::*`-Header (Reference, Value,
44/// Status, QoS, Entity) als auch die Standard-Bibliotheks-Includes, die
45/// fuer die Topic-Bindings benoetigt werden.
46///
47/// # Argumente
48/// - `participant_name`: Name des DomainParticipant-Headers (ohne
49///   `.hpp`-Suffix). Wird als `#include "<name>.hpp"` emittiert.
50///
51/// # Errors
52/// Liefert [`CppGenError::InvalidName`], wenn `participant_name` leer ist
53/// oder unsichere Zeichen (`/`, `\`, `..`) enthaelt.
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 Validierung: keine Path-Traversal-Zeichen.
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/// Emittiert das Reference/Value-Pattern aus Spec §7.1.6.
90///
91/// `dds::core::Reference<T>` ist ein nullable Smart-Pointer-Wrapper,
92/// `dds::core::Value<T>` ist ein by-value-Wrapper. Beide werden als
93/// Template-Klassen im `dds::core`-Namespace emittiert.
94///
95/// # Errors
96/// Liefert [`CppGenError::Internal`], wenn das Schreiben in den
97/// `String`-Buffer scheitert.
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/// Emittiert die DDS-Exception-Hierarchie aus Spec §7.1.7.
107///
108/// # Errors
109/// Liefert [`CppGenError::Internal`], wenn das Schreiben scheitert.
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/// Emittiert das Listener-Skeleton aus Spec §8.3.
119///
120/// Liefert Listener-Klassen mit allen 13 Status-Callbacks aus Block F als
121/// virtuelle Methoden mit Default-Implementation `{}`.
122///
123/// # Errors
124/// Liefert [`CppGenError::Internal`], wenn das Schreiben scheitert.
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/// Emittiert die Condition/WaitSet-Klassen aus Spec §8.3.
134///
135/// # Errors
136/// Liefert [`CppGenError::Internal`], wenn das Schreiben scheitert.
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/// Emittiert die `dds::core`-Basis-Header (Time, Duration, InstanceHandle,
146/// Sample<T>).
147///
148/// # Errors
149/// Liefert [`CppGenError::Internal`], wenn das Schreiben scheitert.
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/// Erzeugt einen vollstaendigen DDS-PSM-CXX-Skeleton-Header inkl. aller
159/// statischer Templates. Wird in den Fixture-Tests gegen die existierenden
160/// C5.1-a-Outputs gemerged geprueft.
161///
162/// # Errors
163/// Liefert [`CppGenError::Internal`], wenn das Schreiben scheitert.
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 fuer alle 13 Status-Klassen (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        // Mindestens drei Erscheinungen von "namespace dds":
302        let count = s.matches("namespace dds").count();
303        assert!(count >= 3, "expected >=3 namespace dds blocks, got {count}");
304    }
305}