phon_codegen/targets/java/
mod.rs1use std::fmt::Write;
7
8use phon_schema::schema_bundle_to_bytes;
9
10use crate::Module;
11
12#[must_use]
13pub fn render_schema_table(module: &Module, package: &str, class_name: &str) -> String {
14 let mut out = format!(
15 "// @generated by phon-codegen; DO NOT EDIT.\npackage {package};\n\n\
16 public final class {class_name} {{\n"
17 );
18 for root in &module.roots {
19 let _ = writeln!(
20 out,
21 " public static final long {}_SCHEMA_ID = 0x{:016x}L;",
22 java_constant(&root.name),
23 root.id.as_u64()
24 );
25 }
26 let bytes = schema_bundle_to_bytes(&module.schemas).expect("validated codegen schema bundle");
27 out.push_str(" public static final byte[] SCHEMA_BUNDLE = schemaBundle();\n");
28 out.push_str(&render_byte_factory("schemaBundle", &bytes));
29 let _ = writeln!(out, " private {class_name}() {{}}");
30 out.push_str("}\n");
31 out
32}
33
34#[must_use]
38pub fn render_byte_factory(name: &str, bytes: &[u8]) -> String {
39 let mut out = format!(
40 " private static byte[] {name}() {{\n var out = new java.io.ByteArrayOutputStream({});\n",
41 bytes.len()
42 );
43 for chunk in bytes.chunks(16 * 1024) {
44 out.push_str(" out.writeBytes(java.util.HexFormat.of().parseHex(\"");
45 for byte in chunk {
46 let _ = write!(out, "{byte:02x}");
47 }
48 out.push_str("\"));\n");
49 }
50 out.push_str(" return out.toByteArray();\n }\n");
51 out
52}
53
54fn java_constant(name: &str) -> String {
55 name.chars()
56 .flat_map(|character| {
57 if character.is_ascii_uppercase() {
58 vec!['_', character]
59 } else if character.is_ascii_alphanumeric() {
60 vec![character.to_ascii_uppercase()]
61 } else {
62 vec!['_']
63 }
64 })
65 .collect::<String>()
66 .trim_start_matches('_')
67 .to_string()
68}
69
70#[cfg(test)]
71mod tests {
72 use facet::Facet;
73
74 use crate::Builder;
75
76 use super::render_schema_table;
77
78 #[derive(Facet)]
79 struct Message {
80 text: String,
81 }
82
83 #[test]
84 fn large_byte_factory_compiles_and_preserves_every_byte() {
85 let bytes: Vec<u8> = (0..262_147).map(|i| (i % 251) as u8).collect();
86 let factory = super::render_byte_factory("bundle", &bytes);
87 assert_eq!(factory, super::render_byte_factory("bundle", &bytes));
88 let directory =
89 std::env::temp_dir().join(format!("phon-java-bundle-{}", std::process::id()));
90 std::fs::create_dir_all(&directory).unwrap();
91 let source = format!(
92 "public class LargeBundle {{ {factory} public static void main(String[] args) throws Exception {{ java.nio.file.Files.write(java.nio.file.Path.of(args[0]), bundle()); }} }}"
93 );
94 std::fs::write(directory.join("LargeBundle.java"), source).unwrap();
95 let compile = std::process::Command::new("javac")
96 .args(["--release", "17"])
97 .arg(directory.join("LargeBundle.java"))
98 .output()
99 .expect("JDK required for Java generation qualification");
100 assert!(
101 compile.status.success(),
102 "{}",
103 String::from_utf8_lossy(&compile.stderr)
104 );
105 let output = directory.join("bundle.bin");
106 assert!(
107 std::process::Command::new("java")
108 .arg("-cp")
109 .arg(&directory)
110 .arg("LargeBundle")
111 .arg(&output)
112 .status()
113 .unwrap()
114 .success()
115 );
116 assert_eq!(std::fs::read(output).unwrap(), bytes);
117 std::fs::remove_dir_all(directory).unwrap();
118 }
119
120 #[test]
121 fn schema_table_is_deterministic_and_contains_canonical_root() {
122 let module = Builder::new()
123 .add::<Message>()
124 .build()
125 .expect("derive schema");
126 let first = render_schema_table(&module, "org.facet.phon.generated", "FixtureSchemas");
127 let second = render_schema_table(&module, "org.facet.phon.generated", "FixtureSchemas");
128 assert_eq!(first, second);
129 assert!(first.contains("MESSAGE_SCHEMA_ID = 0x"));
130 assert!(first.contains("HexFormat.of().parseHex"));
131 assert!(first.contains("private FixtureSchemas()"));
132 }
133}