Skip to main content

zerodds_idl_cpp/
status.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Block-F: DDS-Status-Mapping (Spec idl4-cpp-1.0 §7.4 + dds-1.4 §2.2.4.1).
4//!
5//! Emittiert die 13 OMG-DCPS-Status-Strukturen als C++17-Header in den
6//! Namespace `dds::core::status`. Die Klassen folgen dem Reference-Pattern
7//! aus C5.1-a (private Storage `field_`, mutable + const Getter, Setter).
8//!
9//! Spec-Quellen:
10//! - DDS-PSM-CXX 1.0 §7.5.5 (Listener / Status / Condition).
11//! - DDS 1.4 §2.2.4.1 (Communication-Status).
12//!
13//! # Liste der 13 Status-Klassen
14//!
15//! | Status                          | Counter-Felder                                       |
16//! |---------------------------------|------------------------------------------------------|
17//! | InconsistentTopicStatus         | total_count, total_count_change                      |
18//! | SampleLostStatus                | total_count, total_count_change                      |
19//! | SampleRejectedStatus            | total_count, total_count_change, last_reason, last_instance_handle |
20//! | LivelinessChangedStatus         | alive_count, not_alive_count, alive_count_change, not_alive_count_change, last_publication_handle |
21//! | RequestedDeadlineMissedStatus   | total_count, total_count_change, last_instance_handle |
22//! | RequestedIncompatibleQosStatus  | total_count, total_count_change, last_policy_id, policies |
23//! | OfferedDeadlineMissedStatus     | total_count, total_count_change, last_instance_handle |
24//! | OfferedIncompatibleQosStatus    | total_count, total_count_change, last_policy_id, policies |
25//! | LivelinessLostStatus            | total_count, total_count_change                      |
26//! | PublicationMatchedStatus        | total_count, total_count_change, current_count, current_count_change, last_subscription_handle |
27//! | SubscriptionMatchedStatus       | total_count, total_count_change, current_count, current_count_change, last_publication_handle |
28//! | DataAvailableStatus             | (marker only)                                        |
29//! | DataOnReadersStatus             | (marker only)                                        |
30//!
31//! Die Statusse mit `(marker only)` sind reine Notification-Marker ohne
32//! Counter (Spec DDS 1.4 §2.2.4.1.1.1). Sie werden trotzdem als leere
33//! Klassen emittiert, damit Listener-Signaturen einheitlich sind.
34
35use core::fmt::Write;
36
37use crate::error::CppGenError;
38
39/// Beschreibung eines DDS-Status-Felds.
40#[derive(Debug, Clone, Copy)]
41struct StatusField {
42    /// Feld-Name (snake_case wie in Spec).
43    name: &'static str,
44    /// C++-Typ-Ausdruck.
45    cpp_ty: &'static str,
46}
47
48/// Beschreibung einer DDS-Status-Klasse.
49#[derive(Debug, Clone, Copy)]
50struct StatusSpec {
51    /// Klassen-Name (PascalCase wie in Spec).
52    name: &'static str,
53    /// Felder der Status-Klasse.
54    fields: &'static [StatusField],
55    /// Doc-Kommentar mit Spec-Referenz.
56    spec_ref: &'static str,
57}
58
59/// Vollstaendige Liste der 13 DCPS-Status-Strukturen.
60const STATUSES: &[StatusSpec] = &[
61    StatusSpec {
62        name: "InconsistentTopicStatus",
63        spec_ref: "DDS 1.4 §2.2.4.1.4",
64        fields: &[
65            StatusField {
66                name: "total_count",
67                cpp_ty: "int32_t",
68            },
69            StatusField {
70                name: "total_count_change",
71                cpp_ty: "int32_t",
72            },
73        ],
74    },
75    StatusSpec {
76        name: "SampleLostStatus",
77        spec_ref: "DDS 1.4 §2.2.4.1.5",
78        fields: &[
79            StatusField {
80                name: "total_count",
81                cpp_ty: "int32_t",
82            },
83            StatusField {
84                name: "total_count_change",
85                cpp_ty: "int32_t",
86            },
87        ],
88    },
89    StatusSpec {
90        name: "SampleRejectedStatus",
91        spec_ref: "DDS 1.4 §2.2.4.1.6",
92        fields: &[
93            StatusField {
94                name: "total_count",
95                cpp_ty: "int32_t",
96            },
97            StatusField {
98                name: "total_count_change",
99                cpp_ty: "int32_t",
100            },
101            StatusField {
102                name: "last_reason",
103                cpp_ty: "::dds::core::status::SampleRejectedState",
104            },
105            StatusField {
106                name: "last_instance_handle",
107                cpp_ty: "::dds::core::InstanceHandle",
108            },
109        ],
110    },
111    StatusSpec {
112        name: "LivelinessChangedStatus",
113        spec_ref: "DDS 1.4 §2.2.4.1.7",
114        fields: &[
115            StatusField {
116                name: "alive_count",
117                cpp_ty: "int32_t",
118            },
119            StatusField {
120                name: "not_alive_count",
121                cpp_ty: "int32_t",
122            },
123            StatusField {
124                name: "alive_count_change",
125                cpp_ty: "int32_t",
126            },
127            StatusField {
128                name: "not_alive_count_change",
129                cpp_ty: "int32_t",
130            },
131            StatusField {
132                name: "last_publication_handle",
133                cpp_ty: "::dds::core::InstanceHandle",
134            },
135        ],
136    },
137    StatusSpec {
138        name: "RequestedDeadlineMissedStatus",
139        spec_ref: "DDS 1.4 §2.2.4.1.8",
140        fields: &[
141            StatusField {
142                name: "total_count",
143                cpp_ty: "int32_t",
144            },
145            StatusField {
146                name: "total_count_change",
147                cpp_ty: "int32_t",
148            },
149            StatusField {
150                name: "last_instance_handle",
151                cpp_ty: "::dds::core::InstanceHandle",
152            },
153        ],
154    },
155    StatusSpec {
156        name: "RequestedIncompatibleQosStatus",
157        spec_ref: "DDS 1.4 §2.2.4.1.9",
158        fields: &[
159            StatusField {
160                name: "total_count",
161                cpp_ty: "int32_t",
162            },
163            StatusField {
164                name: "total_count_change",
165                cpp_ty: "int32_t",
166            },
167            StatusField {
168                name: "last_policy_id",
169                cpp_ty: "::dds::core::policy::QosPolicyId",
170            },
171            StatusField {
172                name: "policies",
173                cpp_ty: "std::vector<::dds::core::status::QosPolicyCount>",
174            },
175        ],
176    },
177    StatusSpec {
178        name: "OfferedDeadlineMissedStatus",
179        spec_ref: "DDS 1.4 §2.2.4.1.10",
180        fields: &[
181            StatusField {
182                name: "total_count",
183                cpp_ty: "int32_t",
184            },
185            StatusField {
186                name: "total_count_change",
187                cpp_ty: "int32_t",
188            },
189            StatusField {
190                name: "last_instance_handle",
191                cpp_ty: "::dds::core::InstanceHandle",
192            },
193        ],
194    },
195    StatusSpec {
196        name: "OfferedIncompatibleQosStatus",
197        spec_ref: "DDS 1.4 §2.2.4.1.11",
198        fields: &[
199            StatusField {
200                name: "total_count",
201                cpp_ty: "int32_t",
202            },
203            StatusField {
204                name: "total_count_change",
205                cpp_ty: "int32_t",
206            },
207            StatusField {
208                name: "last_policy_id",
209                cpp_ty: "::dds::core::policy::QosPolicyId",
210            },
211            StatusField {
212                name: "policies",
213                cpp_ty: "std::vector<::dds::core::status::QosPolicyCount>",
214            },
215        ],
216    },
217    StatusSpec {
218        name: "LivelinessLostStatus",
219        spec_ref: "DDS 1.4 §2.2.4.1.12",
220        fields: &[
221            StatusField {
222                name: "total_count",
223                cpp_ty: "int32_t",
224            },
225            StatusField {
226                name: "total_count_change",
227                cpp_ty: "int32_t",
228            },
229        ],
230    },
231    StatusSpec {
232        name: "PublicationMatchedStatus",
233        spec_ref: "DDS 1.4 §2.2.4.1.13",
234        fields: &[
235            StatusField {
236                name: "total_count",
237                cpp_ty: "int32_t",
238            },
239            StatusField {
240                name: "total_count_change",
241                cpp_ty: "int32_t",
242            },
243            StatusField {
244                name: "current_count",
245                cpp_ty: "int32_t",
246            },
247            StatusField {
248                name: "current_count_change",
249                cpp_ty: "int32_t",
250            },
251            StatusField {
252                name: "last_subscription_handle",
253                cpp_ty: "::dds::core::InstanceHandle",
254            },
255        ],
256    },
257    StatusSpec {
258        name: "SubscriptionMatchedStatus",
259        spec_ref: "DDS 1.4 §2.2.4.1.14",
260        fields: &[
261            StatusField {
262                name: "total_count",
263                cpp_ty: "int32_t",
264            },
265            StatusField {
266                name: "total_count_change",
267                cpp_ty: "int32_t",
268            },
269            StatusField {
270                name: "current_count",
271                cpp_ty: "int32_t",
272            },
273            StatusField {
274                name: "current_count_change",
275                cpp_ty: "int32_t",
276            },
277            StatusField {
278                name: "last_publication_handle",
279                cpp_ty: "::dds::core::InstanceHandle",
280            },
281        ],
282    },
283    StatusSpec {
284        name: "DataAvailableStatus",
285        spec_ref: "DDS 1.4 §2.2.4.1.2 (marker)",
286        fields: &[],
287    },
288    StatusSpec {
289        name: "DataOnReadersStatus",
290        spec_ref: "DDS 1.4 §2.2.4.1.3 (marker)",
291        fields: &[],
292    },
293];
294
295/// Schreibt den vollstaendigen Status-Header in `out`.
296///
297/// Der Header enthaelt einen Block-Kommentar mit Spec-Referenz, oeffnet den
298/// Namespace `dds::core::status`, emittiert die 13 Status-Klassen und
299/// schliesst die Namespaces wieder.
300///
301/// # Errors
302/// Liefert [`CppGenError::Internal`], wenn das Schreiben in den
303/// `String`-Buffer scheitert (sollte praktisch nie auftreten).
304pub fn emit_status_header(out: &mut String) -> Result<(), CppGenError> {
305    writeln!(
306        out,
307        "// Block-F: DDS-Status-Strukturen (Spec dds-1.4 §2.2.4.1)."
308    )
309    .map_err(fmt_err)?;
310    writeln!(
311        out,
312        "namespace dds {{ namespace core {{ namespace status {{"
313    )
314    .map_err(fmt_err)?;
315    writeln!(out).map_err(fmt_err)?;
316
317    // Forward-decls fuer Hilfstypen; volle Definitionen liegen ausserhalb
318    // dieses Codegens (in der DDS-PSM-CXX-Runtime).
319    writeln!(out, "// Forward-Declarations fuer Hilfstypen.").map_err(fmt_err)?;
320    writeln!(out, "enum class SampleRejectedState : int32_t;").map_err(fmt_err)?;
321    writeln!(out, "class QosPolicyCount;").map_err(fmt_err)?;
322    writeln!(out).map_err(fmt_err)?;
323
324    for s in STATUSES {
325        emit_status_class(out, s)?;
326    }
327
328    writeln!(out, "}} }} }} // namespace dds::core::status").map_err(fmt_err)?;
329    writeln!(out).map_err(fmt_err)?;
330    Ok(())
331}
332
333/// Liefert die Liste aller emittierten Status-Klassen-Namen.
334#[must_use]
335pub fn status_class_names() -> Vec<&'static str> {
336    STATUSES.iter().map(|s| s.name).collect()
337}
338
339fn emit_status_class(out: &mut String, s: &StatusSpec) -> Result<(), CppGenError> {
340    writeln!(out, "/// {} ({})", s.name, s.spec_ref).map_err(fmt_err)?;
341    writeln!(out, "class {} {{", s.name).map_err(fmt_err)?;
342    writeln!(out, "public:").map_err(fmt_err)?;
343    writeln!(out, "    {}() = default;", s.name).map_err(fmt_err)?;
344    writeln!(out, "    ~{}() = default;", s.name).map_err(fmt_err)?;
345    writeln!(out, "    {0}(const {0}&) = default;", s.name).map_err(fmt_err)?;
346    writeln!(out, "    {0}({0}&&) noexcept = default;", s.name).map_err(fmt_err)?;
347    writeln!(out, "    {0}& operator=(const {0}&) = default;", s.name).map_err(fmt_err)?;
348    writeln!(out, "    {0}& operator=({0}&&) noexcept = default;", s.name).map_err(fmt_err)?;
349
350    if !s.fields.is_empty() {
351        writeln!(out).map_err(fmt_err)?;
352        // Reset-Helper: setzt alle *_change-Felder auf 0 (Spec DDS 1.4
353        // §2.2.4.1.1: Reset on read).
354        writeln!(
355            out,
356            "    /// Reset-on-read: setzt alle *_change-Felder auf 0."
357        )
358        .map_err(fmt_err)?;
359        writeln!(out, "    void reset_changes() {{").map_err(fmt_err)?;
360        for f in s.fields {
361            if f.name.ends_with("_change") {
362                writeln!(out, "        {}_ = 0;", f.name).map_err(fmt_err)?;
363            }
364        }
365        writeln!(out, "    }}").map_err(fmt_err)?;
366    }
367
368    if !s.fields.is_empty() {
369        writeln!(out).map_err(fmt_err)?;
370        // Const + mutable Getter, Setter (const-ref + Move-Setter).
371        for f in s.fields {
372            writeln!(
373                out,
374                "    const {ty}& {name}() const {{ return {name}_; }}",
375                ty = f.cpp_ty,
376                name = f.name
377            )
378            .map_err(fmt_err)?;
379            writeln!(
380                out,
381                "    {ty}& {name}() {{ return {name}_; }}",
382                ty = f.cpp_ty,
383                name = f.name
384            )
385            .map_err(fmt_err)?;
386            writeln!(
387                out,
388                "    void {name}(const {ty}& value) {{ {name}_ = value; }}",
389                ty = f.cpp_ty,
390                name = f.name
391            )
392            .map_err(fmt_err)?;
393            writeln!(
394                out,
395                "    void {name}({ty}&& value) {{ {name}_ = std::move(value); }}",
396                ty = f.cpp_ty,
397                name = f.name
398            )
399            .map_err(fmt_err)?;
400        }
401
402        writeln!(out).map_err(fmt_err)?;
403        writeln!(out, "private:").map_err(fmt_err)?;
404        for f in s.fields {
405            writeln!(out, "    {ty} {name}_{{}};", ty = f.cpp_ty, name = f.name)
406                .map_err(fmt_err)?;
407        }
408    }
409
410    writeln!(out, "}};").map_err(fmt_err)?;
411    writeln!(out).map_err(fmt_err)?;
412    Ok(())
413}
414
415fn fmt_err(_: core::fmt::Error) -> CppGenError {
416    CppGenError::Internal("string formatting failed".into())
417}
418
419#[cfg(test)]
420mod tests {
421    #![allow(clippy::expect_used, clippy::panic)]
422    use super::*;
423
424    fn render() -> String {
425        let mut s = String::new();
426        emit_status_header(&mut s).expect("emit");
427        s
428    }
429
430    #[test]
431    fn status_namespace_is_dds_core_status() {
432        let s = render();
433        assert!(s.contains("namespace dds { namespace core { namespace status {"));
434        assert!(
435            s.contains("}}} // namespace dds::core::status")
436                || s.contains("} } } // namespace dds::core::status")
437        );
438    }
439
440    #[test]
441    fn sample_lost_status_has_total_count_fields() {
442        let s = render();
443        assert!(s.contains("class SampleLostStatus {"));
444        assert!(s.contains("int32_t total_count_{};"));
445        assert!(s.contains("int32_t total_count_change_{};"));
446    }
447
448    #[test]
449    fn sample_rejected_status_has_last_reason() {
450        let s = render();
451        assert!(s.contains("class SampleRejectedStatus {"));
452        assert!(s.contains("SampleRejectedState"));
453        assert!(s.contains("last_instance_handle"));
454    }
455
456    #[test]
457    fn liveliness_changed_status_has_alive_and_not_alive() {
458        let s = render();
459        assert!(s.contains("class LivelinessChangedStatus {"));
460        assert!(s.contains("alive_count_{};"));
461        assert!(s.contains("not_alive_count_{};"));
462        assert!(s.contains("alive_count_change_{};"));
463        assert!(s.contains("not_alive_count_change_{};"));
464    }
465
466    #[test]
467    fn matched_status_has_current_count_pair() {
468        let s = render();
469        assert!(s.contains("class PublicationMatchedStatus {"));
470        assert!(s.contains("class SubscriptionMatchedStatus {"));
471        assert!(s.contains("current_count_{};"));
472        assert!(s.contains("current_count_change_{};"));
473    }
474
475    #[test]
476    fn incompatible_qos_status_has_policy_count_vector() {
477        let s = render();
478        assert!(s.contains("class RequestedIncompatibleQosStatus {"));
479        assert!(s.contains("class OfferedIncompatibleQosStatus {"));
480        assert!(s.contains("std::vector<::dds::core::status::QosPolicyCount> policies_{};"));
481    }
482
483    #[test]
484    fn marker_statuses_emit_empty_class() {
485        let s = render();
486        assert!(s.contains("class DataAvailableStatus {"));
487        assert!(s.contains("class DataOnReadersStatus {"));
488        // Marker-Statusse haben keine Felder, daher kein `int32_t total_count_`.
489        // Aber sie emittieren Default-Konstruktor:
490        assert!(s.contains("DataAvailableStatus() = default;"));
491        assert!(s.contains("DataOnReadersStatus() = default;"));
492    }
493
494    #[test]
495    fn reset_changes_resets_only_change_fields() {
496        let s = render();
497        // Look at the InconsistentTopicStatus::reset_changes() body:
498        let needle = "class InconsistentTopicStatus {";
499        let start = s.find(needle).expect("class header");
500        let after = &s[start..];
501        let reset_pos = after.find("reset_changes()").expect("reset method");
502        let reset_block = &after[reset_pos..];
503        // Inside, we set total_count_change_ to 0 but never total_count_.
504        let close = reset_block.find("    }").expect("end of method");
505        let body = &reset_block[..close];
506        assert!(body.contains("total_count_change_ = 0;"));
507        assert!(!body.contains("total_count_ = 0;"));
508    }
509
510    #[test]
511    fn all_thirteen_status_classes_emitted() {
512        let names = status_class_names();
513        assert_eq!(names.len(), 13);
514        let s = render();
515        for n in names {
516            let class_marker = format!("class {n} {{");
517            assert!(s.contains(&class_marker), "missing class: {n}");
518        }
519    }
520
521    #[test]
522    fn move_setter_variant_present() {
523        let s = render();
524        // Move-Setter mit && + std::move:
525        assert!(s.contains("void total_count(int32_t&& value)"));
526        assert!(s.contains("std::move(value);"));
527    }
528
529    #[test]
530    fn const_getter_returns_const_ref() {
531        let s = render();
532        assert!(s.contains("const int32_t& total_count() const"));
533        assert!(s.contains("int32_t& total_count() {"));
534    }
535
536    #[test]
537    fn copy_and_move_special_members_defaulted() {
538        let s = render();
539        assert!(s.contains("SampleLostStatus(const SampleLostStatus&) = default;"));
540        assert!(s.contains("SampleLostStatus(SampleLostStatus&&) noexcept = default;"));
541    }
542}