1#![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#[derive(Debug, Clone)]
96pub struct JavaGenOptions {
97 pub root_package: String,
100 pub indent_width: usize,
102 pub use_records: bool,
106 pub emit_amqp_helpers: bool,
112 pub emit_corba_traits: bool,
117 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
138pub 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
166pub 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
183pub 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 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 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 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 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 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}