use std::fmt::Write;
use phon_schema::schema_bundle_to_bytes;
use crate::Module;
#[must_use]
pub fn render_schema_table(module: &Module, package: &str, class_name: &str) -> String {
let mut out = format!(
"// @generated by phon-codegen; DO NOT EDIT.\npackage {package};\n\n\
public final class {class_name} {{\n"
);
for root in &module.roots {
let _ = writeln!(
out,
" public static final long {}_SCHEMA_ID = 0x{:016x}L;",
java_constant(&root.name),
root.id.as_u64()
);
}
let bytes = schema_bundle_to_bytes(&module.schemas).expect("validated codegen schema bundle");
out.push_str(" public static final byte[] SCHEMA_BUNDLE = schemaBundle();\n");
out.push_str(&render_byte_factory("schemaBundle", &bytes));
let _ = writeln!(out, " private {class_name}() {{}}");
out.push_str("}\n");
out
}
#[must_use]
pub fn render_byte_factory(name: &str, bytes: &[u8]) -> String {
let mut out = format!(
" private static byte[] {name}() {{\n var out = new java.io.ByteArrayOutputStream({});\n",
bytes.len()
);
for chunk in bytes.chunks(16 * 1024) {
out.push_str(" out.writeBytes(java.util.HexFormat.of().parseHex(\"");
for byte in chunk {
let _ = write!(out, "{byte:02x}");
}
out.push_str("\"));\n");
}
out.push_str(" return out.toByteArray();\n }\n");
out
}
fn java_constant(name: &str) -> String {
name.chars()
.flat_map(|character| {
if character.is_ascii_uppercase() {
vec!['_', character]
} else if character.is_ascii_alphanumeric() {
vec![character.to_ascii_uppercase()]
} else {
vec!['_']
}
})
.collect::<String>()
.trim_start_matches('_')
.to_string()
}
#[cfg(test)]
mod tests {
use facet::Facet;
use crate::Builder;
use super::render_schema_table;
#[derive(Facet)]
struct Message {
text: String,
}
#[test]
fn large_byte_factory_compiles_and_preserves_every_byte() {
let bytes: Vec<u8> = (0..262_147).map(|i| (i % 251) as u8).collect();
let factory = super::render_byte_factory("bundle", &bytes);
assert_eq!(factory, super::render_byte_factory("bundle", &bytes));
let directory =
std::env::temp_dir().join(format!("phon-java-bundle-{}", std::process::id()));
std::fs::create_dir_all(&directory).unwrap();
let source = format!(
"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()); }} }}"
);
std::fs::write(directory.join("LargeBundle.java"), source).unwrap();
let compile = std::process::Command::new("javac")
.args(["--release", "17"])
.arg(directory.join("LargeBundle.java"))
.output()
.expect("JDK required for Java generation qualification");
assert!(
compile.status.success(),
"{}",
String::from_utf8_lossy(&compile.stderr)
);
let output = directory.join("bundle.bin");
assert!(
std::process::Command::new("java")
.arg("-cp")
.arg(&directory)
.arg("LargeBundle")
.arg(&output)
.status()
.unwrap()
.success()
);
assert_eq!(std::fs::read(output).unwrap(), bytes);
std::fs::remove_dir_all(directory).unwrap();
}
#[test]
fn schema_table_is_deterministic_and_contains_canonical_root() {
let module = Builder::new()
.add::<Message>()
.build()
.expect("derive schema");
let first = render_schema_table(&module, "org.facet.phon.generated", "FixtureSchemas");
let second = render_schema_table(&module, "org.facet.phon.generated", "FixtureSchemas");
assert_eq!(first, second);
assert!(first.contains("MESSAGE_SCHEMA_ID = 0x"));
assert!(first.contains("HexFormat.of().parseHex"));
assert!(first.contains("private FixtureSchemas()"));
}
}