zerodds-idl-csharp 1.0.0-rc.1

OMG IDL4 → C# 10 Code-Generator (idl4-csharp-1.0) für ZeroDDS.
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 ZeroDDS Contributors
//! IDL4 → C# 12.0 Source-Code-Generator (OMG IDL4-CSharp-Mapping,
//! formal/2024-12-01).
//!
//! Crate `zerodds-idl-csharp` — Foundation des Sprach-Bindings (Cluster C5.3-a).
//!
//! Safety classification: **SAFE (std-only)**. Reines Build-Zeit-Tool —
//! `forbid(unsafe_code)`, kein no_std-Use-Case.
//!
//! # Scope (C5.3-a / Phase 3.1-3.3)
//! - Phase 3.1: Header-Layout (`#nullable enable`, `using`, `namespace`).
//! - Phase 3.2: Primitive-Mapping
//!   (boolean → bool, octet → byte, char/wchar → char,
//!   short → short, ushort → ushort, long → int, ulong → uint,
//!   long long → long, ulong long → ulong, float/double, string → string).
//! - Phase 3.3: Aggregate-Types — struct → `record class` mit init-only
//!   Properties, enum → `enum`, union → discriminated record,
//!   typedef → `record class`-Wrapper (Spec-konformes file-scoped
//!   using-alias kommt mit C5.3-b), sequence → `IList<T>`, array → `T[]`,
//!   inheritance → `record class : Parent`.
//!
//! # C5.3-b additions (this revision)
//! - `ISequence<T>` / `IBoundedSequence<T>` runtime contract
//!   (see `runtime/Omg.Types.cs`); codegen emits these instead of
//!   bare `IList<T>`.
//! - Annotation-Bridge `@key|@id|@optional|@must_understand|@external`
//!   on members, `@nested|@extensibility(...)` on types — emitted as
//!   C# attributes from `Omg.Types`.
//! - `ITopicType<T>` marker on every top-level (non-`@nested`) struct.
//!
//! # Bewusst nicht im Crate
//! - Phase 3.4: DDS-CSharp-Integration (P/Invoke zu Rust-Core).
//! - Time/Duration/Status/QoS/Listener-Codegen.
//! - File-scoped namespace + `using <Alias> = <Type>;` Top-Level-Form.
//! - Bitset/Bitmask/Map/Fixed/Any/Interface/Valuetype.
//!
//! # Beispiel
//!
//! ```
//! use zerodds_idl::config::ParserConfig;
//! use zerodds_idl_csharp::{generate_csharp, CsGenOptions};
//!
//! let ast = zerodds_idl::parse(
//!     "module M { struct S { long x; }; };",
//!     &ParserConfig::default(),
//! )
//! .expect("parse");
//! let cs = generate_csharp(&ast, &CsGenOptions::default()).expect("gen");
//! assert!(cs.contains("namespace M"));
//! assert!(cs.contains("record class S"));
//! ```

#![forbid(unsafe_code)]
#![warn(missing_docs)]

mod annotations;
pub(crate) mod bitset;
pub(crate) mod corba_traits;
pub mod emitter;
pub mod error;
pub mod keywords;
pub mod type_map;
pub(crate) mod typesupport;
pub(crate) mod verbatim;

pub use error::CsGenError;

use zerodds_idl::ast::Specification;

/// Konfiguration des C#-Code-Generators.
#[derive(Debug, Clone)]
pub struct CsGenOptions {
    /// Optionaler aeusserer Namespace, in den der gesamte Output gewickelt
    /// wird. `None` oder leer = kein Wrapper.
    pub root_namespace: Option<String>,
    /// Indent-Breite in Leerzeichen. Default 4 (C# Coding-Conventions).
    pub indent_width: usize,
    /// Wenn `true`: struct-Mapping verwendet `record class` (Spec-konform).
    /// Wenn `false`: struct-Mapping verwendet plain `class` (Legacy-CCM).
    /// Default `true`.
    pub use_records: bool,
    /// Annex A.1 (idl4-csharp-1.0) — opt-in: emittiert pro
    /// Top-Level-Type CORBA-Marker (`Corba.ValueTypeAttribute`)
    /// + statischen `Corba.Traits`-Helper. Default `false`.
    pub emit_corba_traits: bool,
}

impl Default for CsGenOptions {
    fn default() -> Self {
        Self {
            root_namespace: None,
            indent_width: 4,
            use_records: true,
            emit_corba_traits: false,
        }
    }
}

/// Erzeugt einen vollstaendigen C# 12.0-Quelltext aus einer IDL-Specification.
///
/// # Errors
/// - [`CsGenError::UnsupportedConstruct`]: IDL-Konstrukt außerhalb des aktuellen Scopes
///   (z.B. `interface`, `valuetype`, `fixed`, `any`, `bitset`, `bitmask`).
/// - [`CsGenError::InvalidName`]: Ein Identifier ist leer oder bereits
///   `@`-prefixed (Doppel-Escape).
/// - [`CsGenError::InheritanceCycle`]: Direkte oder indirekte
///   Self-Inheritance im Struct-Graphen.
pub fn generate_csharp(ast: &Specification, opts: &CsGenOptions) -> Result<String, CsGenError> {
    let mut out = emitter::emit_source(ast, opts)?;
    if opts.emit_corba_traits {
        corba_traits::emit_corba_traits(&mut out, ast)?;
    }
    Ok(out)
}

/// Convenience-Variante mit aktiviertem `emit_corba_traits`-Flag.
///
/// Cross-Ref: `idl4-csharp-1.0` Annex A.1.
///
/// # Errors
/// Wie [`generate_csharp`].
pub fn generate_csharp_with_corba_traits(
    ast: &Specification,
    opts: &CsGenOptions,
) -> Result<String, CsGenError> {
    let opts = CsGenOptions {
        emit_corba_traits: true,
        ..opts.clone()
    };
    generate_csharp(ast, &opts)
}

#[cfg(test)]
mod tests {
    #![allow(clippy::expect_used, clippy::panic)]
    use super::*;
    use zerodds_idl::config::ParserConfig;

    fn gen_cs(src: &str) -> String {
        let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
        generate_csharp(&ast, &CsGenOptions::default()).expect("gen must succeed")
    }

    #[test]
    fn empty_source_emits_only_preamble() {
        let cs = gen_cs("");
        assert!(cs.contains("// Generated by zerodds idl-csharp"));
        assert!(cs.contains("#nullable enable"));
        assert!(cs.contains("using System;"));
        assert!(!cs.contains("namespace M"));
    }

    #[test]
    fn empty_module_emits_namespace() {
        let cs = gen_cs("module M {};");
        assert!(cs.contains("namespace M"));
        assert!(cs.contains("} // namespace M"));
    }

    #[test]
    fn three_level_modules_nest() {
        let cs = gen_cs("module A { module B { module C {}; }; };");
        assert!(cs.contains("namespace A"));
        assert!(cs.contains("namespace B"));
        assert!(cs.contains("namespace C"));
    }

    #[test]
    fn primitive_struct_member_uses_correct_cs_types() {
        let cs = gen_cs(
            "struct S { boolean b; octet o; short s; long l; long long ll; \
             unsigned short us; unsigned long ul; unsigned long long ull; \
             float f; double d; };",
        );
        assert!(cs.contains("public bool B"));
        assert!(cs.contains("public byte O"));
        assert!(cs.contains("public short S"));
        assert!(cs.contains("public int L "));
        assert!(cs.contains("public long Ll"));
        assert!(cs.contains("public ushort Us"));
        assert!(cs.contains("public uint Ul "));
        assert!(cs.contains("public ulong Ull"));
        assert!(cs.contains("public float F"));
        assert!(cs.contains("public double D"));
    }

    #[test]
    fn string_member_uses_string() {
        let cs = gen_cs("struct S { string name; };");
        assert!(cs.contains("public string Name"));
    }

    #[test]
    fn sequence_member_uses_isequence() {
        // C5.3-b: unbounded sequence → `ISequence<T>` from Omg.Types.
        let cs = gen_cs("struct S { sequence<long> data; };");
        assert!(cs.contains("using System.Collections.Generic;"));
        assert!(cs.contains("using Omg.Types;"));
        assert!(cs.contains("ISequence<int>"));
    }

    #[test]
    fn bounded_sequence_member_uses_ibounded_sequence() {
        // C5.3-b: bounded sequence → `IBoundedSequence<T>` from Omg.Types.
        let cs = gen_cs("struct S { sequence<long, 100> data; };");
        assert!(cs.contains("using Omg.Types;"));
        assert!(cs.contains("IBoundedSequence<int>"));
    }

    #[test]
    fn array_member_uses_jagged_array() {
        let cs = gen_cs("struct S { long matrix[3][4]; };");
        // Jagged: int[][] (Spec laesst beide Varianten zu).
        assert!(cs.contains("int[][]"));
    }

    #[test]
    fn enum_emits_int_backed_enum() {
        let cs = gen_cs("enum Color { RED, GREEN, BLUE };");
        assert!(cs.contains("public enum Color : int"));
        assert!(cs.contains("RED,"));
        assert!(cs.contains("BLUE,"));
    }

    #[test]
    fn typedef_emits_alias_record() {
        let cs = gen_cs("typedef long MyInt;");
        assert!(cs.contains("public sealed record class MyInt(int Value);"));
    }

    #[test]
    fn inheritance_emits_record_inheritance() {
        let cs = gen_cs("struct Parent { long x; }; struct Child : Parent { long y; };");
        assert!(cs.contains("record class Child : Parent"));
    }

    #[test]
    fn keyed_struct_marker_appears() {
        let cs = gen_cs("struct S { @key long id; long val; };");
        assert!(cs.contains("[Key]"));
    }

    #[test]
    fn optional_member_uses_nullable() {
        let cs = gen_cs("struct S { @optional long maybe; };");
        assert!(cs.contains("int? Maybe"));
    }

    #[test]
    fn exception_inherits_exception() {
        let cs = gen_cs("exception NotFound { string what_; };");
        assert!(cs.contains("class NotFound : Exception"));
    }

    #[test]
    fn union_uses_discriminator_record() {
        let cs = gen_cs(
            "union U switch (long) { case 1: long a; case 2: double b; default: octet c; };",
        );
        assert!(cs.contains("record class U"));
        assert!(cs.contains("public int Discriminator"));
        assert!(cs.contains("public object? Value"));
        assert!(cs.contains("// case default"));
    }

    #[test]
    fn header_starts_with_generated_marker() {
        let cs = gen_cs("");
        assert!(cs.starts_with("// Generated by zerodds idl-csharp."));
    }

    #[test]
    fn nullable_enable_appears_exactly_once() {
        let cs = gen_cs("module M { struct S { long x; }; };");
        let count = cs.matches("#nullable enable").count();
        assert_eq!(count, 1);
    }

    #[test]
    fn record_class_is_init_only() {
        let cs = gen_cs("struct S { long x; };");
        assert!(cs.contains("get; init;"));
    }

    #[test]
    fn root_namespace_option_wraps_output() {
        let ast =
            zerodds_idl::parse("struct S { long x; };", &ParserConfig::default()).expect("parse");
        let opts = CsGenOptions {
            root_namespace: Some("Zerodds.Generated".into()),
            ..Default::default()
        };
        let cs = generate_csharp(&ast, &opts).expect("gen");
        assert!(cs.contains("namespace Zerodds.Generated"));
        assert!(cs.contains("} // namespace Zerodds.Generated"));
    }

    #[test]
    fn non_service_interface_emits_csharp_interface() {
        let ast = zerodds_idl::parse("interface I { void op(); };", &ParserConfig::default())
            .expect("parse");
        let cs = generate_csharp(&ast, &CsGenOptions::default()).expect("ok");
        assert!(cs.contains("public interface I"));
    }

    #[test]
    fn const_decl_emits_const() {
        let cs = gen_cs("const long MAX = 100;");
        assert!(cs.contains("public const int MAX = 100;"));
    }

    #[test]
    fn options_have_sensible_defaults() {
        let o = CsGenOptions::default();
        assert_eq!(o.indent_width, 4);
        assert!(o.root_namespace.is_none());
        assert!(o.use_records);
    }

    #[test]
    fn options_clone_works() {
        let o = CsGenOptions {
            root_namespace: Some("Foo".into()),
            indent_width: 2,
            use_records: false,
            emit_corba_traits: false,
        };
        let cloned = o.clone();
        assert_eq!(cloned.indent_width, 2);
        assert_eq!(cloned.root_namespace.as_deref(), Some("Foo"));
        assert!(!cloned.use_records);
    }
}