Skip to main content

zerodds_idl_csharp/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! IDL4 → C# 12.0 Source-Code-Generator (OMG IDL4-CSharp-Mapping,
4//! formal/2024-12-01).
5//!
6//! Crate `zerodds-idl-csharp` — Foundation des Sprach-Bindings (Cluster C5.3-a).
7//!
8//! Safety classification: **SAFE (std-only)**. Reines Build-Zeit-Tool —
9//! `forbid(unsafe_code)`, kein no_std-Use-Case.
10//!
11//! # Scope (C5.3-a / Phase 3.1-3.3)
12//! - Phase 3.1: Header-Layout (`#nullable enable`, `using`, `namespace`).
13//! - Phase 3.2: Primitive-Mapping
14//!   (boolean → bool, octet → byte, char/wchar → char,
15//!   short → short, ushort → ushort, long → int, ulong → uint,
16//!   long long → long, ulong long → ulong, float/double, string → string).
17//! - Phase 3.3: Aggregate-Types — struct → `record class` mit init-only
18//!   Properties, enum → `enum`, union → discriminated record,
19//!   typedef → `record class`-Wrapper (Spec-konformes file-scoped
20//!   using-alias kommt mit C5.3-b), sequence → `IList<T>`, array → `T[]`,
21//!   inheritance → `record class : Parent`.
22//!
23//! # C5.3-b additions (this revision)
24//! - `ISequence<T>` / `IBoundedSequence<T>` runtime contract
25//!   (see `runtime/Omg.Types.cs`); codegen emits these instead of
26//!   bare `IList<T>`.
27//! - Annotation-Bridge `@key|@id|@optional|@must_understand|@external`
28//!   on members, `@nested|@extensibility(...)` on types — emitted as
29//!   C# attributes from `Omg.Types`.
30//! - `ITopicType<T>` marker on every top-level (non-`@nested`) struct.
31//!
32//! # Bewusst nicht im Crate
33//! - Phase 3.4: DDS-CSharp-Integration (P/Invoke zu Rust-Core).
34//! - Time/Duration/Status/QoS/Listener-Codegen.
35//! - File-scoped namespace + `using <Alias> = <Type>;` Top-Level-Form.
36//! - Bitset/Bitmask/Map/Fixed/Any/Interface/Valuetype.
37//!
38//! # Beispiel
39//!
40//! ```
41//! use zerodds_idl::config::ParserConfig;
42//! use zerodds_idl_csharp::{generate_csharp, CsGenOptions};
43//!
44//! let ast = zerodds_idl::parse(
45//!     "module M { struct S { long x; }; };",
46//!     &ParserConfig::default(),
47//! )
48//! .expect("parse");
49//! let cs = generate_csharp(&ast, &CsGenOptions::default()).expect("gen");
50//! assert!(cs.contains("namespace M"));
51//! assert!(cs.contains("record class S"));
52//! ```
53
54#![forbid(unsafe_code)]
55#![warn(missing_docs)]
56
57mod annotations;
58pub(crate) mod bitset;
59pub(crate) mod corba_traits;
60pub mod emitter;
61pub mod error;
62pub mod keywords;
63pub mod type_map;
64pub(crate) mod typesupport;
65pub(crate) mod verbatim;
66
67pub use error::CsGenError;
68
69use zerodds_idl::ast::Specification;
70
71/// Konfiguration des C#-Code-Generators.
72#[derive(Debug, Clone)]
73pub struct CsGenOptions {
74    /// Optionaler aeusserer Namespace, in den der gesamte Output gewickelt
75    /// wird. `None` oder leer = kein Wrapper.
76    pub root_namespace: Option<String>,
77    /// Indent-Breite in Leerzeichen. Default 4 (C# Coding-Conventions).
78    pub indent_width: usize,
79    /// Wenn `true`: struct-Mapping verwendet `record class` (Spec-konform).
80    /// Wenn `false`: struct-Mapping verwendet plain `class` (Legacy-CCM).
81    /// Default `true`.
82    pub use_records: bool,
83    /// Annex A.1 (idl4-csharp-1.0) — opt-in: emittiert pro
84    /// Top-Level-Type CORBA-Marker (`Corba.ValueTypeAttribute`)
85    /// + statischen `Corba.Traits`-Helper. Default `false`.
86    pub emit_corba_traits: bool,
87}
88
89impl Default for CsGenOptions {
90    fn default() -> Self {
91        Self {
92            root_namespace: None,
93            indent_width: 4,
94            use_records: true,
95            emit_corba_traits: false,
96        }
97    }
98}
99
100/// Erzeugt einen vollstaendigen C# 12.0-Quelltext aus einer IDL-Specification.
101///
102/// # Errors
103/// - [`CsGenError::UnsupportedConstruct`]: IDL-Konstrukt außerhalb des aktuellen Scopes
104///   (z.B. `interface`, `valuetype`, `fixed`, `any`, `bitset`, `bitmask`).
105/// - [`CsGenError::InvalidName`]: Ein Identifier ist leer oder bereits
106///   `@`-prefixed (Doppel-Escape).
107/// - [`CsGenError::InheritanceCycle`]: Direkte oder indirekte
108///   Self-Inheritance im Struct-Graphen.
109pub fn generate_csharp(ast: &Specification, opts: &CsGenOptions) -> Result<String, CsGenError> {
110    let mut out = emitter::emit_source(ast, opts)?;
111    if opts.emit_corba_traits {
112        corba_traits::emit_corba_traits(&mut out, ast)?;
113    }
114    Ok(out)
115}
116
117/// Convenience-Variante mit aktiviertem `emit_corba_traits`-Flag.
118///
119/// Cross-Ref: `idl4-csharp-1.0` Annex A.1.
120///
121/// # Errors
122/// Wie [`generate_csharp`].
123pub fn generate_csharp_with_corba_traits(
124    ast: &Specification,
125    opts: &CsGenOptions,
126) -> Result<String, CsGenError> {
127    let opts = CsGenOptions {
128        emit_corba_traits: true,
129        ..opts.clone()
130    };
131    generate_csharp(ast, &opts)
132}
133
134#[cfg(test)]
135mod tests {
136    #![allow(clippy::expect_used, clippy::panic)]
137    use super::*;
138    use zerodds_idl::config::ParserConfig;
139
140    fn gen_cs(src: &str) -> String {
141        let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
142        generate_csharp(&ast, &CsGenOptions::default()).expect("gen must succeed")
143    }
144
145    #[test]
146    fn empty_source_emits_only_preamble() {
147        let cs = gen_cs("");
148        assert!(cs.contains("// Generated by zerodds idl-csharp"));
149        assert!(cs.contains("#nullable enable"));
150        assert!(cs.contains("using System;"));
151        assert!(!cs.contains("namespace M"));
152    }
153
154    #[test]
155    fn empty_module_emits_namespace() {
156        let cs = gen_cs("module M {};");
157        assert!(cs.contains("namespace M"));
158        assert!(cs.contains("} // namespace M"));
159    }
160
161    #[test]
162    fn three_level_modules_nest() {
163        let cs = gen_cs("module A { module B { module C {}; }; };");
164        assert!(cs.contains("namespace A"));
165        assert!(cs.contains("namespace B"));
166        assert!(cs.contains("namespace C"));
167    }
168
169    #[test]
170    fn primitive_struct_member_uses_correct_cs_types() {
171        let cs = gen_cs(
172            "struct S { boolean b; octet o; short s; long l; long long ll; \
173             unsigned short us; unsigned long ul; unsigned long long ull; \
174             float f; double d; };",
175        );
176        assert!(cs.contains("public bool B"));
177        assert!(cs.contains("public byte O"));
178        assert!(cs.contains("public short S"));
179        assert!(cs.contains("public int L "));
180        assert!(cs.contains("public long Ll"));
181        assert!(cs.contains("public ushort Us"));
182        assert!(cs.contains("public uint Ul "));
183        assert!(cs.contains("public ulong Ull"));
184        assert!(cs.contains("public float F"));
185        assert!(cs.contains("public double D"));
186    }
187
188    #[test]
189    fn string_member_uses_string() {
190        let cs = gen_cs("struct S { string name; };");
191        assert!(cs.contains("public string Name"));
192    }
193
194    #[test]
195    fn sequence_member_uses_isequence() {
196        // C5.3-b: unbounded sequence → `ISequence<T>` from Omg.Types.
197        let cs = gen_cs("struct S { sequence<long> data; };");
198        assert!(cs.contains("using System.Collections.Generic;"));
199        assert!(cs.contains("using Omg.Types;"));
200        assert!(cs.contains("ISequence<int>"));
201    }
202
203    #[test]
204    fn bounded_sequence_member_uses_ibounded_sequence() {
205        // C5.3-b: bounded sequence → `IBoundedSequence<T>` from Omg.Types.
206        let cs = gen_cs("struct S { sequence<long, 100> data; };");
207        assert!(cs.contains("using Omg.Types;"));
208        assert!(cs.contains("IBoundedSequence<int>"));
209    }
210
211    #[test]
212    fn array_member_uses_jagged_array() {
213        let cs = gen_cs("struct S { long matrix[3][4]; };");
214        // Jagged: int[][] (Spec laesst beide Varianten zu).
215        assert!(cs.contains("int[][]"));
216    }
217
218    #[test]
219    fn enum_emits_int_backed_enum() {
220        let cs = gen_cs("enum Color { RED, GREEN, BLUE };");
221        assert!(cs.contains("public enum Color : int"));
222        assert!(cs.contains("RED,"));
223        assert!(cs.contains("BLUE,"));
224    }
225
226    #[test]
227    fn typedef_emits_alias_record() {
228        let cs = gen_cs("typedef long MyInt;");
229        assert!(cs.contains("public sealed record class MyInt(int Value);"));
230    }
231
232    #[test]
233    fn inheritance_emits_record_inheritance() {
234        let cs = gen_cs("struct Parent { long x; }; struct Child : Parent { long y; };");
235        assert!(cs.contains("record class Child : Parent"));
236    }
237
238    #[test]
239    fn keyed_struct_marker_appears() {
240        let cs = gen_cs("struct S { @key long id; long val; };");
241        assert!(cs.contains("[Key]"));
242    }
243
244    #[test]
245    fn optional_member_uses_nullable() {
246        let cs = gen_cs("struct S { @optional long maybe; };");
247        assert!(cs.contains("int? Maybe"));
248    }
249
250    #[test]
251    fn exception_inherits_exception() {
252        let cs = gen_cs("exception NotFound { string what_; };");
253        assert!(cs.contains("class NotFound : Exception"));
254    }
255
256    #[test]
257    fn union_uses_discriminator_record() {
258        let cs = gen_cs(
259            "union U switch (long) { case 1: long a; case 2: double b; default: octet c; };",
260        );
261        assert!(cs.contains("record class U"));
262        assert!(cs.contains("public int Discriminator"));
263        assert!(cs.contains("public object? Value"));
264        assert!(cs.contains("// case default"));
265    }
266
267    #[test]
268    fn header_starts_with_generated_marker() {
269        let cs = gen_cs("");
270        assert!(cs.starts_with("// Generated by zerodds idl-csharp."));
271    }
272
273    #[test]
274    fn nullable_enable_appears_exactly_once() {
275        let cs = gen_cs("module M { struct S { long x; }; };");
276        let count = cs.matches("#nullable enable").count();
277        assert_eq!(count, 1);
278    }
279
280    #[test]
281    fn record_class_is_init_only() {
282        let cs = gen_cs("struct S { long x; };");
283        assert!(cs.contains("get; init;"));
284    }
285
286    #[test]
287    fn root_namespace_option_wraps_output() {
288        let ast =
289            zerodds_idl::parse("struct S { long x; };", &ParserConfig::default()).expect("parse");
290        let opts = CsGenOptions {
291            root_namespace: Some("Zerodds.Generated".into()),
292            ..Default::default()
293        };
294        let cs = generate_csharp(&ast, &opts).expect("gen");
295        assert!(cs.contains("namespace Zerodds.Generated"));
296        assert!(cs.contains("} // namespace Zerodds.Generated"));
297    }
298
299    #[test]
300    fn non_service_interface_emits_csharp_interface() {
301        let ast = zerodds_idl::parse("interface I { void op(); };", &ParserConfig::default())
302            .expect("parse");
303        let cs = generate_csharp(&ast, &CsGenOptions::default()).expect("ok");
304        assert!(cs.contains("public interface I"));
305    }
306
307    #[test]
308    fn const_decl_emits_const() {
309        let cs = gen_cs("const long MAX = 100;");
310        assert!(cs.contains("public const int MAX = 100;"));
311    }
312
313    #[test]
314    fn options_have_sensible_defaults() {
315        let o = CsGenOptions::default();
316        assert_eq!(o.indent_width, 4);
317        assert!(o.root_namespace.is_none());
318        assert!(o.use_records);
319    }
320
321    #[test]
322    fn options_clone_works() {
323        let o = CsGenOptions {
324            root_namespace: Some("Foo".into()),
325            indent_width: 2,
326            use_records: false,
327            emit_corba_traits: false,
328        };
329        let cloned = o.clone();
330        assert_eq!(cloned.indent_width, 2);
331        assert_eq!(cloned.root_namespace.as_deref(), Some("Foo"));
332        assert!(!cloned.use_records);
333    }
334}