1#![forbid(unsafe_code)]
85#![warn(missing_docs)]
86
87pub(crate) mod amqp;
88pub(crate) mod annotations;
89pub(crate) mod bitset;
90pub(crate) mod corba_traits;
91pub mod emitter;
92pub mod error;
93pub mod keywords;
94pub mod rpc;
95pub mod type_map;
96pub(crate) mod typesupport;
97pub(crate) mod verbatim;
98
99pub use emitter::JavaFile;
100pub use error::JavaGenError;
101
102use zerodds_idl::ast::Specification;
103
104#[derive(Debug, Clone)]
106pub struct JavaGenOptions {
107 pub root_package: String,
110 pub indent_width: usize,
112 pub use_records: bool,
115 pub emit_amqp_helpers: bool,
121 pub emit_corba_traits: bool,
126 pub emit_typesupport: bool,
132 pub java8_compat: bool,
139}
140
141impl Default for JavaGenOptions {
142 fn default() -> Self {
143 Self {
144 root_package: String::new(),
145 indent_width: 4,
146 use_records: false,
147 emit_amqp_helpers: false,
148 emit_corba_traits: false,
149 emit_typesupport: true,
150 java8_compat: false,
151 }
152 }
153}
154
155pub fn generate_java_files(
167 ast: &Specification,
168 opts: &JavaGenOptions,
169) -> Result<Vec<JavaFile>, JavaGenError> {
170 let mut files = emitter::emit_files(ast, opts)?;
171 if opts.emit_amqp_helpers {
172 files.extend(amqp::emit_amqp_codec_files(ast, opts)?);
173 }
174 if opts.emit_corba_traits {
175 files.extend(corba_traits::emit_corba_traits_files(ast, opts)?);
176 }
177 if opts.emit_typesupport {
178 files.extend(typesupport::emit_typesupport_files(ast, opts)?);
179 }
180 Ok(files)
181}
182
183pub fn generate_java_files_with_corba_traits(
190 ast: &Specification,
191 opts: &JavaGenOptions,
192) -> Result<Vec<JavaFile>, JavaGenError> {
193 let opts = JavaGenOptions {
194 emit_corba_traits: true,
195 ..opts.clone()
196 };
197 generate_java_files(ast, &opts)
198}
199
200pub fn generate_java_files_with_amqp(
208 ast: &Specification,
209 opts: &JavaGenOptions,
210) -> Result<Vec<JavaFile>, JavaGenError> {
211 let opts = JavaGenOptions {
212 emit_amqp_helpers: true,
213 ..opts.clone()
214 };
215 generate_java_files(ast, &opts)
216}
217
218#[cfg(test)]
219mod tests {
220 #![allow(clippy::expect_used, clippy::panic)]
221 use super::*;
222 use zerodds_idl::config::ParserConfig;
223
224 fn gen_java(src: &str) -> Vec<JavaFile> {
225 let opts = JavaGenOptions {
228 emit_typesupport: false,
229 ..Default::default()
230 };
231 let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
232 generate_java_files(&ast, &opts).expect("gen must succeed")
233 }
234
235 fn gen_with(src: &str, opts: &JavaGenOptions) -> Vec<JavaFile> {
236 let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
237 generate_java_files(&ast, opts).expect("gen must succeed")
238 }
239
240 #[test]
241 fn empty_source_emits_no_files() {
242 assert!(gen_java("").is_empty());
243 }
244
245 #[test]
246 fn empty_module_emits_no_files() {
247 assert!(gen_java("module M {};").is_empty());
250 }
251
252 #[test]
253 fn struct_emits_one_file_per_type() {
254 let files = gen_java("struct A { long x; }; struct B { long y; }; struct C { long z; };");
255 assert_eq!(files.len(), 3);
256 let names: Vec<&str> = files.iter().map(|f| f.class_name.as_str()).collect();
257 assert!(names.contains(&"A"));
258 assert!(names.contains(&"B"));
259 assert!(names.contains(&"C"));
260 }
261
262 #[test]
263 fn module_becomes_lowercase_package() {
264 let files = gen_java("module Foo { struct S { long x; }; };");
265 assert_eq!(files.len(), 1);
266 assert_eq!(files[0].package_path, "foo");
267 assert!(files[0].source.contains("package foo;"));
268 }
269
270 #[test]
271 fn three_level_modules_become_three_packages() {
272 let files = gen_java("module A { module B { module C { struct S { long x; }; }; }; };");
273 assert_eq!(files.len(), 1);
274 assert_eq!(files[0].package_path, "a.b.c");
275 assert!(files[0].source.contains("package a.b.c;"));
276 }
277
278 #[test]
279 fn primitive_struct_uses_correct_java_types() {
280 let files = gen_java(
281 "struct S { boolean b; octet o; short s; long l; long long ll; \
282 unsigned short us; unsigned long ul; unsigned long long ull; \
283 float f; double d; char c; wchar wc; string str; };",
284 );
285 let src = &files[0].source;
286 assert!(src.contains("private boolean b;"));
287 assert!(src.contains("private byte o;"));
288 assert!(src.contains("private short s;"));
289 assert!(src.contains("private int l;"));
290 assert!(src.contains("private long ll;"));
291 assert!(src.contains("private int us;"));
293 assert!(src.contains("private long ul;"));
294 assert!(src.contains("private long ull;"));
295 assert!(src.contains("private float f;"));
296 assert!(src.contains("private double d;"));
297 assert!(src.contains("private char c;"));
298 assert!(src.contains("private char wc;"));
299 assert!(src.contains("private String str;"));
300 }
301
302 #[test]
303 fn unsigned_member_gets_doc_comment() {
304 let files = gen_java("struct S { unsigned long u; };");
305 assert!(files[0].source.contains("unsigned IDL value"));
306 }
307
308 #[test]
309 fn enum_emits_explicit_values() {
310 let files = gen_java("enum Color { RED, GREEN, BLUE };");
311 let src = &files[0].source;
312 assert!(src.contains("public enum Color {"));
313 assert!(src.contains("RED(0),"));
314 assert!(src.contains("GREEN(1),"));
315 assert!(src.contains("BLUE(2);"));
316 assert!(src.contains("public int value()"));
317 }
318
319 #[test]
320 fn union_emits_sealed_interface() {
321 let files = gen_java(
322 "union U switch (long) { case 1: long a; case 2: double b; default: octet c; };",
323 );
324 let src = &files[0].source;
325 assert!(src.contains("public sealed interface U"));
326 assert!(src.contains("permits"));
327 assert!(src.contains("record A(int a) implements U"));
328 assert!(src.contains("record B(double b) implements U"));
329 assert!(src.contains("// case default"));
330 }
331
332 #[test]
333 fn typedef_emits_wrapper_class() {
334 let files = gen_java("typedef long Counter;");
335 assert_eq!(files.len(), 1);
336 let src = &files[0].source;
337 assert!(src.contains("public final class Counter"));
338 assert!(src.contains("private int value;"));
339 }
340
341 #[test]
342 fn sequence_uses_list() {
343 let files = gen_java("struct Bag { sequence<long> items; };");
344 let src = &files[0].source;
345 assert!(src.contains("private java.util.List<Integer> items;"));
346 }
347
348 #[test]
349 fn array_uses_java_array_syntax() {
350 let files = gen_java("struct M { long cells[3][4]; };");
351 let src = &files[0].source;
352 assert!(src.contains("private int[][] cells;"));
353 }
354
355 #[test]
356 fn inherited_struct_uses_extends() {
357 let files = gen_java("struct Parent { long x; }; struct Child : Parent { long y; };");
358 let child = files
359 .iter()
360 .find(|f| f.class_name == "Child")
361 .expect("Child file");
362 assert!(child.source.contains("public class Child extends Parent"));
363 }
364
365 #[test]
366 fn keyed_member_emits_key_annotation() {
367 let files = gen_java("struct S { @key long id; long val; };");
368 assert!(files[0].source.contains("@org.zerodds.types.Key"));
369 }
370
371 #[test]
372 fn optional_member_uses_optional() {
373 let files = gen_java("struct S { @optional long maybe; };");
374 let src = &files[0].source;
375 assert!(src.contains("java.util.Optional<Integer> maybe"));
376 }
377
378 #[test]
379 fn exception_extends_runtime_exception() {
380 let files = gen_java("exception NotFound { string what_; };");
381 let src = &files[0].source;
382 assert!(src.contains("public class NotFound extends RuntimeException"));
383 assert!(src.contains("public NotFound(String message)"));
384 }
385
386 #[test]
387 fn reserved_member_name_gets_underscore_suffix() {
388 let files = gen_java("struct S { long class; };");
389 let src = &files[0].source;
390 assert!(src.contains("class_"));
391 assert!(src.contains("getClass_"));
392 }
393
394 #[test]
395 fn non_service_interface_emits_java_interface() {
396 let opts = JavaGenOptions {
397 emit_typesupport: false,
398 ..Default::default()
399 };
400 let ast = zerodds_idl::parse("interface I { void op(); };", &ParserConfig::default())
401 .expect("parse");
402 let files = generate_java_files(&ast, &opts).expect("ok");
403 let combined: String = files.iter().map(|f| f.source.clone()).collect();
404 assert!(combined.contains("public interface I"));
405 }
406
407 #[test]
408 fn any_member_emits_object() {
409 let ast = zerodds_idl::parse("struct S { any value; };", &ParserConfig::default())
412 .expect("parse");
413 let files = generate_java_files(&ast, &JavaGenOptions::default()).expect("ok");
414 let combined: String = files.iter().map(|f| f.source.clone()).collect();
415 assert!(combined.contains("Object"));
416 assert!(!combined.contains("STypeSupport"));
418 }
419
420 #[test]
421 fn root_package_prepends_to_modules() {
422 let opts = JavaGenOptions {
423 root_package: "org.example".into(),
424 ..Default::default()
425 };
426 let files = gen_with("module Inner { struct S { long x; }; };", &opts);
427 assert_eq!(files[0].package_path, "org.example.inner");
428 }
429
430 #[test]
431 fn relative_path_uses_package_directory() {
432 let files = gen_java("module M { struct S { long x; }; };");
433 assert_eq!(files[0].relative_path(), "m/S.java");
434 }
435
436 #[test]
437 fn relative_path_default_package() {
438 let files = gen_java("struct S { long x; };");
439 assert_eq!(files[0].relative_path(), "S.java");
440 }
441
442 #[test]
443 fn inheritance_cycle_is_rejected() {
444 let ast = zerodds_idl::parse(
445 "struct A : B { long a; };\n\
446 struct B : A { long b; };",
447 &ParserConfig::default(),
448 )
449 .expect("parse");
450 let res = generate_java_files(&ast, &JavaGenOptions::default());
451 assert!(matches!(res, Err(JavaGenError::InheritanceCycle { .. })));
452 }
453
454 #[test]
455 fn options_have_sensible_defaults() {
456 let o = JavaGenOptions::default();
457 assert_eq!(o.indent_width, 4);
458 assert!(o.root_package.is_empty());
459 assert!(!o.use_records);
460 }
461
462 #[test]
463 fn options_clone_works() {
464 let o = JavaGenOptions {
465 root_package: "foo.bar".into(),
466 indent_width: 2,
467 use_records: true,
468 emit_amqp_helpers: false,
469 emit_corba_traits: false,
470 emit_typesupport: true,
471 java8_compat: false,
472 };
473 let cloned = o.clone();
474 assert_eq!(cloned.indent_width, 2);
475 assert_eq!(cloned.root_package, "foo.bar");
476 assert!(cloned.use_records);
477 }
478
479 #[test]
480 fn java_file_struct_field_access() {
481 let files = gen_java("struct S { long x; };");
482 assert_eq!(files[0].class_name, "S");
483 assert_eq!(files[0].package_path, "");
484 assert!(files[0].source.contains("public class S"));
485 }
486
487 #[test]
488 fn const_decl_emits_holder_class() {
489 let files = gen_java("const long MAX = 100;");
490 let src = &files[0].source;
491 assert!(src.contains("public final class MAXConstant"));
492 assert!(src.contains("public static final int MAX = 100;"));
493 }
494
495 #[test]
496 fn each_file_starts_with_generated_marker() {
497 for f in gen_java("struct S { long x; };") {
498 assert!(f.source.starts_with("// Generated by zerodds idl-java."));
499 }
500 }
501}