1use std::collections::HashSet;
4use std::fmt::{self, Write};
5
6use facet_core::{ScalarType, Shape};
7use phon_schema::schema_bundle_to_bytes;
8use vox_types::{
9 EnumInfo, MethodDescriptor, ServiceDescriptor, ShapeKind, StructInfo, VariantKind,
10 classify_shape, classify_variant, is_bytes,
11};
12
13#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct JavaFile {
16 pub relative_path: String,
17 pub source: String,
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct JavaCodegenError {
24 pub service: String,
25 pub method: String,
26 pub detail: String,
27}
28
29impl fmt::Display for JavaCodegenError {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 write!(
32 f,
33 "Java codegen does not support {}.{}: {}",
34 self.service, self.method, self.detail
35 )
36 }
37}
38
39impl std::error::Error for JavaCodegenError {}
40
41pub fn generate_service(service: &ServiceDescriptor) -> Result<Vec<JavaFile>, JavaCodegenError> {
46 validate_service(service)?;
47 let package = "org.facet.vox.generated";
48 let service_name = java_type_name(service.service_name);
49 let named = collect_named_types(service);
50 let mut files = vec![JavaFile {
51 relative_path: "PrimitiveAdapters.java".to_string(),
52 source: generate_primitive_adapters(package),
53 }];
54
55 for (name, shape) in named {
56 files.push(JavaFile {
57 relative_path: format!("{name}.java"),
58 source: generate_named_type(package, &name, shape),
59 });
60 }
61 for method in service.methods {
62 let args_name = args_type_name(&service_name, method);
63 files.push(JavaFile {
64 relative_path: format!("{args_name}.java"),
65 source: generate_args_type(package, &args_name, method),
66 });
67 let response_name = response_type_name(&service_name, method);
68 files.push(JavaFile {
69 relative_path: format!("{response_name}.java"),
70 source: generate_response_type(package, &response_name, method)?,
71 });
72 }
73 files.push(JavaFile {
74 relative_path: format!("{service_name}ServiceDescriptor.java"),
75 source: generate_descriptor(package, &service_name, service),
76 });
77 files.push(JavaFile {
78 relative_path: format!("{service_name}Handler.java"),
79 source: generate_handler(package, &service_name, service),
80 });
81 files.push(JavaFile {
82 relative_path: format!("{service_name}Dispatcher.java"),
83 source: generate_dispatcher(package, &service_name, service),
84 });
85 files.push(JavaFile {
86 relative_path: format!("{service_name}Client.java"),
87 source: generate_client(package, &service_name, service),
88 });
89 files.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
90 Ok(files)
91}
92
93pub fn generate_wire_schemas() -> Result<Vec<JavaFile>, phon::derive::DeriveError> {
98 use facet::Facet;
99 use phon_codegen::Module;
100 use phon_codegen::targets::java::render_schema_table;
101 use vox_types as wire;
102
103 let message = Module::from_shapes(&[<wire::Message<'static> as Facet<'static>>::SHAPE])?;
104 let handshake = Module::from_shapes(&[<wire::HandshakeMessage as Facet<'static>>::SHAPE])?;
105 Ok(vec![
106 JavaFile {
107 relative_path: "HandshakeWireSchemas.java".to_string(),
108 source: render_schema_table(
109 &handshake,
110 "org.facet.vox.generated",
111 "HandshakeWireSchemas",
112 ),
113 },
114 JavaFile {
115 relative_path: "MessageWireSchemas.java".to_string(),
116 source: render_schema_table(&message, "org.facet.vox.generated", "MessageWireSchemas"),
117 },
118 ])
119}
120
121pub fn generate_schema_table(
123 shape: &'static Shape,
124 class_name: &str,
125) -> Result<JavaFile, phon::derive::DeriveError> {
126 let module = phon_codegen::Module::from_shapes(&[shape])?;
127 Ok(JavaFile {
128 relative_path: format!("{class_name}.java"),
129 source: phon_codegen::targets::java::render_schema_table(
130 &module,
131 "org.facet.vox.generated",
132 class_name,
133 ),
134 })
135}
136
137fn header(package: &str) -> String {
138 format!(
139 "// @generated by vox-codegen; DO NOT EDIT.\n\
140 // Regenerate with `cargo xtask codegen --java`.\n\
141 package {package};\n\n"
142 )
143}
144
145fn validate_service(service: &ServiceDescriptor) -> Result<(), JavaCodegenError> {
146 for method in service.methods {
147 if vox_types::shape_contains_channel(method.return_shape) {
148 return Err(error(method, "channels may not appear in return types"));
149 }
150 for arg in method.args {
151 match channel_arg_direction(arg) {
152 Some(_) => validate_shape(
153 method,
154 arg.channel_element.expect("channel element"),
155 "channel element",
156 )?,
157 None => validate_shape(method, arg.shape, "argument")?,
158 }
159 }
160 validate_shape(method, method.return_shape, "return type")?;
161 }
162 Ok(())
163}
164
165fn validate_shape(
166 method: &MethodDescriptor,
167 shape: &'static Shape,
168 location: &str,
169) -> Result<(), JavaCodegenError> {
170 if is_bytes(shape) {
171 return Ok(());
172 }
173 if matches!(shape.def, facet_core::Def::DynamicValue(_)) {
174 return Err(error(method, &format!("dynamic Facet value in {location}")));
175 }
176 #[cfg(unix)]
177 if vox_types::is_fd(shape) {
178 return Err(error(
179 method,
180 &format!("file descriptor capability in {location}"),
181 ));
182 }
183 match classify_shape(shape) {
184 ShapeKind::Opaque => Err(error(
185 method,
186 &format!(
187 "opaque or external shape `{}` in {location}",
188 shape.type_identifier
189 ),
190 )),
191 ShapeKind::Tx { .. } | ShapeKind::Rx { .. } => {
192 Err(error(method, &format!("channel in {location}")))
193 }
194 ShapeKind::List { element } | ShapeKind::Slice { element } => {
195 validate_shape(method, element, location)
196 }
197 ShapeKind::Set { .. }
198 | ShapeKind::Array { .. }
199 | ShapeKind::Option { .. }
200 | ShapeKind::Map { .. } => Err(error(
201 method,
202 &format!(
203 "container shape `{}` in {location} is not implemented yet",
204 shape.type_identifier
205 ),
206 )),
207 ShapeKind::Pointer { pointee } => validate_shape(method, pointee, location),
208 ShapeKind::Result {
209 ok: key,
210 err: value,
211 } => {
212 validate_shape(method, key, location)?;
213 validate_shape(method, value, location)
214 }
215 ShapeKind::Tuple { elements } => {
216 for element in elements {
217 validate_shape(method, element.shape, location)?;
218 }
219 Ok(())
220 }
221 ShapeKind::TupleStruct { fields } | ShapeKind::Struct(StructInfo { fields, .. }) => {
222 for field in fields {
223 validate_shape(method, field.shape(), location)?;
224 }
225 Ok(())
226 }
227 ShapeKind::Enum(EnumInfo { variants, .. }) => {
228 for variant in variants {
229 match classify_variant(variant) {
230 VariantKind::Unit => {}
231 VariantKind::Newtype { .. }
232 | VariantKind::Tuple { .. }
233 | VariantKind::Struct { .. } => {
234 return Err(error(
235 method,
236 &format!(
237 "payload enum `{}` in {location} is not implemented yet",
238 shape.type_identifier
239 ),
240 ));
241 }
242 }
243 }
244 Ok(())
245 }
246 ShapeKind::Scalar(
247 ScalarType::Bool
248 | ScalarType::U8
249 | ScalarType::U16
250 | ScalarType::U32
251 | ScalarType::I8
252 | ScalarType::I16
253 | ScalarType::I32
254 | ScalarType::I64
255 | ScalarType::F32
256 | ScalarType::F64
257 | ScalarType::Str
258 | ScalarType::String
259 | ScalarType::CowStr
260 | ScalarType::Unit,
261 ) => Ok(()),
262 ShapeKind::Scalar(_) => Err(error(
263 method,
264 &format!(
265 "scalar shape `{}` in {location} is not implemented yet",
266 shape.type_identifier
267 ),
268 )),
269 }
270}
271
272fn error(method: &MethodDescriptor, detail: &str) -> JavaCodegenError {
273 JavaCodegenError {
274 service: method.service_name.to_string(),
275 method: method.method_name.to_string(),
276 detail: detail.to_string(),
277 }
278}
279
280#[derive(Clone, Copy)]
281enum JavaChannelDirection {
282 Tx,
283 Rx,
284}
285
286fn channel_arg_direction(arg: &vox_types::ArgDescriptor) -> Option<JavaChannelDirection> {
287 arg.channel_element?;
288 match classify_shape(arg.shape) {
289 ShapeKind::Tx { .. } => Some(JavaChannelDirection::Tx),
290 ShapeKind::Rx { .. } => Some(JavaChannelDirection::Rx),
291 _ => match arg.shape.type_identifier.rsplit("::").next() {
292 Some("Tx") => Some(JavaChannelDirection::Tx),
293 Some("Rx") => Some(JavaChannelDirection::Rx),
294 _ => None,
295 },
296 }
297}
298
299fn java_arg_type(arg: &vox_types::ArgDescriptor) -> String {
300 match channel_arg_direction(arg) {
301 Some(JavaChannelDirection::Tx) => format!(
302 "VoxTx<{}>",
303 boxed_java_type(arg.channel_element.expect("channel element"))
304 ),
305 Some(JavaChannelDirection::Rx) => format!(
306 "VoxRx<{}>",
307 boxed_java_type(arg.channel_element.expect("channel element"))
308 ),
309 None => java_type(arg.shape, false),
310 }
311}
312
313fn collect_named_types(service: &ServiceDescriptor) -> Vec<(String, &'static Shape)> {
314 let mut seen = HashSet::new();
315 let mut result = Vec::new();
316 for method in service.methods {
317 visit_named(method.args_shape, &mut seen, &mut result);
318 visit_named(method.return_shape, &mut seen, &mut result);
319 for arg in method.args {
320 if let Some(element) = arg.channel_element {
321 visit_named(element, &mut seen, &mut result);
322 }
323 }
324 }
325 result.sort_by(|a, b| a.0.cmp(&b.0));
326 result
327}
328
329fn visit_named(
330 shape: &'static Shape,
331 seen: &mut HashSet<String>,
332 result: &mut Vec<(String, &'static Shape)>,
333) {
334 match classify_shape(shape) {
335 ShapeKind::Struct(StructInfo {
336 name: Some(name),
337 fields,
338 ..
339 }) => {
340 for field in fields {
341 visit_named(field.shape(), seen, result);
342 }
343 if seen.insert(name.to_string()) {
344 result.push((java_type_name(name), shape));
345 }
346 }
347 ShapeKind::Enum(EnumInfo {
348 name: Some(name),
349 variants,
350 }) => {
351 for variant in variants {
352 match classify_variant(variant) {
353 VariantKind::Unit => {}
354 VariantKind::Newtype { inner } => visit_named(inner, seen, result),
355 VariantKind::Tuple { fields } | VariantKind::Struct { fields } => {
356 for field in fields {
357 visit_named(field.shape(), seen, result);
358 }
359 }
360 }
361 }
362 if seen.insert(name.to_string()) {
363 result.push((java_type_name(name), shape));
364 }
365 }
366 ShapeKind::List { element }
367 | ShapeKind::Slice { element }
368 | ShapeKind::Set { element }
369 | ShapeKind::Array { element, .. }
370 | ShapeKind::Option { inner: element }
371 | ShapeKind::Pointer { pointee: element } => visit_named(element, seen, result),
372 ShapeKind::Map { key, value }
373 | ShapeKind::Result {
374 ok: key,
375 err: value,
376 } => {
377 visit_named(key, seen, result);
378 visit_named(value, seen, result);
379 }
380 ShapeKind::Tuple { elements } => {
381 for element in elements {
382 visit_named(element.shape, seen, result);
383 }
384 }
385 ShapeKind::TupleStruct { fields } => {
386 for field in fields {
387 visit_named(field.shape(), seen, result);
388 }
389 }
390 ShapeKind::Struct(StructInfo {
391 name: None, fields, ..
392 }) => {
393 for field in fields {
394 visit_named(field.shape(), seen, result);
395 }
396 }
397 ShapeKind::Enum(EnumInfo {
398 name: None,
399 variants,
400 }) => {
401 for variant in variants {
402 match classify_variant(variant) {
403 VariantKind::Unit => {}
404 VariantKind::Newtype { inner } => visit_named(inner, seen, result),
405 VariantKind::Tuple { fields } | VariantKind::Struct { fields } => {
406 for field in fields {
407 visit_named(field.shape(), seen, result);
408 }
409 }
410 }
411 }
412 }
413 ShapeKind::Tx { inner } | ShapeKind::Rx { inner } => visit_named(inner, seen, result),
414 ShapeKind::Scalar(_) | ShapeKind::Opaque => {}
415 }
416}
417
418fn generate_named_type(package: &str, name: &str, shape: &'static Shape) -> String {
419 match classify_shape(shape) {
420 ShapeKind::Struct(StructInfo { fields, .. }) => {
421 let mut out = header(package);
422 out.push_str("import java.util.List;\nimport java.util.Objects;\n");
423 out.push_str("import org.facet.phon.*;\n\n");
424 let _ = writeln!(out, "public final class {name} {{");
425 for field in fields {
426 let _ = writeln!(
427 out,
428 " private final {} {};",
429 java_type(field.shape(), false),
430 java_ident(field.name)
431 );
432 }
433 out.push('\n');
434 let args = fields
435 .iter()
436 .map(|f| format!("{} {}", java_type(f.shape(), false), java_ident(f.name)))
437 .collect::<Vec<_>>()
438 .join(", ");
439 let _ = writeln!(out, " public {name}({args}) {{");
440 for field in fields {
441 let field_name = java_ident(field.name);
442 if is_list(field.shape()) {
443 let _ = writeln!(
444 out,
445 " this.{field_name} = List.copyOf(Objects.requireNonNull({field_name}, \"{field_name}\"));"
446 );
447 } else if is_reference(field.shape()) && !is_option(field.shape()) {
448 let _ = writeln!(
449 out,
450 " this.{field_name} = Objects.requireNonNull({field_name}, \"{field_name}\");"
451 );
452 } else {
453 let _ = writeln!(out, " this.{field_name} = {field_name};");
454 }
455 }
456 out.push_str(" }\n\n");
457 let required_fields = fields
458 .iter()
459 .filter(|field| field.default.is_none())
460 .collect::<Vec<_>>();
461 if required_fields.len() != fields.len() {
462 let args = required_fields
463 .iter()
464 .map(|field| {
465 format!(
466 "{} {}",
467 java_type(field.shape(), false),
468 java_ident(field.name)
469 )
470 })
471 .collect::<Vec<_>>()
472 .join(", ");
473 let values = fields
474 .iter()
475 .map(|field| {
476 if field.default.is_some() {
477 java_default_expression(field.shape())
478 } else {
479 java_ident(field.name).to_string()
480 }
481 })
482 .collect::<Vec<_>>()
483 .join(", ");
484 let _ = writeln!(
485 out,
486 " /** Compatibility constructor omitting fields with a Rust default. */\n public {name}({args}) {{ this({values}); }}\n\n"
487 );
488 }
489 for field in fields {
490 let field_name = java_ident(field.name);
491 let _ = writeln!(
492 out,
493 " public {} {}() {{ return {}; }}",
494 java_type(field.shape(), false),
495 field_name,
496 field_name
497 );
498 }
499 out.push('\n');
500 emit_record_schema_and_adapter(&mut out, name, shape, fields);
501 out.push_str("\n @Override public boolean equals(Object other) {\n");
502 let _ = writeln!(
503 out,
504 " if (!(other instanceof {name} that)) return false;"
505 );
506 let comparisons = fields
507 .iter()
508 .map(|f| {
509 let n = java_ident(f.name);
510 format!("Objects.deepEquals({n}, that.{n})")
511 })
512 .collect::<Vec<_>>()
513 .join(" && ");
514 let _ = writeln!(
515 out,
516 " return {};",
517 if comparisons.is_empty() {
518 "true"
519 } else {
520 &comparisons
521 }
522 );
523 out.push_str(" }\n");
524 let names = fields
525 .iter()
526 .map(|f| java_ident(f.name))
527 .collect::<Vec<_>>()
528 .join(", ");
529 let _ = writeln!(
530 out,
531 " @Override public int hashCode() {{ return Objects.hash({names}); }}"
532 );
533 let _ = writeln!(
534 out,
535 " @Override public String toString() {{ return \"{name}\" + java.util.List.of({names}); }}"
536 );
537 out.push_str("}\n");
538 out
539 }
540 ShapeKind::Enum(EnumInfo { variants, .. })
541 if variants
542 .iter()
543 .all(|v| matches!(classify_variant(v), VariantKind::Unit)) =>
544 {
545 let mut out = header(package);
546 out.push_str("import java.util.List;\nimport org.facet.phon.*;\n\n");
547 let constants = variants
548 .iter()
549 .map(|v| java_enum_constant(v.name))
550 .collect::<Vec<_>>()
551 .join(", ");
552 let _ = writeln!(out, "public enum {name} {{ {constants};");
553 let id = schema_id(shape);
554 let variant_exprs = variants
555 .iter()
556 .enumerate()
557 .map(|(index, variant)| {
558 format!(
559 "new Schema.Variant(\"{}\", {index}, Schema.Payload.unit())",
560 variant.name
561 )
562 })
563 .collect::<Vec<_>>()
564 .join(", ");
565 let _ = writeln!(
566 out,
567 " public static final Schema SCHEMA = new Schema(SchemaId.fromLong(0x{id:016x}L), List.of(), new Schema.EnumKind(\"{name}\", List.of({variant_exprs})));"
568 );
569 let _ = writeln!(
570 out,
571 " public static final PhonAdapter<{name}> ADAPTER = new PhonAdapter<>() {{"
572 );
573 out.push_str(" @Override public SchemaClosure schema() { return SchemaClosure.uncheckedOf(SCHEMA); }\n");
574 out.push_str(" @Override public void encode(PhonEncoder encoder, ");
575 let _ = writeln!(
576 out,
577 "{name} value) throws PhonException {{ encoder.writeU32Unaligned(value.ordinal()); }}"
578 );
579 let _ = writeln!(
580 out,
581 " @Override public {name} decode(PhonDecoder decoder) throws PhonException {{ long value = decoder.readU32Unaligned(); if (value >= values().length) throw new PhonException(PhonException.Kind.MALFORMED, \"invalid {name} discriminant \" + value); return values()[(int) value]; }}"
582 );
583 out.push_str(" };\n");
584 out.push_str("}\n");
585 out
586 }
587 _ => {
588 let mut out = header(package);
589 let _ = writeln!(
590 out,
591 "// Shape `{name}` is referenced through its generated adapter."
592 );
593 let _ = writeln!(out, "public final class {name} {{ private {name}() {{}} }}");
594 out
595 }
596 }
597}
598
599fn generate_args_type(package: &str, name: &str, method: &MethodDescriptor) -> String {
600 let mut out = header(package);
601 out.push_str("import java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport org.facet.phon.*;\nimport org.facet.vox.*;\n\n");
602 let _ = writeln!(out, "public final class {name} {{");
603 for arg in method.args {
604 let _ = writeln!(
605 out,
606 " private final {} {};",
607 java_arg_type(arg),
608 java_ident(arg.name)
609 );
610 }
611 let args = method
612 .args
613 .iter()
614 .map(|arg| format!("{} {}", java_arg_type(arg), java_ident(arg.name)))
615 .collect::<Vec<_>>()
616 .join(", ");
617 let _ = writeln!(out, " public {name}({args}) {{");
618 for arg in method.args {
619 let arg_name = java_ident(arg.name);
620 if is_list(arg.shape) {
621 let _ = writeln!(
622 out,
623 " this.{arg_name} = List.copyOf(Objects.requireNonNull({arg_name}, \"{arg_name}\"));"
624 );
625 } else if is_reference(arg.shape) && !is_option(arg.shape) {
626 let _ = writeln!(
627 out,
628 " this.{arg_name} = Objects.requireNonNull({arg_name}, \"{arg_name}\");"
629 );
630 } else {
631 let _ = writeln!(out, " this.{arg_name} = {arg_name};");
632 }
633 }
634 out.push_str(" }\n");
635 for arg in method.args {
636 let arg_name = java_ident(arg.name);
637 let _ = writeln!(
638 out,
639 " public {} {}() {{ return {}; }}",
640 java_arg_type(arg),
641 arg_name,
642 arg_name
643 );
644 }
645 let id = schema_id(method.args_shape);
646 let refs = method
647 .args
648 .iter()
649 .map(|arg| match channel_arg_direction(arg) {
650 Some(_) => {
651 "Schema.Ref.concrete(Schema.primitive(Schema.Primitive.BYTES).id())".to_string()
654 }
655 None => schema_ref_expr(arg.shape),
656 })
657 .collect::<Vec<_>>()
658 .join(", ");
659 let _ = writeln!(
660 out,
661 " public static final Schema SCHEMA = new Schema(SchemaId.fromLong(0x{id:016x}L), List.of(), new Schema.TupleKind(List.of({refs})));"
662 );
663 emit_exact_schema_closure(
664 &mut out,
665 method.args_shape,
666 "SCHEMA_CLOSURE",
667 "schemaClosure",
668 );
669 let _ = writeln!(
670 out,
671 " public static final PhonAdapter<{name}> ADAPTER = new PhonAdapter<>() {{"
672 );
673 let channel_roots = method
674 .args
675 .iter()
676 .enumerate()
677 .filter_map(|(index, arg)| {
678 let direction = match channel_arg_direction(arg) {
679 Some(JavaChannelDirection::Tx) => "tx",
680 Some(JavaChannelDirection::Rx) => "rx",
681 _ => return None,
682 };
683 let element = arg.channel_element.expect("channel element");
684 Some(format!(
685 "Map.entry(\"channel.arg.{index}.{direction}.element\", {}.schema())",
686 adapter_for(element)
687 ))
688 })
689 .collect::<Vec<_>>()
690 .join(", ");
691 let closure = if channel_roots.is_empty() {
692 "SCHEMA_CLOSURE".to_string()
693 } else {
694 format!(
695 "SchemaClosure.uncheckedWithAuxiliaryClosures(SCHEMA_CLOSURE, Map.ofEntries({channel_roots}))"
696 )
697 };
698 let _ = writeln!(
699 out,
700 " @Override public SchemaClosure schema() {{ return {closure}; }}"
701 );
702 let _ = writeln!(
703 out,
704 " @Override public void encode(PhonEncoder encoder, {name} value) throws PhonException {{"
705 );
706 let mut channel_index = 0_u32;
707 for arg in method.args {
708 let statement = match channel_arg_direction(arg) {
709 Some(_) => {
710 let current = channel_index;
711 channel_index += 1;
712 format!("encoder.writeBytes(VoxChannelDecoding.encodeIndex({current}L));")
713 }
714 None => encode_statement(arg.shape, &format!("value.{}()", java_ident(arg.name))),
715 };
716 let _ = writeln!(out, " {}", statement);
717 }
718 out.push_str(" }\n");
719 let decoded = method
720 .args
721 .iter()
722 .map(|arg| match channel_arg_direction(arg) {
723 Some(JavaChannelDirection::Tx) => {
724 "VoxChannelDecoding.tx(decoder.readBytes())".to_string()
725 }
726 Some(JavaChannelDirection::Rx) => {
727 "VoxChannelDecoding.rx(decoder.readBytes())".to_string()
728 }
729 None => decode_expression(arg.shape),
730 })
731 .collect::<Vec<_>>()
732 .join(", ");
733 let _ = writeln!(
734 out,
735 " @Override public {name} decode(PhonDecoder decoder) throws PhonException {{ return new {name}({decoded}); }}"
736 );
737 out.push_str(" };\n");
738 out.push_str("}\n");
739 out
740}
741
742fn emit_record_schema_and_adapter(
743 out: &mut String,
744 name: &str,
745 shape: &'static Shape,
746 fields: &[facet_core::Field],
747) {
748 let id = schema_id(shape);
749 let field_exprs = fields
750 .iter()
751 .map(|field| {
752 format!(
753 "new Schema.Field(\"{}\", {}, {})",
754 field.name,
755 schema_ref_expr(field.shape()),
756 field.default.is_none()
757 )
758 })
759 .collect::<Vec<_>>()
760 .join(", ");
761 let _ = writeln!(
762 out,
763 " public static final Schema SCHEMA = new Schema(SchemaId.fromLong(0x{id:016x}L), List.of(), new Schema.RecordKind(\"{name}\", List.of({field_exprs})));"
764 );
765 emit_exact_schema_closure(out, shape, "SCHEMA_CLOSURE", "schemaClosure");
766 let _ = writeln!(
767 out,
768 " public static final PhonAdapter<{name}> ADAPTER = new PhonAdapter<>() {{"
769 );
770 let _ = writeln!(
771 out,
772 " @Override public SchemaClosure schema() {{ return SCHEMA_CLOSURE; }}"
773 );
774 let _ = writeln!(
775 out,
776 " @Override public void encode(PhonEncoder encoder, {name} value) throws PhonException {{"
777 );
778 for field in fields {
779 let _ = writeln!(
780 out,
781 " {}",
782 encode_statement(
783 field.shape(),
784 &format!("value.{}()", java_ident(field.name))
785 )
786 );
787 }
788 out.push_str(" }\n");
789 let decoded = fields
790 .iter()
791 .map(|field| decode_expression(field.shape()))
792 .collect::<Vec<_>>()
793 .join(", ");
794 let _ = writeln!(
795 out,
796 " @Override public {name} decode(PhonDecoder decoder) throws PhonException {{ return new {name}({decoded}); }}"
797 );
798 out.push_str(" };\n");
799}
800
801fn schema_id(shape: &'static Shape) -> u64 {
802 vox_phon::schema_id_for_shape(shape)
803 .unwrap_or_else(|error| {
804 panic!(
805 "Java schema generation failed for `{}`: {error}",
806 shape.type_identifier
807 )
808 })
809 .as_u64()
810}
811
812fn emit_exact_schema_closure(
813 out: &mut String,
814 shape: &'static Shape,
815 field_name: &str,
816 factory_name: &str,
817) {
818 let module = phon_codegen::Module::from_shapes(&[shape]).unwrap_or_else(|error| {
819 panic!(
820 "Java schema closure generation failed for `{}`: {error}",
821 shape.type_identifier
822 )
823 });
824 let root = module
825 .roots
826 .first()
827 .expect("one schema closure root")
828 .id
829 .as_u64();
830 let bytes = schema_bundle_to_bytes(&module.schemas).expect("validated Java schema bundle");
831 let _ = writeln!(out, " private static final SchemaClosure {field_name} = {factory_name}();");
832 let _ = writeln!(out,
833 " private static SchemaClosure {factory_name}() {{\n try {{\n return SchemaClosure.fromSchemaBundleBytes(SchemaId.fromLong(0x{root:016x}L), {factory_name}Bundle(), PhonLimits.defaults());\n }} catch (PhonException error) {{\n throw new ExceptionInInitializerError(error);\n }}\n }}");
834 out.push_str(&phon_codegen::targets::java::render_byte_factory(&format!("{factory_name}Bundle"), &bytes));
835}
836
837fn exact_schema_closure_expression(shape: &'static Shape) -> String {
838 let module = phon_codegen::Module::from_shapes(&[shape]).unwrap_or_else(|error| {
839 panic!(
840 "Java schema closure generation failed for `{}`: {error}",
841 shape.type_identifier
842 )
843 });
844 let root = module
845 .roots
846 .first()
847 .expect("one schema closure root")
848 .id
849 .as_u64();
850 let bytes = schema_bundle_to_bytes(&module.schemas).expect("validated Java schema bundle");
851 let factory = phon_codegen::targets::java::render_byte_factory("bundle", &bytes);
852 format!("PrimitiveAdapters.schemaClosure(0x{root:016x}L, new java.util.function.Supplier<byte[]>() {{ {factory} public byte[] get() {{ return bundle(); }} }}.get())")
855}
856
857fn schema_ref_expr(shape: &'static Shape) -> String {
858 format!(
859 "Schema.Ref.concrete(SchemaId.fromLong(0x{:016x}L))",
860 schema_id(shape)
861 )
862}
863
864fn encode_statement(shape: &'static Shape, value: &str) -> String {
865 if is_bytes(shape) {
866 return format!("encoder.writeBytes({value});");
867 }
868 match classify_shape(shape) {
869 ShapeKind::Scalar(ScalarType::Bool) => format!("encoder.writeBool({value});"),
870 ShapeKind::Scalar(ScalarType::U8) => format!("encoder.writeU8({value});"),
871 ShapeKind::Scalar(ScalarType::U16) => format!("encoder.writeU16({value});"),
872 ShapeKind::Scalar(ScalarType::U32) => format!("encoder.writeU32({value});"),
873 ShapeKind::Scalar(ScalarType::I8) => format!("encoder.writeI8({value});"),
874 ShapeKind::Scalar(ScalarType::I16) => format!("encoder.writeI16({value});"),
875 ShapeKind::Scalar(ScalarType::I32) => format!("encoder.writeI32({value});"),
876 ShapeKind::Scalar(ScalarType::I64) => format!("encoder.writeI64({value});"),
877 ShapeKind::Scalar(ScalarType::F32) => format!("encoder.writeF32({value});"),
878 ShapeKind::Scalar(ScalarType::F64) => format!("encoder.writeF64({value});"),
879 ShapeKind::Scalar(ScalarType::Char) => format!("encoder.writeChar({value});"),
880 ShapeKind::Scalar(ScalarType::Unit) => String::new(),
881 ShapeKind::Scalar(ScalarType::Str | ScalarType::String | ScalarType::CowStr) => {
882 format!("encoder.writeString({value});")
883 }
884 ShapeKind::Struct(StructInfo {
885 name: Some(name), ..
886 })
887 | ShapeKind::Enum(EnumInfo {
888 name: Some(name), ..
889 }) => format!(
890 "encoder.writeAdapted({}.ADAPTER, {value});",
891 java_type_name(name)
892 ),
893 ShapeKind::List { element } | ShapeKind::Slice { element } => format!(
894 "PrimitiveAdapters.writeList(encoder, {value}, {});",
895 adapter_for(element)
896 ),
897 _ => format!(
898 "throw new PhonException(PhonException.Kind.DECODE, \"generated adapter for {} is not implemented\");",
899 shape.type_identifier.replace('"', "\\\"")
900 ),
901 }
902}
903
904fn decode_expression(shape: &'static Shape) -> String {
905 if is_bytes(shape) {
906 return "decoder.readBytes()".into();
907 }
908 match classify_shape(shape) {
909 ShapeKind::Scalar(ScalarType::Bool) => "decoder.readBool()".into(),
910 ShapeKind::Scalar(ScalarType::U8) => "decoder.readU8()".into(),
911 ShapeKind::Scalar(ScalarType::U16) => "decoder.readU16()".into(),
912 ShapeKind::Scalar(ScalarType::U32) => "decoder.readU32()".into(),
913 ShapeKind::Scalar(ScalarType::I8) => "decoder.readI8()".into(),
914 ShapeKind::Scalar(ScalarType::I16) => "decoder.readI16()".into(),
915 ShapeKind::Scalar(ScalarType::I32) => "decoder.readI32()".into(),
916 ShapeKind::Scalar(ScalarType::I64) => "decoder.readI64()".into(),
917 ShapeKind::Scalar(ScalarType::F32) => "decoder.readF32()".into(),
918 ShapeKind::Scalar(ScalarType::F64) => "decoder.readF64()".into(),
919 ShapeKind::Scalar(ScalarType::Char) => "decoder.readChar()".into(),
920 ShapeKind::Scalar(ScalarType::Unit) => "null".into(),
921 ShapeKind::Scalar(ScalarType::Str | ScalarType::String | ScalarType::CowStr) => {
922 "decoder.readString()".into()
923 }
924 ShapeKind::Struct(StructInfo {
925 name: Some(name), ..
926 })
927 | ShapeKind::Enum(EnumInfo {
928 name: Some(name), ..
929 }) => format!("decoder.readAdapted({}.ADAPTER)", java_type_name(name)),
930 ShapeKind::List { element } | ShapeKind::Slice { element } => format!(
931 "PrimitiveAdapters.readList(decoder, {})",
932 adapter_for(element)
933 ),
934 _ => format!(
935 "unsupportedDecode(\"{}\")",
936 shape.type_identifier.replace('"', "\\\"")
937 ),
938 }
939}
940
941fn generate_descriptor(package: &str, name: &str, service: &ServiceDescriptor) -> String {
942 let mut out = header(package);
943 out.push_str("import java.util.List;\nimport org.facet.vox.*;\n\n");
944 let _ = writeln!(out, "public final class {name}ServiceDescriptor {{");
945 for method in service.methods {
946 let upper = java_constant(method.method_name);
947 let args_adapter = format!("{}.ADAPTER", args_type_name(name, method));
948 let (ok, err) = result_parts(method.return_shape);
949 let ret_adapter = adapter_for(ok);
950 let response_adapter = format!("{}.ADAPTER", response_type_name(name, method));
951 let channels = method
952 .args
953 .iter()
954 .enumerate()
955 .filter_map(|(index, arg)| {
956 let direction = match channel_arg_direction(arg) {
957 Some(JavaChannelDirection::Tx) => "TX",
958 Some(JavaChannelDirection::Rx) => "RX",
959 _ => return None,
960 };
961 let element = arg.channel_element.expect("channel element");
962 Some(format!(
963 "new ChannelDescriptor({index}, ChannelDescriptor.Direction.{direction}, \"channel.arg.{index}.{}.element\", {})",
964 direction.to_ascii_lowercase(),
965 adapter_for(element)
966 ))
967 })
968 .collect::<Vec<_>>()
969 .join(", ");
970 let channel_list = format!("List.of({channels})");
971 let _ = if let Some(err) = err {
972 writeln!(
973 out,
974 " public static final MethodDescriptor {upper} = new MethodDescriptor(0x{:016x}L, \"{}\", {args_adapter}, {ret_adapter}, {}, {response_adapter}, {channel_list});",
975 crate::method_id(method),
976 method.method_name,
977 adapter_for(err)
978 )
979 } else {
980 writeln!(
981 out,
982 " public static final MethodDescriptor {upper} = new MethodDescriptor(0x{:016x}L, \"{}\", {args_adapter}, {ret_adapter}, null, {response_adapter}, {channel_list});",
983 crate::method_id(method),
984 method.method_name
985 )
986 };
987 }
988 let methods = service
989 .methods
990 .iter()
991 .map(|m| java_constant(m.method_name))
992 .collect::<Vec<_>>()
993 .join(", ");
994 let _ = writeln!(
995 out,
996 " public static final ServiceDescriptor INSTANCE = new ServiceDescriptor(\"{}\", List.of({methods}));",
997 service.service_name
998 );
999 let _ = writeln!(out, " private {name}ServiceDescriptor() {{}}");
1000 out.push_str("}\n");
1001 out
1002}
1003
1004fn generate_handler(package: &str, name: &str, service: &ServiceDescriptor) -> String {
1005 let mut out = header(package);
1006 out.push_str("import java.util.concurrent.CompletableFuture;\nimport org.facet.vox.*;\n\n");
1007 let _ = writeln!(out, "public interface {name}Handler {{");
1008 for method in service.methods {
1009 let args = method
1010 .args
1011 .iter()
1012 .map(|a| format!("{} {}", java_arg_type(a), java_ident(a.name)))
1013 .collect::<Vec<_>>()
1014 .join(", ");
1015 let args = if args.is_empty() {
1016 "CallContext context".to_string()
1017 } else {
1018 format!("CallContext context, {args}")
1019 };
1020 let return_ty = handler_return(method.return_shape);
1021 let _ = writeln!(
1022 out,
1023 " CompletableFuture<{return_ty}> {}({args});",
1024 java_ident(method.method_name)
1025 );
1026 }
1027 out.push_str("}\n");
1028 out
1029}
1030
1031fn generate_response_type(
1032 package: &str,
1033 name: &str,
1034 method: &MethodDescriptor,
1035) -> Result<String, JavaCodegenError> {
1036 let (ok_shape, error_shape) = result_parts(method.return_shape);
1037 let ok_type = boxed_java_type(ok_shape);
1038 let error_type = error_shape.map_or_else(|| "Void".to_string(), boxed_java_type);
1039 let ok_adapter = adapter_for(ok_shape);
1040 let error_adapter = error_shape.map(adapter_for);
1041 let mut out = header(package);
1042 out.push_str("import org.facet.phon.*;\nimport org.facet.vox.VoxResult;\n\n");
1043 let _ = writeln!(out, "public final class {name} {{");
1044 emit_exact_schema_closure(
1045 &mut out,
1046 method.response_wire_shape,
1047 "SCHEMA",
1048 "responseSchema",
1049 );
1050 let _ = writeln!(
1051 out,
1052 " public static final PhonAdapter<VoxResult<{ok_type}, {error_type}>> ADAPTER = new PhonAdapter<>() {{"
1053 );
1054 out.push_str(" @Override public SchemaClosure schema() { return SCHEMA; }\n");
1055 let _ = writeln!(
1056 out,
1057 " @Override public void encode(PhonEncoder encoder, VoxResult<{ok_type}, {error_type}> value) throws PhonException {{"
1058 );
1059 out.push_str(
1060 " if (value.isSuccess()) {\n encoder.writeU32Unaligned(0);\n encoder.writeAdapted(",
1061 );
1062 let _ = writeln!(
1063 out,
1064 "{ok_adapter}, value.success());\n return;\n }}"
1065 );
1066 out.push_str(" encoder.writeU32Unaligned(1);\n switch (value.kind()) {\n");
1067 if let Some(ref error_adapter) = error_adapter {
1068 let _ = writeln!(
1069 out,
1070 " case APPLICATION_ERROR:\n encoder.writeU32Unaligned(0);\n encoder.writeAdapted({error_adapter}, value.applicationError());\n return;"
1071 );
1072 } else {
1073 out.push_str(
1074 " case APPLICATION_ERROR:\n throw new PhonException(PhonException.Kind.ENCODE, \"infallible method cannot encode an application error\");\n",
1075 );
1076 }
1077 out.push_str(
1078 " case UNKNOWN_METHOD: encoder.writeU32Unaligned(1); return;\n\
1079 case INVALID_PAYLOAD: encoder.writeU32Unaligned(2); encoder.writeString(value.detail()); return;\n\
1080 case CANCELLED: encoder.writeU32Unaligned(3); return;\n\
1081 case CONNECTION_CLOSED: encoder.writeU32Unaligned(4); return;\n\
1082 case CONNECTION_SHUTDOWN: encoder.writeU32Unaligned(5); return;\n\
1083 case SEND_FAILED: encoder.writeU32Unaligned(6); return;\n\
1084 case TIMED_OUT: encoder.writeU32Unaligned(7); return;\n\
1085 case INDETERMINATE: encoder.writeU32Unaligned(8); return;\n\
1086 default: throw new PhonException(PhonException.Kind.ENCODE, \"invalid response result kind\");\n\
1087 }\n }\n",
1088 );
1089 let _ = writeln!(
1090 out,
1091 " @Override public VoxResult<{ok_type}, {error_type}> decode(PhonDecoder decoder) throws PhonException {{"
1092 );
1093 out.push_str(" long outer = decoder.readU32Unaligned();\n");
1094 if matches!(
1095 classify_shape(ok_shape),
1096 ShapeKind::Scalar(ScalarType::Unit)
1097 ) {
1098 let _ = writeln!(
1099 out,
1100 " if (outer == 0) {{ decoder.readAdapted({ok_adapter}); return VoxResult.successUnit(); }}"
1101 );
1102 } else {
1103 let _ = writeln!(
1104 out,
1105 " if (outer == 0) return VoxResult.success(decoder.readAdapted({ok_adapter}));"
1106 );
1107 }
1108 out.push_str(
1109 " if (outer != 1) throw new PhonException(PhonException.Kind.MALFORMED, \"invalid Result discriminant\");\n long error = decoder.readU32Unaligned();\n switch ((int) error) {\n",
1110 );
1111 if let Some(ref error_adapter) = error_adapter {
1112 let _ = writeln!(
1113 out,
1114 " case 0: return VoxResult.applicationError(decoder.readAdapted({error_adapter}));"
1115 );
1116 } else {
1117 out.push_str(
1118 " case 0: throw new PhonException(PhonException.Kind.MALFORMED, \"infallible method received User error\");\n",
1119 );
1120 }
1121 out.push_str(
1122 " case 1: return VoxResult.infrastructure(VoxResult.Kind.UNKNOWN_METHOD);\n\
1123 case 2: return VoxResult.infrastructure(VoxResult.Kind.INVALID_PAYLOAD, decoder.readString());\n\
1124 case 3: return VoxResult.infrastructure(VoxResult.Kind.CANCELLED);\n\
1125 case 4: return VoxResult.infrastructure(VoxResult.Kind.CONNECTION_CLOSED);\n\
1126 case 5: return VoxResult.infrastructure(VoxResult.Kind.CONNECTION_SHUTDOWN);\n\
1127 case 6: return VoxResult.infrastructure(VoxResult.Kind.SEND_FAILED);\n\
1128 case 7: return VoxResult.infrastructure(VoxResult.Kind.TIMED_OUT);\n\
1129 case 8: return VoxResult.infrastructure(VoxResult.Kind.INDETERMINATE);\n\
1130 default: throw new PhonException(PhonException.Kind.MALFORMED, \"invalid VoxError discriminant\");\n\
1131 }\n }\n };\n",
1132 );
1133 let _ = writeln!(out, " private {name}() {{}}");
1134 out.push_str("}\n");
1135 Ok(out)
1136}
1137
1138fn generate_dispatcher(package: &str, name: &str, service: &ServiceDescriptor) -> String {
1139 let mut out = header(package);
1140 out.push_str("import java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport org.facet.phon.*;\nimport org.facet.vox.*;\n\n");
1141 let _ = writeln!(
1142 out,
1143 "public final class {name}Dispatcher implements ServiceDispatcher {{"
1144 );
1145 let _ = writeln!(out, " private final {name}Handler handler;");
1146 let _ = writeln!(
1147 out,
1148 " public {name}Dispatcher({name}Handler handler) {{ this.handler = java.util.Objects.requireNonNull(handler); }}"
1149 );
1150 let _ = writeln!(
1151 out,
1152 " @Override public ServiceDescriptor descriptor() {{ return {name}ServiceDescriptor.INSTANCE; }}"
1153 );
1154 out.push_str(
1155 " @Override public CompletableFuture<Void> dispatch(InboundCall call) {\n try {\n",
1156 );
1157 for method in service.methods {
1158 let constant = java_constant(method.method_name);
1159 let tuple_ty = args_type_name(name, method);
1160 let invoke_args = method
1161 .args
1162 .iter()
1163 .map(|arg| format!("args.{}()", java_ident(arg.name)))
1164 .collect::<Vec<_>>()
1165 .join(", ");
1166 let invoke_args = if invoke_args.is_empty() {
1167 "call.context()".to_string()
1168 } else {
1169 format!("call.context(), {invoke_args}")
1170 };
1171 let (_, error_shape) = result_parts(method.return_shape);
1172 let response_ty = response_type_name(name, method);
1173 let _ = writeln!(
1174 out,
1175 " if (call.method().id() == {name}ServiceDescriptor.{constant}.id()) {{"
1176 );
1177 let _ = writeln!(
1178 out,
1179 " {tuple_ty} args = call.decodeArguments({tuple_ty}.ADAPTER);"
1180 );
1181 if error_shape.is_some() {
1182 let _ = writeln!(
1183 out,
1184 " return handler.{}({invoke_args}).thenAccept(value -> {{\n try {{\n call.respond(PhonCodec.encode({response_ty}.ADAPTER, value, PhonLimits.defaults()));\n }} catch (PhonException error) {{\n throw new CompletionException(error);\n }}\n }});",
1185 java_ident(method.method_name)
1186 );
1187 } else {
1188 let success = if matches!(
1189 classify_shape(method.return_shape),
1190 ShapeKind::Scalar(ScalarType::Unit)
1191 ) {
1192 "VoxResult.successUnit()"
1193 } else {
1194 "VoxResult.success(value)"
1195 };
1196 let _ = writeln!(
1197 out,
1198 " return handler.{}({invoke_args}).thenAccept(value -> {{\n try {{\n call.respond(PhonCodec.encode({response_ty}.ADAPTER, {success}, PhonLimits.defaults()));\n }} catch (PhonException error) {{\n throw new CompletionException(error);\n }}\n }});",
1199 java_ident(method.method_name),
1200 );
1201 }
1202 out.push_str(" }\n");
1203 }
1204 out.push_str(" return CompletableFuture.failedFuture(new VoxException(\"unknown method \" + call.method().id()));\n } catch (Exception error) {\n return CompletableFuture.failedFuture(error);\n }\n }\n}\n");
1205 out
1206}
1207
1208fn generate_client(package: &str, name: &str, service: &ServiceDescriptor) -> String {
1209 let mut out = header(package);
1210 out.push_str("import java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport org.facet.phon.*;\nimport org.facet.vox.*;\n\n");
1211 let _ = writeln!(out, "public final class {name}Client {{");
1212 out.push_str(" private final ServiceLane lane;\n");
1213 let _ = writeln!(
1214 out,
1215 " public {name}Client(ServiceLane lane) {{ this.lane = java.util.Objects.requireNonNull(lane); }}"
1216 );
1217 for method in service.methods {
1218 let args = method
1219 .args
1220 .iter()
1221 .map(|a| format!("{} {}", java_arg_type(a), java_ident(a.name)))
1222 .collect::<Vec<_>>()
1223 .join(", ");
1224 let return_ty = handler_return(method.return_shape);
1225 let method_name = java_ident(method.method_name);
1226 let constant = java_constant(method.method_name);
1227 let tuple_ty = args_type_name(name, method);
1228 let (_, error_shape) = result_parts(method.return_shape);
1229 let response_ty = response_type_name(name, method);
1230 let wire_result_ty = wire_result_type(method.return_shape);
1231 let channel_arguments = method
1232 .args
1233 .iter()
1234 .enumerate()
1235 .filter_map(|(argument_index, arg)| {
1236 let (factory, argument_name) = match channel_arg_direction(arg) {
1237 Some(JavaChannelDirection::Tx) => ("tx", java_ident(arg.name)),
1238 Some(JavaChannelDirection::Rx) => ("rx", java_ident(arg.name)),
1239 _ => return None,
1240 };
1241 let channel_index = method.args[..argument_index]
1242 .iter()
1243 .filter(|previous| channel_arg_direction(previous).is_some())
1244 .count();
1245 Some(format!(
1246 "VoxChannelArgument.{factory}({name}ServiceDescriptor.{constant}.channels().get({channel_index}), {argument_name})"
1247 ))
1248 })
1249 .collect::<Vec<_>>()
1250 .join(", ");
1251 let channel_list = format!("List.of({channel_arguments})");
1252 let _ = writeln!(
1253 out,
1254 " public CompletableFuture<{return_ty}> {method_name}({args}) {{ return {method_name}({}{sep}CallOptions.defaults()); }}",
1255 args_names(method),
1256 sep = if args.is_empty() { "" } else { ", " }
1257 );
1258 let with_opts = if args.is_empty() {
1259 "CallOptions options".to_string()
1260 } else {
1261 format!("{args}, CallOptions options")
1262 };
1263 let _ = writeln!(
1264 out,
1265 " public CompletableFuture<{return_ty}> {method_name}({with_opts}) {{"
1266 );
1267 let _ = writeln!(
1268 out,
1269 " try {{\n byte[] encoded = PhonCodec.encode({tuple_ty}.ADAPTER, new {}({}), PhonLimits.defaults());",
1270 tuple_ty,
1271 args_names(method)
1272 );
1273 if error_shape.is_some() {
1274 let _ = writeln!(
1275 out,
1276 " return VoxFutures.mapCancellable(lane.call({name}ServiceDescriptor.{constant}, encoded, options, {channel_list}), bytes -> {{\n try {{\n {wire_result_ty} result = PhonCodec.decode({response_ty}.ADAPTER, bytes, PhonLimits.defaults());\n if (result.isInfrastructureError()) throw remoteFailure(result);\n return result;\n }} catch (PhonException error) {{\n throw new CompletionException(error);\n }}\n }});\n }} catch (PhonException error) {{\n return CompletableFuture.failedFuture(error);\n }}"
1277 );
1278 } else {
1279 let _ = writeln!(
1280 out,
1281 " return VoxFutures.mapCancellable(lane.call({name}ServiceDescriptor.{constant}, encoded, options, {channel_list}), bytes -> {{\n try {{\n {wire_result_ty} result = PhonCodec.decode({response_ty}.ADAPTER, bytes, PhonLimits.defaults());\n if (!result.isSuccess()) throw remoteFailure(result);\n return result.success();\n }} catch (PhonException error) {{\n throw new CompletionException(error);\n }}\n }});\n }} catch (PhonException error) {{\n return CompletableFuture.failedFuture(error);\n }}"
1282 );
1283 }
1284 out.push_str(" }\n");
1285 }
1286 out.push_str(
1287 " private static CompletionException remoteFailure(VoxResult<?, ?> result) {\n\
1288 String detail = result.detail() == null ? \"\" : \": \" + result.detail();\n\
1289 return new CompletionException(new VoxException(\"remote Vox error \" + result.kind() + detail));\n\
1290 }\n}\n",
1291 );
1292 out
1293}
1294
1295fn args_names(method: &MethodDescriptor) -> String {
1296 method
1297 .args
1298 .iter()
1299 .map(|a| java_ident(a.name))
1300 .collect::<Vec<_>>()
1301 .join(", ")
1302}
1303
1304fn args_type_name(service_name: &str, method: &MethodDescriptor) -> String {
1305 format!("{service_name}{}Args", upper_camel(method.method_name))
1306}
1307
1308fn response_type_name(service_name: &str, method: &MethodDescriptor) -> String {
1309 format!("{service_name}{}Response", upper_camel(method.method_name))
1310}
1311
1312fn result_parts(shape: &'static Shape) -> (&'static Shape, Option<&'static Shape>) {
1313 match classify_shape(shape) {
1314 ShapeKind::Result { ok, err } => (ok, Some(err)),
1315 _ => (shape, None),
1316 }
1317}
1318
1319fn handler_return(shape: &'static Shape) -> String {
1320 let (ok, err) = result_parts(shape);
1321 match err {
1322 Some(err) => format!(
1323 "VoxResult<{}, {}>",
1324 boxed_java_type(ok),
1325 boxed_java_type(err)
1326 ),
1327 None => boxed_java_type(ok),
1328 }
1329}
1330
1331fn wire_result_type(shape: &'static Shape) -> String {
1332 let (ok, err) = result_parts(shape);
1333 format!(
1334 "VoxResult<{}, {}>",
1335 boxed_java_type(ok),
1336 err.map_or_else(|| "Void".to_string(), boxed_java_type)
1337 )
1338}
1339
1340fn adapter_for(shape: &'static Shape) -> String {
1341 if is_bytes(shape) {
1342 return "PrimitiveAdapters.BYTES".to_string();
1343 }
1344 match classify_shape(shape) {
1345 ShapeKind::Struct(StructInfo {
1346 name: Some(name), ..
1347 })
1348 | ShapeKind::Enum(EnumInfo {
1349 name: Some(name), ..
1350 }) => {
1351 format!("{}.ADAPTER", java_type_name(name))
1352 }
1353 ShapeKind::Scalar(scalar) => {
1354 format!("PrimitiveAdapters.{}", primitive_adapter_name(scalar))
1355 }
1356 ShapeKind::List { element } | ShapeKind::Slice { element } => format!(
1357 "PrimitiveAdapters.list({}, {})",
1358 adapter_for(element),
1359 exact_schema_closure_expression(shape)
1360 ),
1361 _ => format!(
1362 "PrimitiveAdapters.unsupported(\"{}\")",
1363 shape.type_identifier
1364 ),
1365 }
1366}
1367
1368fn generate_primitive_adapters(package: &str) -> String {
1369 let mut out = header(package);
1370 out.push_str(
1371 "import java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport org.facet.phon.*;\n\n",
1372 );
1373 out.push_str("final class PrimitiveAdapters {\n");
1374 let adapters = [
1375 (
1376 "STRING",
1377 "String",
1378 "STRING",
1379 "encoder.writeString(value);",
1380 "decoder.readString()",
1381 ),
1382 (
1383 "BOOL",
1384 "Boolean",
1385 "BOOL",
1386 "encoder.writeBool(value);",
1387 "decoder.readBool()",
1388 ),
1389 (
1390 "I32",
1391 "Integer",
1392 "I32",
1393 "encoder.writeI32(value);",
1394 "decoder.readI32()",
1395 ),
1396 (
1397 "I64",
1398 "Long",
1399 "I64",
1400 "encoder.writeI64(value);",
1401 "decoder.readI64()",
1402 ),
1403 (
1404 "U8",
1405 "Integer",
1406 "U8",
1407 "encoder.writeU8(value);",
1408 "decoder.readU8()",
1409 ),
1410 (
1411 "U16",
1412 "Integer",
1413 "U16",
1414 "encoder.writeU16(value);",
1415 "decoder.readU16()",
1416 ),
1417 (
1418 "U32",
1419 "Long",
1420 "U32",
1421 "encoder.writeU32(value);",
1422 "decoder.readU32()",
1423 ),
1424 (
1425 "F32",
1426 "Float",
1427 "F32",
1428 "encoder.writeF32(value);",
1429 "decoder.readF32()",
1430 ),
1431 (
1432 "F64",
1433 "Double",
1434 "F64",
1435 "encoder.writeF64(value);",
1436 "decoder.readF64()",
1437 ),
1438 (
1439 "BYTES",
1440 "byte[]",
1441 "BYTES",
1442 "encoder.writeBytes(value);",
1443 "decoder.readBytes()",
1444 ),
1445 ];
1446 for (field, ty, primitive, encode, decode) in adapters {
1447 let _ = writeln!(
1448 out,
1449 " static final PhonAdapter<{ty}> {field} = new PhonAdapter<>() {{"
1450 );
1451 let _ = writeln!(
1452 out,
1453 " @Override public SchemaClosure schema() {{ return SchemaClosure.uncheckedOf(Schema.primitive(Schema.Primitive.{primitive})); }}"
1454 );
1455 let _ = writeln!(
1456 out,
1457 " @Override public void encode(PhonEncoder encoder, {ty} value) throws PhonException {{ {encode} }}"
1458 );
1459 let _ = writeln!(
1460 out,
1461 " @Override public {ty} decode(PhonDecoder decoder) throws PhonException {{ return {decode}; }}"
1462 );
1463 out.push_str(" };\n");
1464 }
1465 out.push_str(
1466 " static final PhonAdapter<Void> UNIT = new PhonAdapter<>() {\n private final SchemaClosure schema = SchemaClosure.uncheckedOf(Schema.primitive(Schema.Primitive.UNIT));\n @Override public SchemaClosure schema() { return schema; }\n @Override public void encode(PhonEncoder encoder, Void value) throws PhonException {}\n @Override public Void decode(PhonDecoder decoder) throws PhonException { return null; }\n };\n",
1467 );
1468 out.push_str(
1469 " static <T> void writeList(PhonEncoder encoder, List<T> values, PhonAdapter<T> elementAdapter) throws PhonException {\n Objects.requireNonNull(values, \"values\");\n Objects.requireNonNull(elementAdapter, \"elementAdapter\");\n encoder.writeCount(values.size());\n for (T value : values) encoder.writeAdapted(elementAdapter, Objects.requireNonNull(value, \"list element\"));\n }\n",
1470 );
1471 out.push_str(
1472 " static <T> List<T> readList(PhonDecoder decoder, PhonAdapter<T> elementAdapter) throws PhonException {\n Objects.requireNonNull(elementAdapter, \"elementAdapter\");\n int count = decoder.readCount();\n ArrayList<T> values = new ArrayList<>(count);\n for (int index = 0; index < count; index++) values.add(decoder.readAdapted(elementAdapter));\n return List.copyOf(values);\n }\n",
1473 );
1474 out.push_str(
1475 " static <T> PhonAdapter<List<T>> list(PhonAdapter<T> elementAdapter, SchemaClosure schema) {\n Objects.requireNonNull(elementAdapter, \"elementAdapter\");\n Objects.requireNonNull(schema, \"schema\");\n return new PhonAdapter<>() {\n @Override public SchemaClosure schema() { return schema; }\n @Override public void encode(PhonEncoder encoder, List<T> values) throws PhonException { writeList(encoder, values, elementAdapter); }\n @Override public List<T> decode(PhonDecoder decoder) throws PhonException { return readList(decoder, elementAdapter); }\n };\n }\n",
1476 );
1477 out.push_str(
1478 " static SchemaClosure schemaClosure(long rootId, byte[] schemaBundle) {\n try {\n return SchemaClosure.fromSchemaBundleBytes(SchemaId.fromLong(rootId), schemaBundle, PhonLimits.defaults());\n } catch (PhonException error) {\n throw new ExceptionInInitializerError(error);\n }\n }\n",
1479 );
1480 out.push_str(" static <T> PhonAdapter<T> unsupported(String shape) { throw new IllegalArgumentException(\"unsupported generated adapter \" + shape); }\n");
1481 out.push_str(" private PrimitiveAdapters() {}\n}\n");
1482 out
1483}
1484
1485fn primitive_adapter_name(scalar: ScalarType) -> &'static str {
1486 match scalar {
1487 ScalarType::Bool => "BOOL",
1488 ScalarType::U8 => "U8",
1489 ScalarType::U16 => "U16",
1490 ScalarType::U32 => "U32",
1491 ScalarType::I8 | ScalarType::I16 | ScalarType::I32 => "I32",
1492 ScalarType::I64 => "I64",
1493 ScalarType::F32 => "F32",
1494 ScalarType::F64 => "F64",
1495 ScalarType::Str | ScalarType::String | ScalarType::CowStr | ScalarType::Char => "STRING",
1496 ScalarType::Unit => "UNIT",
1497 _ => "I64",
1498 }
1499}
1500
1501fn java_type(shape: &'static Shape, boxed: bool) -> String {
1502 if is_bytes(shape) {
1503 return "byte[]".to_string();
1504 }
1505 match classify_shape(shape) {
1506 ShapeKind::Scalar(s) => scalar_java(s, boxed),
1507 ShapeKind::Option { inner } => format!("java.util.Optional<{}>", boxed_java_type(inner)),
1508 ShapeKind::List { element }
1509 | ShapeKind::Slice { element }
1510 | ShapeKind::Array { element, .. } => {
1511 format!("java.util.List<{}>", boxed_java_type(element))
1512 }
1513 ShapeKind::Set { element } => format!("java.util.Set<{}>", boxed_java_type(element)),
1514 ShapeKind::Map { key, value } => format!(
1515 "java.util.Map<{}, {}>",
1516 boxed_java_type(key),
1517 boxed_java_type(value)
1518 ),
1519 ShapeKind::Struct(StructInfo {
1520 name: Some(name), ..
1521 })
1522 | ShapeKind::Enum(EnumInfo {
1523 name: Some(name), ..
1524 }) => java_type_name(name),
1525 ShapeKind::Tuple { elements } => format!("Tuple{}", elements.len()),
1526 ShapeKind::TupleStruct { fields } => format!("Tuple{}", fields.len()),
1527 ShapeKind::Result { ok, err } => format!(
1528 "VoxResult<{}, {}>",
1529 boxed_java_type(ok),
1530 boxed_java_type(err)
1531 ),
1532 ShapeKind::Tx { inner } => format!("VoxTx<{}>", boxed_java_type(inner)),
1533 ShapeKind::Rx { inner } => format!("VoxRx<{}>", boxed_java_type(inner)),
1534 ShapeKind::Pointer { pointee } => java_type(pointee, boxed),
1535 _ => "Object".to_string(),
1536 }
1537}
1538
1539fn boxed_java_type(shape: &'static Shape) -> String {
1540 java_type(shape, true)
1541}
1542
1543fn java_default_expression(shape: &'static Shape) -> String {
1544 match classify_shape(shape) {
1545 ShapeKind::Scalar(ScalarType::Bool) => "false".into(),
1546 ShapeKind::Scalar(
1547 ScalarType::U8
1548 | ScalarType::U16
1549 | ScalarType::U32
1550 | ScalarType::I8
1551 | ScalarType::I16
1552 | ScalarType::I32
1553 | ScalarType::I64
1554 | ScalarType::U64
1555 | ScalarType::ISize
1556 | ScalarType::USize,
1557 ) => "0".into(),
1558 ShapeKind::Scalar(ScalarType::F32) => "0.0f".into(),
1559 ShapeKind::Scalar(ScalarType::F64) => "0.0d".into(),
1560 ShapeKind::Scalar(ScalarType::Unit) => "null".into(),
1561 ShapeKind::Scalar(
1562 ScalarType::Char | ScalarType::Str | ScalarType::String | ScalarType::CowStr,
1563 ) => "\"\"".into(),
1564 ShapeKind::Scalar(ScalarType::U128 | ScalarType::I128) => {
1565 "java.math.BigInteger.ZERO".into()
1566 }
1567 ShapeKind::Option { .. } => "java.util.Optional.empty()".into(),
1568 ShapeKind::List { .. } | ShapeKind::Slice { .. } | ShapeKind::Array { .. } => {
1569 "java.util.List.of()".into()
1570 }
1571 ShapeKind::Set { .. } => "java.util.Set.of()".into(),
1572 ShapeKind::Map { .. } => "java.util.Map.of()".into(),
1573 ShapeKind::Struct(StructInfo {
1574 name: Some(name),
1575 fields,
1576 ..
1577 }) => {
1578 let values = fields
1579 .iter()
1580 .map(|field| java_default_expression(field.shape()))
1581 .collect::<Vec<_>>()
1582 .join(", ");
1583 format!("new {}({values})", java_type_name(name))
1584 }
1585 ShapeKind::Enum(EnumInfo {
1586 name: Some(name),
1587 variants,
1588 }) if variants
1589 .iter()
1590 .all(|variant| matches!(classify_variant(variant), VariantKind::Unit)) =>
1591 {
1592 variants.first().map_or_else(
1593 || "null".to_string(),
1594 |variant| {
1595 format!(
1596 "{}.{}",
1597 java_type_name(name),
1598 java_enum_constant(variant.name)
1599 )
1600 },
1601 )
1602 }
1603 ShapeKind::Pointer { pointee } => java_default_expression(pointee),
1604 _ => "null".into(),
1605 }
1606}
1607
1608fn scalar_java(scalar: ScalarType, boxed: bool) -> String {
1609 match scalar {
1610 ScalarType::Bool => if boxed { "Boolean" } else { "boolean" }.into(),
1611 ScalarType::U8 | ScalarType::U16 | ScalarType::I8 | ScalarType::I16 | ScalarType::I32 => {
1612 if boxed { "Integer" } else { "int" }.into()
1613 }
1614 ScalarType::U32
1615 | ScalarType::I64
1616 | ScalarType::U64
1617 | ScalarType::ISize
1618 | ScalarType::USize => if boxed { "Long" } else { "long" }.into(),
1619 ScalarType::F32 => if boxed { "Float" } else { "float" }.into(),
1620 ScalarType::F64 => if boxed { "Double" } else { "double" }.into(),
1621 ScalarType::U128 | ScalarType::I128 => "java.math.BigInteger".into(),
1622 ScalarType::Char | ScalarType::Str | ScalarType::String | ScalarType::CowStr => {
1623 "String".into()
1624 }
1625 ScalarType::Unit => "Void".into(),
1626 _ => "Object".into(),
1627 }
1628}
1629
1630fn is_reference(shape: &'static Shape) -> bool {
1631 !matches!(
1632 classify_shape(shape),
1633 ShapeKind::Scalar(
1634 ScalarType::Bool
1635 | ScalarType::U8
1636 | ScalarType::U16
1637 | ScalarType::U32
1638 | ScalarType::U64
1639 | ScalarType::USize
1640 | ScalarType::I8
1641 | ScalarType::I16
1642 | ScalarType::I32
1643 | ScalarType::I64
1644 | ScalarType::ISize
1645 | ScalarType::F32
1646 | ScalarType::F64
1647 | ScalarType::Char
1648 )
1649 )
1650}
1651
1652fn is_option(shape: &'static Shape) -> bool {
1653 matches!(classify_shape(shape), ShapeKind::Option { .. })
1654}
1655
1656fn is_list(shape: &'static Shape) -> bool {
1657 !is_bytes(shape)
1658 && matches!(
1659 classify_shape(shape),
1660 ShapeKind::List { .. } | ShapeKind::Slice { .. }
1661 )
1662}
1663
1664fn java_type_name(name: &str) -> String {
1665 name.rsplit("::")
1666 .next()
1667 .unwrap_or(name)
1668 .chars()
1669 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
1670 .collect()
1671}
1672
1673fn upper_camel(name: &str) -> String {
1674 name.split('_')
1675 .filter(|part| !part.is_empty())
1676 .map(|part| {
1677 let mut chars = part.chars();
1678 chars
1679 .next()
1680 .map(|c| c.to_ascii_uppercase().to_string() + chars.as_str())
1681 .unwrap_or_default()
1682 })
1683 .collect()
1684}
1685
1686fn java_ident(name: &str) -> String {
1687 let mut out = String::new();
1688 for (i, part) in name.split('_').enumerate() {
1689 if i == 0 {
1690 out.push_str(part);
1691 } else {
1692 let mut chars = part.chars();
1693 if let Some(c) = chars.next() {
1694 out.push(c.to_ascii_uppercase());
1695 }
1696 out.extend(chars);
1697 }
1698 }
1699 match out.as_str() {
1700 "class" | "enum" | "interface" | "return" | "default" | "switch" | "case" | "new" => {
1701 format!("{out}_")
1702 }
1703 _ => out,
1704 }
1705}
1706
1707fn java_constant(name: &str) -> String {
1708 name.chars()
1709 .flat_map(|c| {
1710 if c.is_ascii_uppercase() {
1711 vec!['_', c]
1712 } else {
1713 vec![c.to_ascii_uppercase()]
1714 }
1715 })
1716 .collect::<String>()
1717 .trim_start_matches('_')
1718 .to_string()
1719}
1720
1721fn java_enum_constant(name: &str) -> String {
1722 java_constant(name)
1723}
1724
1725#[cfg(test)]
1726mod tests {
1727 use std::fs;
1728 use std::path::{Path, PathBuf};
1729 use std::process::Command;
1730 use std::time::{SystemTime, UNIX_EPOCH};
1731
1732 use facet::Facet;
1733 use vox_types::{MethodDescriptorOptions, Rx, ServiceDescriptor, method_descriptor};
1734
1735 use super::{generate_service, generate_wire_schemas};
1736
1737 #[derive(Clone, Debug, Facet)]
1738 struct NestedRequest {
1739 message: String,
1740 }
1741
1742 #[derive(Clone, Debug, Facet)]
1743 struct NestedResponse {
1744 echoed: String,
1745 }
1746
1747 #[derive(Clone, Debug, Facet)]
1748 struct DefaultedRequest {
1749 required: String,
1750 #[facet(default)]
1751 optional_count: u32,
1752 }
1753
1754 #[derive(Clone, Debug, PartialEq, Eq, Facet)]
1755 struct ListItem {
1756 ordinal: u32,
1757 label: String,
1758 }
1759
1760 #[derive(Clone, Debug, PartialEq, Eq, Facet)]
1761 struct ListEnvelope {
1762 items: Vec<ListItem>,
1763 }
1764
1765 #[derive(Clone, Debug, Facet)]
1766 #[repr(u8)]
1767 enum DivideByZero {
1768 Zero = 0,
1769 }
1770
1771 #[derive(Clone, Debug, Facet)]
1772 #[repr(u8)]
1773 enum DefaultedMode {
1774 Auto = 0,
1775 Manual = 1,
1776 }
1777
1778 fn fixture_service() -> ServiceDescriptor {
1779 let echo = method_descriptor::<(String,), String>(
1780 "Review",
1781 "echo",
1782 &["value"],
1783 &[None],
1784 MethodDescriptorOptions {
1785 response_wire_shape: <Result<String, vox_types::VoxError> as Facet>::SHAPE,
1786 doc: None,
1787 },
1788 );
1789 let inspect = method_descriptor::<(NestedRequest,), NestedResponse>(
1790 "Review",
1791 "inspect",
1792 &["request"],
1793 &[None],
1794 MethodDescriptorOptions {
1795 response_wire_shape: <Result<NestedResponse, vox_types::VoxError> as Facet>::SHAPE,
1796 doc: None,
1797 },
1798 );
1799 let divide = method_descriptor::<(i64, i64), Result<i64, DivideByZero>>(
1800 "Review",
1801 "divide",
1802 &["dividend", "divisor"],
1803 &[None, None],
1804 MethodDescriptorOptions {
1805 response_wire_shape:
1806 <Result<i64, vox_types::VoxError<DivideByZero>> as Facet>::SHAPE,
1807 doc: None,
1808 },
1809 );
1810 let methods = Box::leak(vec![echo, inspect, divide].into_boxed_slice());
1811 ServiceDescriptor {
1812 service_name: "Review",
1813 methods,
1814 doc: None,
1815 }
1816 }
1817
1818 fn list_fixture_service() -> ServiceDescriptor {
1819 let inspect = method_descriptor::<(ListEnvelope,), ListEnvelope>(
1820 "ListFixture",
1821 "inspect",
1822 &["request"],
1823 &[None],
1824 MethodDescriptorOptions {
1825 response_wire_shape: <Result<ListEnvelope, vox_types::VoxError> as Facet>::SHAPE,
1826 doc: None,
1827 },
1828 );
1829 let echo_items = method_descriptor::<(Vec<ListItem>,), Vec<ListItem>>(
1830 "ListFixture",
1831 "echo_items",
1832 &["items"],
1833 &[None],
1834 MethodDescriptorOptions {
1835 response_wire_shape: <Result<Vec<ListItem>, vox_types::VoxError> as Facet>::SHAPE,
1836 doc: None,
1837 },
1838 );
1839 let methods = Box::leak(vec![inspect, echo_items].into_boxed_slice());
1840 ServiceDescriptor {
1841 service_name: "ListFixture",
1842 methods,
1843 doc: None,
1844 }
1845 }
1846
1847 #[test]
1848 fn java_generation_is_deterministic_and_emits_frozen_surfaces() {
1849 let service = fixture_service();
1850 let first = generate_service(&service).expect("supported service");
1851 let second = generate_service(&service).expect("repeat generation");
1852 assert_eq!(first, second);
1853 let all = first
1854 .iter()
1855 .map(|file| file.source.as_str())
1856 .collect::<String>();
1857 assert!(all.contains("public final class NestedRequest"));
1858 assert!(all.contains("public static final PhonAdapter<NestedRequest> ADAPTER"));
1859 assert!(all.contains("public interface ReviewHandler"));
1860 assert!(all.contains("CompletableFuture<String> echo("));
1861 assert!(all.contains("CompletableFuture<VoxResult<Long, DivideByZero>> divide("));
1862 assert!(all.contains("public final class ReviewDispatcher implements ServiceDispatcher"));
1863 assert!(all.contains("public final class ReviewClient"));
1864 assert!(all.contains("0x"));
1865 }
1866
1867 #[test]
1868 fn java_generation_preserves_defaulted_field_requiredness() {
1869 let inspect = method_descriptor::<(DefaultedRequest,), String>(
1870 "ReviewDefaults",
1871 "inspect",
1872 &["request"],
1873 &[None],
1874 MethodDescriptorOptions {
1875 response_wire_shape: <Result<String, vox_types::VoxError> as Facet>::SHAPE,
1876 doc: None,
1877 },
1878 );
1879 let methods = Box::leak(vec![inspect].into_boxed_slice());
1880 let service = ServiceDescriptor {
1881 service_name: "ReviewDefaults",
1882 methods,
1883 doc: None,
1884 };
1885 let file = generate_service(&service)
1886 .expect("defaulted service should generate")
1887 .into_iter()
1888 .find(|file| file.relative_path.ends_with("DefaultedRequest.java"))
1889 .expect("defaulted request should be generated");
1890
1891 let required = super::schema_ref_expr(<String as Facet>::SHAPE);
1892 let optional = super::schema_ref_expr(<u32 as Facet>::SHAPE);
1893 assert!(
1894 file.source
1895 .contains(&format!("new Schema.Field(\"required\", {required}, true)"))
1896 );
1897 assert!(file.source.contains(&format!(
1898 "new Schema.Field(\"optional_count\", {optional}, false)"
1899 )));
1900 }
1901
1902 #[test]
1903 fn java_default_expression_uses_first_unit_enum_variant() {
1904 assert_eq!(
1905 super::java_default_expression(DefaultedMode::SHAPE),
1906 "DefaultedMode.AUTO"
1907 );
1908 }
1909
1910 #[test]
1911 fn java_generation_emits_bounded_immutable_record_lists() {
1912 let files = generate_service(&list_fixture_service()).expect("record lists are supported");
1913 let source = |name: &str| {
1914 files
1915 .iter()
1916 .find(|file| file.relative_path == name)
1917 .unwrap_or_else(|| panic!("missing generated {name}"))
1918 .source
1919 .as_str()
1920 };
1921
1922 let envelope = source("ListEnvelope.java");
1923 assert!(envelope.contains("private final java.util.List<ListItem> items;"));
1924 assert!(
1925 envelope
1926 .contains("this.items = List.copyOf(Objects.requireNonNull(items, \"items\"));")
1927 );
1928 assert!(
1929 envelope
1930 .contains("PrimitiveAdapters.writeList(encoder, value.items(), ListItem.ADAPTER);")
1931 );
1932 assert!(envelope.contains("PrimitiveAdapters.readList(decoder, ListItem.ADAPTER)"));
1933 assert!(envelope.contains("SchemaClosure.fromSchemaBundleBytes"));
1934
1935 let primitive = source("PrimitiveAdapters.java");
1936 assert!(primitive.contains("encoder.writeCount(values.size())"));
1937 assert!(primitive.contains("int count = decoder.readCount()"));
1938 assert!(primitive.contains("return List.copyOf(values)"));
1939
1940 let direct_args = source("ListFixtureEchoItemsArgs.java");
1941 assert!(
1942 direct_args
1943 .contains("this.items = List.copyOf(Objects.requireNonNull(items, \"items\"));")
1944 );
1945 let descriptor = source("ListFixtureServiceDescriptor.java");
1946 assert!(descriptor.contains("PrimitiveAdapters.list(ListItem.ADAPTER"));
1947 assert!(descriptor.contains("PrimitiveAdapters.schemaClosure(0x"));
1948 assert!(descriptor.contains("HexFormat.of().parseHex"));
1949 }
1950
1951 #[test]
1952 fn java_wire_schemas_are_derived_deterministically_from_rust_shapes() {
1953 let first = generate_wire_schemas().expect("wire schemas");
1954 let second = generate_wire_schemas().expect("repeat wire schemas");
1955 assert_eq!(first, second);
1956 assert_eq!(
1957 first
1958 .iter()
1959 .map(|file| file.relative_path.as_str())
1960 .collect::<Vec<_>>(),
1961 ["HandshakeWireSchemas.java", "MessageWireSchemas.java"]
1962 );
1963 assert!(
1964 first
1965 .iter()
1966 .all(|file| file.source.contains("SCHEMA_BUNDLE"))
1967 );
1968 assert!(first[0].source.contains("HANDSHAKE_MESSAGE_SCHEMA_ID"));
1969 assert!(first[1].source.contains("MESSAGE_SCHEMA_ID"));
1970 }
1971
1972 #[test]
1973 fn java_generation_emits_typed_channel_bindings() {
1974 let stream = method_descriptor::<(Rx<String>,), String>(
1975 "StreamingService",
1976 "stream",
1977 &["input"],
1978 &[Some(<String as Facet>::SHAPE)],
1979 MethodDescriptorOptions {
1980 response_wire_shape: <Result<String, vox_types::VoxError> as Facet>::SHAPE,
1981 doc: None,
1982 },
1983 );
1984 let methods = Box::leak(vec![stream].into_boxed_slice());
1985 let service = ServiceDescriptor {
1986 service_name: "StreamingService",
1987 methods,
1988 doc: None,
1989 };
1990 let files = generate_service(&service).expect("channels are supported");
1991 let joined = files
1992 .iter()
1993 .map(|file| file.source.as_str())
1994 .collect::<String>();
1995 assert!(joined.contains("VoxRx<String> input"));
1996 assert!(joined.contains("channel.arg.0.rx.element"));
1997 assert!(joined.contains("VoxChannelArgument.rx"));
1998 }
1999
2000 #[test]
2001 fn generated_fixture_compiles_with_javac_release_17() {
2002 let dir = unique_temp_dir();
2003 let generated = dir.join("org/facet/vox/generated");
2004 fs::create_dir_all(&generated).expect("create generated package");
2005 let mut files = generate_service(&fixture_service()).expect("generate fixture");
2006 files.extend(generate_service(&list_fixture_service()).expect("generate list fixture"));
2007 files.extend(
2008 generate_service(spec_proto::terminal::terminal_service_descriptor())
2009 .expect("generate terminal fixture"),
2010 );
2011 for file in files {
2012 fs::write(generated.join(file.relative_path), file.source).expect("write Java file");
2013 }
2014 write_compile_stubs(&dir);
2015
2016 let sources = collect_java_files(&dir);
2017 let classes = dir.join("classes");
2018 fs::create_dir_all(&classes).expect("create classes");
2019 let output = Command::new("javac")
2020 .arg("--release")
2021 .arg("17")
2022 .arg("-d")
2023 .arg(&classes)
2024 .args(&sources)
2025 .output()
2026 .expect("JDK 17+ javac must be available for the Java generator gate");
2027 let compiler_close_only = output.status.code() == Some(3)
2028 && sources.iter().all(|source| {
2029 source
2030 .strip_prefix(&dir)
2031 .ok()
2032 .map(|relative| classes.join(relative).with_extension("class").is_file())
2033 .unwrap_or(false)
2034 });
2035 if !output.status.success() && !compiler_close_only {
2036 panic!(
2037 "javac --release 17 failed:\nstdout:\n{}\nstderr:\n{}",
2038 String::from_utf8_lossy(&output.stdout),
2039 String::from_utf8_lossy(&output.stderr)
2040 );
2041 }
2042 if compiler_close_only {
2043 eprintln!(
2044 "warning: javac returned 3 after emitting every top-level class; treating the compiler resource-close failure as non-fatal"
2045 );
2046 }
2047 fs::remove_dir_all(&dir).expect("remove Java compile fixture");
2048 }
2049
2050 #[test]
2051 fn record_list_wire_roundtrips_between_rust_and_generated_java() {
2052 let dir = unique_temp_dir();
2053 let generated = dir.join("org/facet/vox/generated");
2054 fs::create_dir_all(&generated).expect("create generated package");
2055 for file in generate_service(&list_fixture_service()).expect("generate list fixture") {
2056 if matches!(
2057 file.relative_path.as_str(),
2058 "PrimitiveAdapters.java" | "ListItem.java" | "ListEnvelope.java"
2059 ) {
2060 fs::write(generated.join(file.relative_path), file.source)
2061 .expect("write generated list source");
2062 }
2063 }
2064 fs::write(
2065 generated.join("ListRoundtrip.java"),
2066 r#"package org.facet.vox.generated;
2067
2068import java.nio.file.Files;
2069import java.nio.file.Path;
2070import java.util.ArrayList;
2071import java.util.Arrays;
2072import java.util.List;
2073import org.facet.phon.PhonCodec;
2074import org.facet.phon.PhonLimits;
2075
2076public final class ListRoundtrip {
2077 public static void main(String[] args) throws Exception {
2078 byte[] rustBytes = Files.readAllBytes(Path.of(args[0]));
2079 ListEnvelope decoded = PhonCodec.decode(ListEnvelope.ADAPTER, rustBytes, PhonLimits.defaults());
2080 check(decoded.items().equals(List.of(
2081 new ListItem(7L, "alpha"),
2082 new ListItem(11L, "beta"))), "Rust payload decoded in Java");
2083
2084 boolean decodedImmutable = false;
2085 try {
2086 decoded.items().add(new ListItem(13L, "forbidden"));
2087 } catch (UnsupportedOperationException expected) {
2088 decodedImmutable = true;
2089 }
2090 check(decodedImmutable, "decoded list is immutable");
2091
2092 ArrayList<ListItem> mutable = new ArrayList<>(decoded.items());
2093 ListEnvelope copied = new ListEnvelope(mutable);
2094 mutable.clear();
2095 check(copied.items().size() == 2, "constructor makes a defensive immutable copy");
2096
2097 byte[] javaBytes = PhonCodec.encode(ListEnvelope.ADAPTER, copied, PhonLimits.defaults());
2098 check(Arrays.equals(rustBytes, javaBytes), "Java encoding matches Rust encoding");
2099 Files.write(Path.of(args[1]), javaBytes);
2100 }
2101
2102 private static void check(boolean condition, String message) {
2103 if (!condition) throw new AssertionError(message);
2104 }
2105}
2106"#,
2107 )
2108 .expect("write Java roundtrip harness");
2109
2110 let value = ListEnvelope {
2111 items: vec![
2112 ListItem {
2113 ordinal: 7,
2114 label: "alpha".to_string(),
2115 },
2116 ListItem {
2117 ordinal: 11,
2118 label: "beta".to_string(),
2119 },
2120 ],
2121 };
2122 let rust_wire = phon::api::encode(&value).expect("encode Rust list fixture");
2123 let rust_wire_path = dir.join("rust.phon");
2124 let java_wire_path = dir.join("java.phon");
2125 fs::write(&rust_wire_path, &rust_wire).expect("write Rust wire fixture");
2126
2127 let facet_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
2128 let mut sources = collect_java_files(&facet_root.join("phon/java/runtime/src/main/java"));
2129 sources.extend(collect_java_files(&generated));
2130 sources.sort();
2131 let classes = dir.join("classes");
2132 fs::create_dir_all(&classes).expect("create Java classes directory");
2133 let javac = Command::new("javac")
2134 .arg("--release")
2135 .arg("17")
2136 .arg("-d")
2137 .arg(&classes)
2138 .args(&sources)
2139 .output()
2140 .expect("JDK 17+ javac must be available for the Java wire gate");
2141 let compiler_close_only = javac.status.code() == Some(3)
2142 && sources.iter().all(|source| {
2143 source
2144 .strip_prefix(&facet_root.join("phon/java/runtime/src/main/java"))
2145 .or_else(|_| source.strip_prefix(&dir))
2146 .ok()
2147 .map(|relative| classes.join(relative).with_extension("class").is_file())
2148 .unwrap_or(false)
2149 });
2150 if !javac.status.success() && !compiler_close_only {
2151 panic!(
2152 "javac --release 17 failed:\nstdout:\n{}\nstderr:\n{}",
2153 String::from_utf8_lossy(&javac.stdout),
2154 String::from_utf8_lossy(&javac.stderr)
2155 );
2156 }
2157 if compiler_close_only {
2158 eprintln!(
2159 "warning: javac returned 3 after emitting every roundtrip class; treating the compiler resource-close failure as non-fatal"
2160 );
2161 }
2162
2163 let java = Command::new("java")
2164 .arg("-ea")
2165 .arg("-cp")
2166 .arg(&classes)
2167 .arg("org.facet.vox.generated.ListRoundtrip")
2168 .arg(&rust_wire_path)
2169 .arg(&java_wire_path)
2170 .output()
2171 .expect("JDK 17+ java must be available for the Java wire gate");
2172 if !java.status.success() {
2173 panic!(
2174 "Java list roundtrip failed:\nstdout:\n{}\nstderr:\n{}",
2175 String::from_utf8_lossy(&java.stdout),
2176 String::from_utf8_lossy(&java.stderr)
2177 );
2178 }
2179
2180 let java_wire = fs::read(&java_wire_path).expect("read Java wire fixture");
2181 assert_eq!(java_wire, rust_wire);
2182 let decoded: ListEnvelope =
2183 phon::api::decode(&java_wire).expect("decode Java list fixture in Rust");
2184 assert_eq!(decoded, value);
2185 fs::remove_dir_all(&dir).expect("remove Java roundtrip fixture");
2186 }
2187
2188 fn unique_temp_dir() -> PathBuf {
2189 let nonce = SystemTime::now()
2190 .duration_since(UNIX_EPOCH)
2191 .expect("clock")
2192 .as_nanos();
2193 std::env::temp_dir().join(format!("vox-java-codegen-{}-{nonce}", std::process::id()))
2194 }
2195
2196 fn collect_java_files(root: &Path) -> Vec<PathBuf> {
2197 fn visit(path: &Path, files: &mut Vec<PathBuf>) {
2198 for entry in fs::read_dir(path).expect("read fixture dir") {
2199 let path = entry.expect("dir entry").path();
2200 if path.is_dir() {
2201 visit(&path, files);
2202 } else if path.extension().is_some_and(|ext| ext == "java") {
2203 files.push(path);
2204 }
2205 }
2206 }
2207 let mut files = Vec::new();
2208 visit(root, &mut files);
2209 files.sort();
2210 files
2211 }
2212
2213 fn write_compile_stubs(root: &Path) {
2214 let files = [
2215 (
2216 "org/facet/phon/PhonAdapter.java",
2217 "package org.facet.phon; public interface PhonAdapter<T> { SchemaClosure schema(); void encode(PhonEncoder e,T v) throws PhonException; T decode(PhonDecoder d) throws PhonException; }",
2218 ),
2219 (
2220 "org/facet/phon/SchemaClosure.java",
2221 "package org.facet.phon; import java.util.*; public final class SchemaClosure { public static SchemaClosure uncheckedOf(Schema s,Schema... r){return new SchemaClosure();} public static SchemaClosure uncheckedWithAuxiliaryClosures(Schema s,List<Schema> r,Map<String,SchemaClosure> a){return new SchemaClosure();} public static SchemaClosure uncheckedWithAuxiliaryClosures(SchemaClosure s,Map<String,SchemaClosure> a){return new SchemaClosure();} public static SchemaClosure fromSchemaBundleBytes(SchemaId id,byte[] b,PhonLimits l)throws PhonException{return new SchemaClosure();} }",
2222 ),
2223 (
2224 "org/facet/phon/SchemaId.java",
2225 "package org.facet.phon; public final class SchemaId { public static SchemaId fromLong(long v){return new SchemaId();} }",
2226 ),
2227 (
2228 "org/facet/phon/Schema.java",
2229 "package org.facet.phon; import java.util.*; public final class Schema { public enum Primitive { STRING,BOOL,I32,I64,U8,U16,U32,F32,F64,BYTES,UNIT } public static Schema primitive(Primitive p){return new Schema(null,List.of(),null);} public Schema(SchemaId i,List<Object> p,Object k){} public SchemaId id(){return null;} public static final class Ref { public static Ref concrete(SchemaId i){return new Ref();} } public static final class Field { public Field(String n,Ref r,boolean q){} } public static final class RecordKind { public RecordKind(String n,List<Field> f){} } public static final class TupleKind { public TupleKind(List<Ref> r){} } public static final class EnumKind { public EnumKind(String n,List<Variant> v){} } public static final class Variant { public Variant(String n,int i,Payload p){} } public static final class Payload { public static Payload unit(){return new Payload();} } }",
2230 ),
2231 (
2232 "org/facet/phon/PhonException.java",
2233 "package org.facet.phon; public class PhonException extends Exception { public enum Kind { ENCODE,DECODE,MALFORMED } public PhonException(Kind k,String m){super(m);} }",
2234 ),
2235 (
2236 "org/facet/phon/PhonEncoder.java",
2237 "package org.facet.phon; public final class PhonEncoder { public void writeBool(boolean v){} public void writeU8(int v){} public void writeU16(int v){} public void writeU32(long v){} public void writeU32Unaligned(long v){} public void writeI8(int v){} public void writeI16(int v){} public void writeI32(int v){} public void writeI64(long v){} public void writeF32(float v){} public void writeF64(double v){} public void writeChar(char v){} public void writeString(String v){} public void writeBytes(byte[] v){} public void writeCount(int v){} public <T> void writeAdapted(PhonAdapter<T> a,T v){} }",
2238 ),
2239 (
2240 "org/facet/phon/PhonDecoder.java",
2241 "package org.facet.phon; public final class PhonDecoder { public boolean readBool(){return false;} public int readU8(){return 0;} public int readU16(){return 0;} public long readU32(){return 0;} public long readU32Unaligned(){return 0;} public int readI8(){return 0;} public int readI16(){return 0;} public int readI32(){return 0;} public long readI64(){return 0;} public float readF32(){return 0;} public double readF64(){return 0;} public char readChar(){return 0;} public String readString(){return null;} public byte[] readBytes(){return null;} public int readCount(){return 0;} public <T> T readAdapted(PhonAdapter<T> a){return null;} }",
2242 ),
2243 (
2244 "org/facet/phon/PhonLimits.java",
2245 "package org.facet.phon; public final class PhonLimits { public static PhonLimits defaults(){return new PhonLimits();} }",
2246 ),
2247 (
2248 "org/facet/phon/PhonCodec.java",
2249 "package org.facet.phon; public final class PhonCodec { public static <T> byte[] encode(PhonAdapter<T> a,T v,PhonLimits l)throws PhonException{return new byte[0];} public static <T> T decode(PhonAdapter<T> a,byte[] b,PhonLimits l)throws PhonException{return null;} }",
2250 ),
2251 (
2252 "org/facet/vox/MethodDescriptor.java",
2253 "package org.facet.vox; import java.util.*; import org.facet.phon.*; public final class MethodDescriptor { private final long id; private final PhonAdapter<?> a,r,w; private final List<ChannelDescriptor> c; public MethodDescriptor(long i,String n,PhonAdapter<?> a,PhonAdapter<?> r){this(i,n,a,r,null,r,List.of());} public MethodDescriptor(long i,String n,PhonAdapter<?> a,PhonAdapter<?> r,PhonAdapter<?> e){this(i,n,a,r,e,r,List.of());} public MethodDescriptor(long i,String n,PhonAdapter<?> a,PhonAdapter<?> r,PhonAdapter<?> e,PhonAdapter<?> w){this(i,n,a,r,e,w,List.of());} public MethodDescriptor(long i,String n,PhonAdapter<?> a,PhonAdapter<?> r,PhonAdapter<?> e,PhonAdapter<?> w,List<ChannelDescriptor> c){id=i;this.a=a;this.r=r;this.w=w;this.c=c;} public long id(){return id;} public PhonAdapter argumentAdapter(){return a;} public PhonAdapter returnAdapter(){return r;} public PhonAdapter responseWireAdapter(){return w;} public List<ChannelDescriptor> channels(){return c;} }",
2254 ),
2255 (
2256 "org/facet/vox/ChannelDescriptor.java",
2257 "package org.facet.vox; import org.facet.phon.*; public final class ChannelDescriptor { public enum Direction { TX,RX } public ChannelDescriptor(int i,Direction d,String r,PhonAdapter<?> a){} }",
2258 ),
2259 (
2260 "org/facet/vox/VoxTx.java",
2261 "package org.facet.vox; public final class VoxTx<T> {}",
2262 ),
2263 (
2264 "org/facet/vox/VoxRx.java",
2265 "package org.facet.vox; public final class VoxRx<T> {}",
2266 ),
2267 (
2268 "org/facet/vox/VoxChannelArgument.java",
2269 "package org.facet.vox; public final class VoxChannelArgument { public static VoxChannelArgument tx(ChannelDescriptor d,VoxTx<?> v){return new VoxChannelArgument();} public static VoxChannelArgument rx(ChannelDescriptor d,VoxRx<?> v){return new VoxChannelArgument();} }",
2270 ),
2271 (
2272 "org/facet/vox/VoxChannelDecoding.java",
2273 "package org.facet.vox; public final class VoxChannelDecoding { public static byte[] encodeIndex(long i){return null;} public static <T> VoxTx<T> tx(byte[] i){return null;} public static <T> VoxRx<T> rx(byte[] i){return null;} }",
2274 ),
2275 (
2276 "org/facet/vox/VoxFutures.java",
2277 "package org.facet.vox; import java.util.concurrent.*; import java.util.function.*; public final class VoxFutures { public static <T,U> CompletableFuture<U> mapCancellable(CompletableFuture<T> f,Function<? super T,? extends U> m){return null;} }",
2278 ),
2279 (
2280 "org/facet/vox/ServiceDescriptor.java",
2281 "package org.facet.vox; import java.util.*; public final class ServiceDescriptor { public ServiceDescriptor(String n,List<MethodDescriptor> m){} }",
2282 ),
2283 (
2284 "org/facet/vox/CallContext.java",
2285 "package org.facet.vox; public final class CallContext {}",
2286 ),
2287 (
2288 "org/facet/vox/VoxResult.java",
2289 "package org.facet.vox; public final class VoxResult<O,E> { public enum Kind { SUCCESS,APPLICATION_ERROR,UNKNOWN_METHOD,INVALID_PAYLOAD,CANCELLED,CONNECTION_CLOSED,CONNECTION_SHUTDOWN,SEND_FAILED,TIMED_OUT,INDETERMINATE } public static <O,E> VoxResult<O,E> success(O v){return new VoxResult<>();} public static <E> VoxResult<Void,E> successUnit(){return new VoxResult<>();} public static <O,E> VoxResult<O,E> applicationError(E v){return new VoxResult<>();} public static <O,E> VoxResult<O,E> infrastructure(Kind k){return new VoxResult<>();} public static <O,E> VoxResult<O,E> infrastructure(Kind k,String d){return new VoxResult<>();} public Kind kind(){return Kind.SUCCESS;} public boolean isSuccess(){return true;} public boolean isInfrastructureError(){return false;} public String detail(){return null;} public O success(){return null;} public E applicationError(){return null;} }",
2290 ),
2291 (
2292 "org/facet/vox/VoxException.java",
2293 "package org.facet.vox; public class VoxException extends Exception { public VoxException(String m){super(m);} }",
2294 ),
2295 (
2296 "org/facet/vox/ServiceDispatcher.java",
2297 "package org.facet.vox; import java.util.concurrent.*; public interface ServiceDispatcher { ServiceDescriptor descriptor(); CompletableFuture<Void> dispatch(InboundCall c); }",
2298 ),
2299 (
2300 "org/facet/vox/InboundCall.java",
2301 "package org.facet.vox; import org.facet.phon.*; public final class InboundCall { public MethodDescriptor method(){return null;} public byte[] encodedArguments(){return null;} public <T>T decodeArguments(PhonAdapter<T>a){return null;} public CallContext context(){return null;} public void respond(byte[] b){} }",
2302 ),
2303 (
2304 "org/facet/vox/ServiceLane.java",
2305 "package org.facet.vox; import java.util.*; import java.util.concurrent.*; public final class ServiceLane { public CompletableFuture<byte[]> call(MethodDescriptor m,byte[] b,CallOptions o){return null;} public CompletableFuture<byte[]> call(MethodDescriptor m,byte[] b,CallOptions o,List<VoxChannelArgument> c){return null;} }",
2306 ),
2307 (
2308 "org/facet/vox/CallOptions.java",
2309 "package org.facet.vox; public final class CallOptions { public static CallOptions defaults(){return new CallOptions();} }",
2310 ),
2311 ];
2312 for (relative, source) in files {
2313 let path = root.join(relative);
2314 fs::create_dir_all(path.parent().expect("parent")).expect("create stub package");
2315 fs::write(path, source).expect("write compile stub");
2316 }
2317 }
2318}