zerodds-idl-java 1.0.0-rc.1

OMG IDL4 → Java 17 Code-Generator (idl4-java-1.0 + DDS-Java-PSM) für ZeroDDS.
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 ZeroDDS Contributors
//! IDL-to-Java 1.0 Annex A.1 — CORBA-spezifische Type-Mappings.
//!
//! Spec: `idl4-java-1.0` Annex A.1. Java-Aequivalent zum
//! C++-`CORBA::traits<T>`-Template:
//!
//! - Pro Top-Level-Type wird eine separate Companion-Datei
//!   `<TypeName>CorbaTraits.java` im selben Package emittiert.
//! - Sie traegt per-Type-Konstanten (`FULL_NAME`, `IS_VARIABLE_SIZE`,
//!   `IS_LOCAL`) und ein Marker-Annotation-Hint, das CORBA-Tooling
//!   (JacORB / Java-SE-CORBA) konsumieren kann.
//!
//! Variable-size-Klassifikation analog idl-cpp / idl-csharp:
//! string/sequence/map/scoped → variable.
//!
//! Opt-in via `JavaGenOptions::emit_corba_traits = true`.

use std::fmt::Write;

use zerodds_idl::ast::{
    ConstrTypeDecl, Definition, Specification, StructDcl, StructDef, TypeDecl, TypeSpec, UnionDcl,
    UnionDef,
};

use crate::JavaGenOptions;
use crate::emitter::JavaFile;
use crate::error::JavaGenError;

/// Emittiert pro Top-Level-Struct/Union/Enum eine
/// `<TypeName>CorbaTraits.java`-Datei.
///
/// # Errors
/// `JavaGenError::Internal` bei `fmt::Write`-Fehler.
pub(crate) fn emit_corba_traits_files(
    spec: &Specification,
    opts: &JavaGenOptions,
) -> Result<Vec<JavaFile>, JavaGenError> {
    let pkg = sanitize_package(&opts.root_package);
    let mut emitted = Vec::new();
    collect_top_level(&spec.definitions, &pkg, &mut Vec::new(), &mut emitted);

    let mut files = Vec::new();
    for entry in emitted {
        files.push(emit_one(&entry)?);
    }
    Ok(files)
}

#[derive(Debug)]
struct TraitEntry {
    package_path: String,
    class_name: String,
    full_name: String,
    variable_size: bool,
}

fn sanitize_package(p: &str) -> String {
    p.split('.')
        .filter(|s| !s.is_empty())
        .collect::<Vec<_>>()
        .join(".")
}

fn lower(s: &str) -> String {
    s.to_lowercase()
}

/// zerodds-lint: recursion-depth 32 (IDL-Module-Nesting)
fn collect_top_level(
    defs: &[Definition],
    pkg: &str,
    scope: &mut Vec<String>,
    out: &mut Vec<TraitEntry>,
) {
    for d in defs {
        match d {
            Definition::Module(m) => {
                let inner_pkg = if pkg.is_empty() {
                    lower(&m.name.text)
                } else {
                    format!("{}.{}", pkg, lower(&m.name.text))
                };
                scope.push(m.name.text.clone());
                collect_top_level(&m.definitions, &inner_pkg, scope, out);
                scope.pop();
            }
            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
                out.push(TraitEntry {
                    package_path: pkg.to_string(),
                    class_name: s.name.text.clone(),
                    full_name: full_name(scope, &s.name.text),
                    variable_size: struct_is_variable(s),
                });
            }
            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
                out.push(TraitEntry {
                    package_path: pkg.to_string(),
                    class_name: u.name.text.clone(),
                    full_name: full_name(scope, &u.name.text),
                    variable_size: union_is_variable(u),
                });
            }
            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(e))) => {
                out.push(TraitEntry {
                    package_path: pkg.to_string(),
                    class_name: e.name.text.clone(),
                    full_name: full_name(scope, &e.name.text),
                    variable_size: false,
                });
            }
            _ => {}
        }
    }
}

fn full_name(scope: &[String], name: &str) -> String {
    if scope.is_empty() {
        name.to_string()
    } else {
        format!("{}::{name}", scope.join("::"))
    }
}

fn struct_is_variable(s: &StructDef) -> bool {
    s.members.iter().any(|m| type_is_variable(&m.type_spec))
}

fn union_is_variable(u: &UnionDef) -> bool {
    u.cases
        .iter()
        .any(|c| type_is_variable(&c.element.type_spec))
}

fn type_is_variable(ts: &TypeSpec) -> bool {
    match ts {
        TypeSpec::Primitive(_) => false,
        TypeSpec::Fixed(_) => false,
        TypeSpec::String(_) => true,
        TypeSpec::Sequence(_) => true,
        TypeSpec::Map(_) => true,
        TypeSpec::Scoped(_) => true,
        TypeSpec::Any => true,
    }
}

fn emit_one(entry: &TraitEntry) -> Result<JavaFile, JavaGenError> {
    let class_name = format!("{}CorbaTraits", entry.class_name);
    let mut src = String::new();
    writeln!(&mut src, "// Generated by zerodds idl-java. Do not edit.").map_err(fmt_err)?;
    writeln!(&mut src, "// Annex A.1 — CORBA-spezifische Type-Mappings.").map_err(fmt_err)?;
    if !entry.package_path.is_empty() {
        writeln!(&mut src, "package {};", entry.package_path).map_err(fmt_err)?;
        writeln!(&mut src).map_err(fmt_err)?;
    }
    writeln!(&mut src, "/**").map_err(fmt_err)?;
    writeln!(
        &mut src,
        " * CORBA-Annex-A.1 Trait-Konstanten fuer Type {}.",
        entry.full_name
    )
    .map_err(fmt_err)?;
    writeln!(
        &mut src,
        " * Aequivalent zum C++-`CORBA::traits<T>` Template."
    )
    .map_err(fmt_err)?;
    writeln!(&mut src, " */").map_err(fmt_err)?;
    writeln!(&mut src, "public final class {class_name} {{").map_err(fmt_err)?;
    writeln!(
        &mut src,
        "    public static final String FULL_NAME = \"{}\";",
        entry.full_name
    )
    .map_err(fmt_err)?;
    writeln!(
        &mut src,
        "    public static final boolean IS_VARIABLE_SIZE = {};",
        if entry.variable_size { "true" } else { "false" }
    )
    .map_err(fmt_err)?;
    writeln!(
        &mut src,
        "    public static final boolean IS_LOCAL = false;"
    )
    .map_err(fmt_err)?;
    writeln!(&mut src).map_err(fmt_err)?;
    writeln!(&mut src, "    private {class_name}() {{}}").map_err(fmt_err)?;
    writeln!(&mut src, "}}").map_err(fmt_err)?;

    Ok(JavaFile {
        package_path: entry.package_path.clone(),
        class_name,
        source: src,
    })
}

fn fmt_err(_: std::fmt::Error) -> JavaGenError {
    JavaGenError::Internal("string formatting failed".into())
}

#[cfg(test)]
mod tests {
    #![allow(clippy::expect_used)]
    use crate::{JavaGenOptions, generate_java_files_with_corba_traits};
    use zerodds_idl::config::ParserConfig;

    fn render(src: &str) -> Vec<crate::emitter::JavaFile> {
        let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse");
        let opts = JavaGenOptions::default();
        generate_java_files_with_corba_traits(&ast, &opts).expect("gen")
    }

    fn find_traits<'a>(
        files: &'a [crate::emitter::JavaFile],
        class: &str,
    ) -> Option<&'a crate::emitter::JavaFile> {
        files.iter().find(|f| f.class_name == class)
    }

    #[test]
    fn empty_source_emits_no_traits_files() {
        let files = render("");
        assert!(files.iter().all(|f| !f.class_name.contains("CorbaTraits")));
    }

    #[test]
    fn struct_emits_companion_traits_file() {
        let files = render("struct S { long x; };");
        let f = find_traits(&files, "SCorbaTraits").expect("traits file");
        assert!(f.source.contains("public final class SCorbaTraits"));
        assert!(
            f.source
                .contains("public static final String FULL_NAME = \"S\";")
        );
        assert!(
            f.source
                .contains("public static final boolean IS_VARIABLE_SIZE = false;")
        );
        assert!(
            f.source
                .contains("public static final boolean IS_LOCAL = false;")
        );
    }

    #[test]
    fn variable_size_struct_marked_correctly() {
        let files = render("struct S { string name; };");
        let f = find_traits(&files, "SCorbaTraits").expect("traits file");
        assert!(f.source.contains("IS_VARIABLE_SIZE = true;"));
    }

    #[test]
    fn enum_emits_traits_file() {
        let files = render("enum Color { RED, GREEN, BLUE };");
        let f = find_traits(&files, "ColorCorbaTraits").expect("traits");
        assert!(f.source.contains("FULL_NAME = \"Color\";"));
        assert!(f.source.contains("IS_VARIABLE_SIZE = false;"));
    }

    #[test]
    fn nested_module_yields_correct_package_path() {
        let files = render("module A { module B { struct S { long x; }; }; };");
        let f = find_traits(&files, "SCorbaTraits").expect("traits");
        assert_eq!(f.package_path, "a.b");
        assert!(f.source.contains("package a.b;"));
        assert!(f.source.contains("FULL_NAME = \"A::B::S\";"));
    }

    #[test]
    fn union_with_string_branch_is_variable() {
        let files = render("union U switch (long) { case 1: string s; case 2: long n; };");
        let f = find_traits(&files, "UCorbaTraits").expect("traits");
        assert!(f.source.contains("IS_VARIABLE_SIZE = true;"));
    }

    #[test]
    fn sequence_member_marks_struct_variable() {
        let files = render("struct S { sequence<long> data; };");
        let f = find_traits(&files, "SCorbaTraits").expect("traits");
        assert!(f.source.contains("IS_VARIABLE_SIZE = true;"));
    }

    #[test]
    fn private_constructor_prevents_instantiation() {
        let files = render("struct S { long x; };");
        let f = find_traits(&files, "SCorbaTraits").expect("traits");
        assert!(f.source.contains("private SCorbaTraits() {}"));
    }

    #[test]
    fn no_local_default_set_to_false() {
        let files = render("struct S { long x; };");
        let f = find_traits(&files, "SCorbaTraits").expect("traits");
        assert!(f.source.contains("IS_LOCAL = false;"));
    }
}