#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub(crate) mod amqp;
pub(crate) mod annotations;
pub(crate) mod bitset;
pub(crate) mod corba_traits;
pub mod emitter;
pub mod error;
pub mod keywords;
pub mod rpc;
pub mod type_map;
pub(crate) mod typesupport;
pub(crate) mod verbatim;
pub use emitter::JavaFile;
pub use error::JavaGenError;
use zerodds_idl::ast::Specification;
#[derive(Debug, Clone)]
pub struct JavaGenOptions {
pub root_package: String,
pub indent_width: usize,
pub use_records: bool,
pub emit_amqp_helpers: bool,
pub emit_corba_traits: bool,
pub emit_typesupport: bool,
}
impl Default for JavaGenOptions {
fn default() -> Self {
Self {
root_package: String::new(),
indent_width: 4,
use_records: false,
emit_amqp_helpers: false,
emit_corba_traits: false,
emit_typesupport: true,
}
}
}
pub fn generate_java_files(
ast: &Specification,
opts: &JavaGenOptions,
) -> Result<Vec<JavaFile>, JavaGenError> {
let mut files = emitter::emit_files(ast, opts)?;
if opts.emit_amqp_helpers {
files.extend(amqp::emit_amqp_codec_files(ast, opts)?);
}
if opts.emit_corba_traits {
files.extend(corba_traits::emit_corba_traits_files(ast, opts)?);
}
if opts.emit_typesupport {
files.extend(typesupport::emit_typesupport_files(ast, opts)?);
}
Ok(files)
}
pub fn generate_java_files_with_corba_traits(
ast: &Specification,
opts: &JavaGenOptions,
) -> Result<Vec<JavaFile>, JavaGenError> {
let opts = JavaGenOptions {
emit_corba_traits: true,
..opts.clone()
};
generate_java_files(ast, &opts)
}
pub fn generate_java_files_with_amqp(
ast: &Specification,
opts: &JavaGenOptions,
) -> Result<Vec<JavaFile>, JavaGenError> {
let opts = JavaGenOptions {
emit_amqp_helpers: true,
..opts.clone()
};
generate_java_files(ast, &opts)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic)]
use super::*;
use zerodds_idl::config::ParserConfig;
fn gen_java(src: &str) -> Vec<JavaFile> {
let opts = JavaGenOptions {
emit_typesupport: false,
..Default::default()
};
let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
generate_java_files(&ast, &opts).expect("gen must succeed")
}
fn gen_with(src: &str, opts: &JavaGenOptions) -> Vec<JavaFile> {
let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
generate_java_files(&ast, opts).expect("gen must succeed")
}
#[test]
fn empty_source_emits_no_files() {
assert!(gen_java("").is_empty());
}
#[test]
fn empty_module_emits_no_files() {
assert!(gen_java("module M {};").is_empty());
}
#[test]
fn struct_emits_one_file_per_type() {
let files = gen_java("struct A { long x; }; struct B { long y; }; struct C { long z; };");
assert_eq!(files.len(), 3);
let names: Vec<&str> = files.iter().map(|f| f.class_name.as_str()).collect();
assert!(names.contains(&"A"));
assert!(names.contains(&"B"));
assert!(names.contains(&"C"));
}
#[test]
fn module_becomes_lowercase_package() {
let files = gen_java("module Foo { struct S { long x; }; };");
assert_eq!(files.len(), 1);
assert_eq!(files[0].package_path, "foo");
assert!(files[0].source.contains("package foo;"));
}
#[test]
fn three_level_modules_become_three_packages() {
let files = gen_java("module A { module B { module C { struct S { long x; }; }; }; };");
assert_eq!(files.len(), 1);
assert_eq!(files[0].package_path, "a.b.c");
assert!(files[0].source.contains("package a.b.c;"));
}
#[test]
fn primitive_struct_uses_correct_java_types() {
let files = gen_java(
"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; char c; wchar wc; string str; };",
);
let src = &files[0].source;
assert!(src.contains("private boolean b;"));
assert!(src.contains("private byte o;"));
assert!(src.contains("private short s;"));
assert!(src.contains("private int l;"));
assert!(src.contains("private long ll;"));
assert!(src.contains("private int us;"));
assert!(src.contains("private long ul;"));
assert!(src.contains("private long ull;"));
assert!(src.contains("private float f;"));
assert!(src.contains("private double d;"));
assert!(src.contains("private char c;"));
assert!(src.contains("private char wc;"));
assert!(src.contains("private String str;"));
}
#[test]
fn unsigned_member_gets_doc_comment() {
let files = gen_java("struct S { unsigned long u; };");
assert!(files[0].source.contains("unsigned IDL value"));
}
#[test]
fn enum_emits_explicit_values() {
let files = gen_java("enum Color { RED, GREEN, BLUE };");
let src = &files[0].source;
assert!(src.contains("public enum Color {"));
assert!(src.contains("RED(0),"));
assert!(src.contains("GREEN(1),"));
assert!(src.contains("BLUE(2);"));
assert!(src.contains("public int value()"));
}
#[test]
fn union_emits_sealed_interface() {
let files = gen_java(
"union U switch (long) { case 1: long a; case 2: double b; default: octet c; };",
);
let src = &files[0].source;
assert!(src.contains("public sealed interface U"));
assert!(src.contains("permits"));
assert!(src.contains("record A(int a) implements U"));
assert!(src.contains("record B(double b) implements U"));
assert!(src.contains("// case default"));
}
#[test]
fn typedef_emits_wrapper_class() {
let files = gen_java("typedef long Counter;");
assert_eq!(files.len(), 1);
let src = &files[0].source;
assert!(src.contains("public final class Counter"));
assert!(src.contains("private int value;"));
}
#[test]
fn sequence_uses_list() {
let files = gen_java("struct Bag { sequence<long> items; };");
let src = &files[0].source;
assert!(src.contains("private java.util.List<Integer> items;"));
}
#[test]
fn array_uses_java_array_syntax() {
let files = gen_java("struct M { long cells[3][4]; };");
let src = &files[0].source;
assert!(src.contains("private int[][] cells;"));
}
#[test]
fn inherited_struct_uses_extends() {
let files = gen_java("struct Parent { long x; }; struct Child : Parent { long y; };");
let child = files
.iter()
.find(|f| f.class_name == "Child")
.expect("Child file");
assert!(child.source.contains("public class Child extends Parent"));
}
#[test]
fn keyed_member_emits_key_annotation() {
let files = gen_java("struct S { @key long id; long val; };");
assert!(files[0].source.contains("@org.zerodds.types.Key"));
}
#[test]
fn optional_member_uses_optional() {
let files = gen_java("struct S { @optional long maybe; };");
let src = &files[0].source;
assert!(src.contains("java.util.Optional<Integer> maybe"));
}
#[test]
fn exception_extends_runtime_exception() {
let files = gen_java("exception NotFound { string what_; };");
let src = &files[0].source;
assert!(src.contains("public class NotFound extends RuntimeException"));
assert!(src.contains("public NotFound(String message)"));
}
#[test]
fn reserved_member_name_gets_underscore_suffix() {
let files = gen_java("struct S { long class; };");
let src = &files[0].source;
assert!(src.contains("class_"));
assert!(src.contains("getClass_"));
}
#[test]
fn non_service_interface_emits_java_interface() {
let opts = JavaGenOptions {
emit_typesupport: false,
..Default::default()
};
let ast = zerodds_idl::parse("interface I { void op(); };", &ParserConfig::default())
.expect("parse");
let files = generate_java_files(&ast, &opts).expect("ok");
let combined: String = files.iter().map(|f| f.source.clone()).collect();
assert!(combined.contains("public interface I"));
}
#[test]
fn any_member_emits_object() {
let ast = zerodds_idl::parse("struct S { any value; };", &ParserConfig::default())
.expect("parse");
let files = generate_java_files(&ast, &JavaGenOptions::default()).expect("ok");
let combined: String = files.iter().map(|f| f.source.clone()).collect();
assert!(combined.contains("Object"));
assert!(!combined.contains("STypeSupport"));
}
#[test]
fn root_package_prepends_to_modules() {
let opts = JavaGenOptions {
root_package: "org.example".into(),
..Default::default()
};
let files = gen_with("module Inner { struct S { long x; }; };", &opts);
assert_eq!(files[0].package_path, "org.example.inner");
}
#[test]
fn relative_path_uses_package_directory() {
let files = gen_java("module M { struct S { long x; }; };");
assert_eq!(files[0].relative_path(), "m/S.java");
}
#[test]
fn relative_path_default_package() {
let files = gen_java("struct S { long x; };");
assert_eq!(files[0].relative_path(), "S.java");
}
#[test]
fn inheritance_cycle_is_rejected() {
let ast = zerodds_idl::parse(
"struct A : B { long a; };\n\
struct B : A { long b; };",
&ParserConfig::default(),
)
.expect("parse");
let res = generate_java_files(&ast, &JavaGenOptions::default());
assert!(matches!(res, Err(JavaGenError::InheritanceCycle { .. })));
}
#[test]
fn options_have_sensible_defaults() {
let o = JavaGenOptions::default();
assert_eq!(o.indent_width, 4);
assert!(o.root_package.is_empty());
assert!(!o.use_records);
}
#[test]
fn options_clone_works() {
let o = JavaGenOptions {
root_package: "foo.bar".into(),
indent_width: 2,
use_records: true,
emit_amqp_helpers: false,
emit_corba_traits: false,
emit_typesupport: true,
};
let cloned = o.clone();
assert_eq!(cloned.indent_width, 2);
assert_eq!(cloned.root_package, "foo.bar");
assert!(cloned.use_records);
}
#[test]
fn java_file_struct_field_access() {
let files = gen_java("struct S { long x; };");
assert_eq!(files[0].class_name, "S");
assert_eq!(files[0].package_path, "");
assert!(files[0].source.contains("public class S"));
}
#[test]
fn const_decl_emits_holder_class() {
let files = gen_java("const long MAX = 100;");
let src = &files[0].source;
assert!(src.contains("public final class MAXConstant"));
assert!(src.contains("public static final int MAX = 100;"));
}
#[test]
fn each_file_starts_with_generated_marker() {
for f in gen_java("struct S { long x; };") {
assert!(f.source.starts_with("// Generated by zerodds idl-java."));
}
}
}