Skip to main content

zerodds_idl_java/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! IDL4 → Java-17-Source-Codegen (OMG IDL4-Java-Mapping v1.0).
4//!
5//! Crate `zerodds-idl-java` — Java-Sprach-Bindings, Cluster C5.4-a (Foundation)
6//! plus C5.4-b (Bitset/Bitmask, Multi-Inheritance, Annotation-Bridge,
7//! TopicType-Marker).
8//!
9//! Safety classification: **SAFE (std-only)**. Reines Build-Zeit-Tool —
10//! `forbid(unsafe_code)`, kein no_std-Use-Case.
11//!
12//! # Scope (C5.4-a)
13//! - Block A: Header-Layout (`package`, Class-Modifiers, FQN-Imports).
14//! - Block B: Primitive-Mapping (boolean → boolean, octet → byte, ...,
15//!   inkl. unsigned-Workaround per Spec §6).
16//! - Block C: struct → public class (Bean-Pattern), enum, union →
17//!   sealed interface + case-records, typedef → Wrapper-Class,
18//!   sequence → `java.util.List<T>`, array → Java-Array, single
19//!   inheritance → `extends`.
20//! - Block D: Exception → `class X extends RuntimeException`.
21//!
22//! # Scope (C5.4-b — Cluster E)
23//! - Bitmask → Wrapper-Class mit Inner-Enum `Flag` und
24//!   `EnumSet<Flag> bits` (Spec idl4-java-1.0 §6.3).
25//! - Bitset (≤ 64 Bit kumulativ) → Wrapper-Class mit `long bits` und
26//!   Mask/Shift-Accessors pro Bitfield. > 64 Bit → harter Fehler
27//!   [`error::JavaGenError::UnsupportedConstruct`].
28//! - Multi-Inheritance via Interface-Pattern: jeder Struct, der selbst
29//!   Basis eines anderen Structs ist, bekommt ein
30//!   `<Name>Interface.java`-Companion. Sub-Sub-Klassen verwenden
31//!   `extends DirectBase implements GrandparentInterface, ...`.
32//! - `@value(N)` auf Enum-Members → expliziter `int`-Konstruktor-Wert
33//!   statt Auto-Ordinal.
34//! - Annotation-Bridge: `@key`, `@id(N)`, `@optional`,
35//!   `@must_understand`, `@external`, `@nested`, `@extensibility(...)`
36//!   → Java-Annotations unter `org.zerodds.types.*` (siehe
37//!   `runtime/`).
38//! - DDS-Java-PSM-Stub: jeder Top-Level-`struct` ohne `@nested`
39//!   implementiert `org.omg.dds.topic.TopicType<SelfType>`.
40//!
41//! # Bewusst nicht im Crate
42//! - Cluster F-H: ServiceEnvironment-SPI, Time/Duration/Status/QoS/
43//!   Listener-Codegen (C5.5).
44//! - Reflection-basierte TypeRep (java-psm §8) — Stretch-Goal.
45//! - JNI-Bridge zu Rust-Core (C5.5).
46//! - `interface`, `valuetype`, `fixed`, `any`, `map<K,V>` → kommen
47//!   mit `zerodds-rpc-java`.
48//!
49//! # Multi-File-Output
50//! Java erfordert eine `.java`-Datei pro top-level public class.
51//! Daher gibt [`generate_java_files`] eine [`Vec<JavaFile>`] zurueck;
52//! jede `JavaFile` hat package-path + class-name + source.
53//!
54//! # Beispiel
55//!
56//! ```
57//! use zerodds_idl::config::ParserConfig;
58//! use zerodds_idl_java::{generate_java_files, JavaGenOptions};
59//!
60//! let ast = zerodds_idl::parse(
61//!     "module M { struct S { long x; }; };",
62//!     &ParserConfig::default(),
63//! )
64//! .expect("parse");
65//! let files = generate_java_files(&ast, &JavaGenOptions::default()).expect("gen");
66//! // POJO + TypeSupport (zerodds-xcdr2-java-1.0 §4).
67//! assert_eq!(files.len(), 2);
68//! let pojo = files.iter().find(|f| f.class_name == "S").expect("POJO");
69//! assert!(pojo.source.contains("package m;"));
70//! assert!(pojo.source.contains("public class S"));
71//! assert!(files.iter().any(|f| f.class_name == "STypeSupport"));
72//! ```
73
74#![forbid(unsafe_code)]
75#![warn(missing_docs)]
76
77pub(crate) mod amqp;
78pub(crate) mod annotations;
79pub(crate) mod bitset;
80pub(crate) mod corba_traits;
81pub mod emitter;
82pub mod error;
83pub mod keywords;
84pub mod rpc;
85pub mod type_map;
86pub(crate) mod typesupport;
87pub(crate) mod verbatim;
88
89pub use emitter::JavaFile;
90pub use error::JavaGenError;
91
92use zerodds_idl::ast::Specification;
93
94/// Konfiguration des Java-Code-Generators.
95#[derive(Debug, Clone)]
96pub struct JavaGenOptions {
97    /// Java-Root-Package, in das alle generierten Klassen gehoeren
98    /// (z.B. `"org.example.types"`). Leer-String = Default-Package.
99    pub root_package: String,
100    /// Indent-Breite in Leerzeichen. Default 4.
101    pub indent_width: usize,
102    /// Wenn `true`, werden flache Aggregat-Types als Java `record`
103    /// emittiert (Java 14+). Default `false` — Spec verlangt
104    /// Bean-Pattern.
105    pub use_records: bool,
106    /// Spec §7.2.3 / §8.1.2 / §8.1.3 — opt-in: emittiert pro
107    /// Top-Level-Struct/Union eine zusätzliche `<TypeName>AmqpCodec.java`-
108    /// Datei mit statischen `toAmqpValue` / `toJsonString`-Helpern.
109    /// Default `false`, weil die emittierten Calls eine
110    /// `org.zerodds.amqp`-Runtime-Library voraussetzen.
111    pub emit_amqp_helpers: bool,
112    /// Annex A.1 (idl4-java-1.0) — opt-in: emittiert pro
113    /// Top-Level-Type eine zusaetzliche `<TypeName>CorbaTraits.java`-
114    /// Datei mit per-Type-Konstanten (`FULL_NAME`, `IS_VARIABLE_SIZE`,
115    /// `IS_LOCAL`). Default `false`.
116    pub emit_corba_traits: bool,
117    /// Spec zerodds-xcdr2-java-1.0 §4 — opt-in: emittiert pro
118    /// Top-Level-Struct eine zusaetzliche `<TypeName>TypeSupport.java`-
119    /// Datei mit `org.zerodds.cdr.TopicTypeSupport<T>`-
120    /// Implementierung (encode/decode/keyHash + INSTANCE).
121    /// Default `true` ab v1.0 (Spec-Pflicht).
122    pub emit_typesupport: bool,
123}
124
125impl Default for JavaGenOptions {
126    fn default() -> Self {
127        Self {
128            root_package: String::new(),
129            indent_width: 4,
130            use_records: false,
131            emit_amqp_helpers: false,
132            emit_corba_traits: false,
133            emit_typesupport: true,
134        }
135    }
136}
137
138/// Erzeugt eine Liste von Java-Source-Files aus einer IDL-Specification.
139///
140/// # Errors
141/// - [`JavaGenError::UnsupportedConstruct`]: IDL-Konstrukt außerhalb des aktuellen Scopes
142///   (z.B. `interface`, `valuetype`, `fixed`, `any`) oder C5.4-b-
143///   Constraint verletzt (z.B. Bitset-Summe > 64 Bit, Bitmask-
144///   `bit_bound > 64`).
145/// - [`JavaGenError::InvalidName`]: Ein Identifier ist leer oder
146///   kollidiert nach Sanitisierung weiterhin mit einem Java-Keyword.
147/// - [`JavaGenError::InheritanceCycle`]: Direkte oder indirekte
148///   Self-Inheritance im Struct-Graphen.
149pub fn generate_java_files(
150    ast: &Specification,
151    opts: &JavaGenOptions,
152) -> Result<Vec<JavaFile>, JavaGenError> {
153    let mut files = emitter::emit_files(ast, opts)?;
154    if opts.emit_amqp_helpers {
155        files.extend(amqp::emit_amqp_codec_files(ast, opts)?);
156    }
157    if opts.emit_corba_traits {
158        files.extend(corba_traits::emit_corba_traits_files(ast, opts)?);
159    }
160    if opts.emit_typesupport {
161        files.extend(typesupport::emit_typesupport_files(ast, opts)?);
162    }
163    Ok(files)
164}
165
166/// Convenience-Variante mit aktiviertem `emit_corba_traits`-Flag.
167///
168/// Cross-Ref: `idl4-java-1.0` Annex A.1.
169///
170/// # Errors
171/// Wie [`generate_java_files`].
172pub fn generate_java_files_with_corba_traits(
173    ast: &Specification,
174    opts: &JavaGenOptions,
175) -> Result<Vec<JavaFile>, JavaGenError> {
176    let opts = JavaGenOptions {
177        emit_corba_traits: true,
178        ..opts.clone()
179    };
180    generate_java_files(ast, &opts)
181}
182
183/// Convenience-Variante mit aktiviertem `emit_amqp_helpers`-Flag.
184///
185/// Identisch zu [`generate_java_files`], aber zwingt
186/// `opts.emit_amqp_helpers = true`.
187///
188/// # Errors
189/// Wie [`generate_java_files`].
190pub fn generate_java_files_with_amqp(
191    ast: &Specification,
192    opts: &JavaGenOptions,
193) -> Result<Vec<JavaFile>, JavaGenError> {
194    let opts = JavaGenOptions {
195        emit_amqp_helpers: true,
196        ..opts.clone()
197    };
198    generate_java_files(ast, &opts)
199}
200
201#[cfg(test)]
202mod tests {
203    #![allow(clippy::expect_used, clippy::panic)]
204    use super::*;
205    use zerodds_idl::config::ParserConfig;
206
207    fn gen_java(src: &str) -> Vec<JavaFile> {
208        // Inline-Tests pruefen das POJO-Emitter-Verhalten — TypeSupport
209        // hat eigene Tests in `typesupport`-Modul + Snapshots.
210        let opts = JavaGenOptions {
211            emit_typesupport: false,
212            ..Default::default()
213        };
214        let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
215        generate_java_files(&ast, &opts).expect("gen must succeed")
216    }
217
218    fn gen_with(src: &str, opts: &JavaGenOptions) -> Vec<JavaFile> {
219        let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
220        generate_java_files(&ast, opts).expect("gen must succeed")
221    }
222
223    #[test]
224    fn empty_source_emits_no_files() {
225        assert!(gen_java("").is_empty());
226    }
227
228    #[test]
229    fn empty_module_emits_no_files() {
230        // Module ohne Type-Defs erzeugt keine Java-File (Java hat keinen
231        // package-marker-File).
232        assert!(gen_java("module M {};").is_empty());
233    }
234
235    #[test]
236    fn struct_emits_one_file_per_type() {
237        let files = gen_java("struct A { long x; }; struct B { long y; }; struct C { long z; };");
238        assert_eq!(files.len(), 3);
239        let names: Vec<&str> = files.iter().map(|f| f.class_name.as_str()).collect();
240        assert!(names.contains(&"A"));
241        assert!(names.contains(&"B"));
242        assert!(names.contains(&"C"));
243    }
244
245    #[test]
246    fn module_becomes_lowercase_package() {
247        let files = gen_java("module Foo { struct S { long x; }; };");
248        assert_eq!(files.len(), 1);
249        assert_eq!(files[0].package_path, "foo");
250        assert!(files[0].source.contains("package foo;"));
251    }
252
253    #[test]
254    fn three_level_modules_become_three_packages() {
255        let files = gen_java("module A { module B { module C { struct S { long x; }; }; }; };");
256        assert_eq!(files.len(), 1);
257        assert_eq!(files[0].package_path, "a.b.c");
258        assert!(files[0].source.contains("package a.b.c;"));
259    }
260
261    #[test]
262    fn primitive_struct_uses_correct_java_types() {
263        let files = gen_java(
264            "struct S { boolean b; octet o; short s; long l; long long ll; \
265             unsigned short us; unsigned long ul; unsigned long long ull; \
266             float f; double d; char c; wchar wc; string str; };",
267        );
268        let src = &files[0].source;
269        assert!(src.contains("private boolean b;"));
270        assert!(src.contains("private byte o;"));
271        assert!(src.contains("private short s;"));
272        assert!(src.contains("private int l;"));
273        assert!(src.contains("private long ll;"));
274        // Unsigned-Workaround:
275        assert!(src.contains("private int us;"));
276        assert!(src.contains("private long ul;"));
277        assert!(src.contains("private long ull;"));
278        assert!(src.contains("private float f;"));
279        assert!(src.contains("private double d;"));
280        assert!(src.contains("private char c;"));
281        assert!(src.contains("private char wc;"));
282        assert!(src.contains("private String str;"));
283    }
284
285    #[test]
286    fn unsigned_member_gets_doc_comment() {
287        let files = gen_java("struct S { unsigned long u; };");
288        assert!(files[0].source.contains("unsigned IDL value"));
289    }
290
291    #[test]
292    fn enum_emits_explicit_values() {
293        let files = gen_java("enum Color { RED, GREEN, BLUE };");
294        let src = &files[0].source;
295        assert!(src.contains("public enum Color {"));
296        assert!(src.contains("RED(0),"));
297        assert!(src.contains("GREEN(1),"));
298        assert!(src.contains("BLUE(2);"));
299        assert!(src.contains("public int value()"));
300    }
301
302    #[test]
303    fn union_emits_sealed_interface() {
304        let files = gen_java(
305            "union U switch (long) { case 1: long a; case 2: double b; default: octet c; };",
306        );
307        let src = &files[0].source;
308        assert!(src.contains("public sealed interface U"));
309        assert!(src.contains("permits"));
310        assert!(src.contains("record A(int a) implements U"));
311        assert!(src.contains("record B(double b) implements U"));
312        assert!(src.contains("// case default"));
313    }
314
315    #[test]
316    fn typedef_emits_wrapper_class() {
317        let files = gen_java("typedef long Counter;");
318        assert_eq!(files.len(), 1);
319        let src = &files[0].source;
320        assert!(src.contains("public final class Counter"));
321        assert!(src.contains("private int value;"));
322    }
323
324    #[test]
325    fn sequence_uses_list() {
326        let files = gen_java("struct Bag { sequence<long> items; };");
327        let src = &files[0].source;
328        assert!(src.contains("private java.util.List<Integer> items;"));
329    }
330
331    #[test]
332    fn array_uses_java_array_syntax() {
333        let files = gen_java("struct M { long cells[3][4]; };");
334        let src = &files[0].source;
335        assert!(src.contains("private int[][] cells;"));
336    }
337
338    #[test]
339    fn inherited_struct_uses_extends() {
340        let files = gen_java("struct Parent { long x; }; struct Child : Parent { long y; };");
341        let child = files
342            .iter()
343            .find(|f| f.class_name == "Child")
344            .expect("Child file");
345        assert!(child.source.contains("public class Child extends Parent"));
346    }
347
348    #[test]
349    fn keyed_member_emits_key_annotation() {
350        let files = gen_java("struct S { @key long id; long val; };");
351        assert!(files[0].source.contains("@org.zerodds.types.Key"));
352    }
353
354    #[test]
355    fn optional_member_uses_optional() {
356        let files = gen_java("struct S { @optional long maybe; };");
357        let src = &files[0].source;
358        assert!(src.contains("java.util.Optional<Integer> maybe"));
359    }
360
361    #[test]
362    fn exception_extends_runtime_exception() {
363        let files = gen_java("exception NotFound { string what_; };");
364        let src = &files[0].source;
365        assert!(src.contains("public class NotFound extends RuntimeException"));
366        assert!(src.contains("public NotFound(String message)"));
367    }
368
369    #[test]
370    fn reserved_member_name_gets_underscore_suffix() {
371        let files = gen_java("struct S { long class; };");
372        let src = &files[0].source;
373        assert!(src.contains("class_"));
374        assert!(src.contains("getClass_"));
375    }
376
377    #[test]
378    fn non_service_interface_emits_java_interface() {
379        let opts = JavaGenOptions {
380            emit_typesupport: false,
381            ..Default::default()
382        };
383        let ast = zerodds_idl::parse("interface I { void op(); };", &ParserConfig::default())
384            .expect("parse");
385        let files = generate_java_files(&ast, &opts).expect("ok");
386        let combined: String = files.iter().map(|f| f.source.clone()).collect();
387        assert!(combined.contains("public interface I"));
388    }
389
390    #[test]
391    fn any_member_emits_object() {
392        // `any`-Member ist ausserhalb des TypeSupport-Scopes (v1.0);
393        // die POJO emittiert weiterhin `Object`, TypeSupport entfaellt.
394        let ast = zerodds_idl::parse("struct S { any value; };", &ParserConfig::default())
395            .expect("parse");
396        let files = generate_java_files(&ast, &JavaGenOptions::default()).expect("ok");
397        let combined: String = files.iter().map(|f| f.source.clone()).collect();
398        assert!(combined.contains("Object"));
399        // Keine TypeSupport-Generation fuer `any`.
400        assert!(!combined.contains("STypeSupport"));
401    }
402
403    #[test]
404    fn root_package_prepends_to_modules() {
405        let opts = JavaGenOptions {
406            root_package: "org.example".into(),
407            ..Default::default()
408        };
409        let files = gen_with("module Inner { struct S { long x; }; };", &opts);
410        assert_eq!(files[0].package_path, "org.example.inner");
411    }
412
413    #[test]
414    fn relative_path_uses_package_directory() {
415        let files = gen_java("module M { struct S { long x; }; };");
416        assert_eq!(files[0].relative_path(), "m/S.java");
417    }
418
419    #[test]
420    fn relative_path_default_package() {
421        let files = gen_java("struct S { long x; };");
422        assert_eq!(files[0].relative_path(), "S.java");
423    }
424
425    #[test]
426    fn inheritance_cycle_is_rejected() {
427        let ast = zerodds_idl::parse(
428            "struct A : B { long a; };\n\
429             struct B : A { long b; };",
430            &ParserConfig::default(),
431        )
432        .expect("parse");
433        let res = generate_java_files(&ast, &JavaGenOptions::default());
434        assert!(matches!(res, Err(JavaGenError::InheritanceCycle { .. })));
435    }
436
437    #[test]
438    fn options_have_sensible_defaults() {
439        let o = JavaGenOptions::default();
440        assert_eq!(o.indent_width, 4);
441        assert!(o.root_package.is_empty());
442        assert!(!o.use_records);
443    }
444
445    #[test]
446    fn options_clone_works() {
447        let o = JavaGenOptions {
448            root_package: "foo.bar".into(),
449            indent_width: 2,
450            use_records: true,
451            emit_amqp_helpers: false,
452            emit_corba_traits: false,
453            emit_typesupport: true,
454        };
455        let cloned = o.clone();
456        assert_eq!(cloned.indent_width, 2);
457        assert_eq!(cloned.root_package, "foo.bar");
458        assert!(cloned.use_records);
459    }
460
461    #[test]
462    fn java_file_struct_field_access() {
463        let files = gen_java("struct S { long x; };");
464        assert_eq!(files[0].class_name, "S");
465        assert_eq!(files[0].package_path, "");
466        assert!(files[0].source.contains("public class S"));
467    }
468
469    #[test]
470    fn const_decl_emits_holder_class() {
471        let files = gen_java("const long MAX = 100;");
472        let src = &files[0].source;
473        assert!(src.contains("public final class MAXConstant"));
474        assert!(src.contains("public static final int MAX = 100;"));
475    }
476
477    #[test]
478    fn each_file_starts_with_generated_marker() {
479        for f in gen_java("struct S { long x; };") {
480            assert!(f.source.starts_with("// Generated by zerodds idl-java."));
481        }
482    }
483}