1use crate::config::E2eConfig;
7use crate::escape::{escape_java, sanitize_filename};
8use crate::field_access::FieldResolver;
9use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup};
10use alef_core::backend::GeneratedFile;
11use alef_core::config::AlefConfig;
12use alef_core::hash::{self, CommentStyle};
13use alef_core::template_versions as tv;
14use anyhow::Result;
15use heck::{ToLowerCamelCase, ToUpperCamelCase};
16use std::collections::HashSet;
17use std::fmt::Write as FmtWrite;
18use std::path::PathBuf;
19
20use super::E2eCodegen;
21
22pub struct JavaCodegen;
24
25impl E2eCodegen for JavaCodegen {
26 fn generate(
27 &self,
28 groups: &[FixtureGroup],
29 e2e_config: &E2eConfig,
30 alef_config: &AlefConfig,
31 ) -> Result<Vec<GeneratedFile>> {
32 let lang = self.language_name();
33 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
34
35 let mut files = Vec::new();
36
37 let call = &e2e_config.call;
39 let overrides = call.overrides.get(lang);
40 let _module_path = overrides
41 .and_then(|o| o.module.as_ref())
42 .cloned()
43 .unwrap_or_else(|| call.module.clone());
44 let function_name = overrides
45 .and_then(|o| o.function.as_ref())
46 .cloned()
47 .unwrap_or_else(|| call.function.clone());
48 let class_name = overrides
49 .and_then(|o| o.class.as_ref())
50 .cloned()
51 .unwrap_or_else(|| alef_config.crate_config.name.to_upper_camel_case());
52 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
53 let result_var = &call.result_var;
54
55 let java_pkg = e2e_config.resolve_package("java");
57 let pkg_name = java_pkg
58 .as_ref()
59 .and_then(|p| p.name.as_ref())
60 .cloned()
61 .unwrap_or_else(|| alef_config.crate_config.name.clone());
62
63 let java_group_id = alef_config.java_group_id();
65 let pkg_version = alef_config.resolved_version().unwrap_or_else(|| "0.1.0".to_string());
66
67 files.push(GeneratedFile {
69 path: output_base.join("pom.xml"),
70 content: render_pom_xml(&pkg_name, &java_group_id, &pkg_version, e2e_config.dep_mode),
71 generated_header: false,
72 });
73
74 let mut test_base = output_base.join("src").join("test").join("java");
78 for segment in java_group_id.split('.') {
79 test_base = test_base.join(segment);
80 }
81 let test_base = test_base.join("e2e");
82
83 let options_type = overrides.and_then(|o| o.options_type.clone());
85 let field_resolver = FieldResolver::new(
86 &e2e_config.fields,
87 &e2e_config.fields_optional,
88 &e2e_config.result_fields,
89 &e2e_config.fields_array,
90 );
91
92 for group in groups {
93 let active: Vec<&Fixture> = group
94 .fixtures
95 .iter()
96 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
97 .collect();
98
99 if active.is_empty() {
100 continue;
101 }
102
103 let class_file_name = format!("{}Test.java", sanitize_filename(&group.category).to_upper_camel_case());
104 let content = render_test_file(
105 &group.category,
106 &active,
107 &class_name,
108 &function_name,
109 &java_group_id,
110 result_var,
111 &e2e_config.call.args,
112 options_type.as_deref(),
113 &field_resolver,
114 result_is_simple,
115 &e2e_config.fields_enum,
116 e2e_config,
117 );
118 files.push(GeneratedFile {
119 path: test_base.join(class_file_name),
120 content,
121 generated_header: true,
122 });
123 }
124
125 Ok(files)
126 }
127
128 fn language_name(&self) -> &'static str {
129 "java"
130 }
131}
132
133fn render_pom_xml(
138 pkg_name: &str,
139 java_group_id: &str,
140 pkg_version: &str,
141 dep_mode: crate::config::DependencyMode,
142) -> String {
143 let (dep_group_id, dep_artifact_id) = if let Some((g, a)) = pkg_name.split_once(':') {
145 (g, a)
146 } else {
147 (java_group_id, pkg_name)
148 };
149 let artifact_id = format!("{dep_artifact_id}-e2e-java");
150 let dep_block = match dep_mode {
151 crate::config::DependencyMode::Registry => {
152 format!(
153 r#" <dependency>
154 <groupId>{dep_group_id}</groupId>
155 <artifactId>{dep_artifact_id}</artifactId>
156 <version>{pkg_version}</version>
157 </dependency>"#
158 )
159 }
160 crate::config::DependencyMode::Local => {
161 format!(
162 r#" <dependency>
163 <groupId>{dep_group_id}</groupId>
164 <artifactId>{dep_artifact_id}</artifactId>
165 <version>{pkg_version}</version>
166 <scope>system</scope>
167 <systemPath>${{project.basedir}}/../../packages/java/target/{dep_artifact_id}-{pkg_version}.jar</systemPath>
168 </dependency>"#
169 )
170 }
171 };
172 format!(
173 r#"<?xml version="1.0" encoding="UTF-8"?>
174<project xmlns="http://maven.apache.org/POM/4.0.0"
175 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
176 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
177 <modelVersion>4.0.0</modelVersion>
178
179 <groupId>{java_group_id}</groupId>
180 <artifactId>{artifact_id}</artifactId>
181 <version>0.1.0</version>
182
183 <properties>
184 <maven.compiler.source>25</maven.compiler.source>
185 <maven.compiler.target>25</maven.compiler.target>
186 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
187 <junit.version>{junit}</junit.version>
188 </properties>
189
190 <dependencies>
191{dep_block}
192 <dependency>
193 <groupId>com.fasterxml.jackson.core</groupId>
194 <artifactId>jackson-databind</artifactId>
195 <version>{jackson}</version>
196 </dependency>
197 <dependency>
198 <groupId>com.fasterxml.jackson.datatype</groupId>
199 <artifactId>jackson-datatype-jdk8</artifactId>
200 <version>{jackson}</version>
201 </dependency>
202 <dependency>
203 <groupId>org.junit.jupiter</groupId>
204 <artifactId>junit-jupiter</artifactId>
205 <version>${{junit.version}}</version>
206 <scope>test</scope>
207 </dependency>
208 </dependencies>
209
210 <build>
211 <plugins>
212 <plugin>
213 <groupId>org.codehaus.mojo</groupId>
214 <artifactId>build-helper-maven-plugin</artifactId>
215 <version>{build_helper}</version>
216 <executions>
217 <execution>
218 <id>add-test-source</id>
219 <phase>generate-test-sources</phase>
220 <goals>
221 <goal>add-test-source</goal>
222 </goals>
223 <configuration>
224 <sources>
225 <source>src/test/java</source>
226 </sources>
227 </configuration>
228 </execution>
229 </executions>
230 </plugin>
231 <plugin>
232 <groupId>org.apache.maven.plugins</groupId>
233 <artifactId>maven-surefire-plugin</artifactId>
234 <version>{maven_surefire}</version>
235 <configuration>
236 <argLine>--enable-preview --enable-native-access=ALL-UNNAMED -Djava.library.path=../../target/release</argLine>
237 </configuration>
238 </plugin>
239 </plugins>
240 </build>
241</project>
242"#,
243 junit = tv::maven::JUNIT,
244 jackson = tv::maven::JACKSON_E2E,
245 build_helper = tv::maven::BUILD_HELPER_MAVEN_PLUGIN,
246 maven_surefire = tv::maven::MAVEN_SUREFIRE_PLUGIN_E2E,
247 )
248}
249
250#[allow(clippy::too_many_arguments)]
251fn render_test_file(
252 category: &str,
253 fixtures: &[&Fixture],
254 class_name: &str,
255 function_name: &str,
256 java_group_id: &str,
257 result_var: &str,
258 args: &[crate::config::ArgMapping],
259 options_type: Option<&str>,
260 field_resolver: &FieldResolver,
261 result_is_simple: bool,
262 enum_fields: &HashSet<String>,
263 e2e_config: &E2eConfig,
264) -> String {
265 let mut out = String::new();
266 out.push_str(&hash::header(CommentStyle::DoubleSlash));
267 let test_class_name = format!("{}Test", sanitize_filename(category).to_upper_camel_case());
268
269 let (import_path, simple_class) = if class_name.contains('.') {
272 let simple = class_name.rsplit('.').next().unwrap_or(class_name);
273 (class_name, simple)
274 } else {
275 ("", class_name)
276 };
277
278 let _ = writeln!(out, "package {java_group_id}.e2e;");
279 let _ = writeln!(out);
280
281 let lang_for_om = "java";
285 let needs_object_mapper_for_options = fixtures.iter().any(|f| {
286 let call_cfg = e2e_config.resolve_call(f.call.as_deref());
287 let eff_opts = call_cfg
288 .overrides
289 .get(lang_for_om)
290 .and_then(|o| o.options_type.as_deref())
291 .or(options_type);
292 if eff_opts.is_none() {
293 return false;
294 }
295 call_cfg.args.iter().any(|arg| {
296 if arg.arg_type != "json_object" {
297 return false;
298 }
299 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
300 let val = f.input.get(field);
301 match val {
304 None | Some(serde_json::Value::Null) => arg.optional, Some(v) => !v.is_array(), }
307 })
308 });
309 let needs_object_mapper_for_handle = fixtures.iter().any(|f| {
311 args.iter().filter(|a| a.arg_type == "handle").any(|a| {
312 let v = f.input.get(&a.field).unwrap_or(&serde_json::Value::Null);
313 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
314 })
315 });
316 let needs_object_mapper = needs_object_mapper_for_options || needs_object_mapper_for_handle;
317
318 let mut all_options_types: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
320 if let Some(t) = options_type {
321 all_options_types.insert(t.to_string());
322 }
323 for f in fixtures.iter() {
324 let call_cfg = e2e_config.resolve_call(f.call.as_deref());
325 if let Some(ov) = call_cfg.overrides.get(lang_for_om) {
326 if let Some(t) = &ov.options_type {
327 all_options_types.insert(t.clone());
328 }
329 }
330 }
331
332 let _ = writeln!(out, "import org.junit.jupiter.api.Test;");
333 let _ = writeln!(out, "import static org.junit.jupiter.api.Assertions.*;");
334 if !import_path.is_empty() {
335 let _ = writeln!(out, "import {import_path};");
336 }
337 if needs_object_mapper {
338 let _ = writeln!(out, "import com.fasterxml.jackson.databind.ObjectMapper;");
339 let _ = writeln!(out, "import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;");
340 }
341 if needs_object_mapper && !all_options_types.is_empty() {
343 let opts_pkg = if !import_path.is_empty() {
344 import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("")
345 } else {
346 ""
347 };
348 for opts_type in &all_options_types {
349 let qualified = if opts_pkg.is_empty() {
350 opts_type.clone()
351 } else {
352 format!("{opts_pkg}.{opts_type}")
353 };
354 let _ = writeln!(out, "import {qualified};");
355 }
356 }
357 if needs_object_mapper_for_handle && !import_path.is_empty() {
359 let pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
360 let _ = writeln!(out, "import {pkg}.CrawlConfig;");
361 }
362 let _ = writeln!(out);
363
364 let _ = writeln!(out, "/** E2e tests for category: {category}. */");
365 let _ = writeln!(out, "class {test_class_name} {{");
366
367 if needs_object_mapper {
368 let _ = writeln!(out);
369 let _ = writeln!(
370 out,
371 " private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());"
372 );
373 }
374
375 for fixture in fixtures {
376 render_test_method(
377 &mut out,
378 fixture,
379 simple_class,
380 function_name,
381 result_var,
382 args,
383 options_type,
384 field_resolver,
385 result_is_simple,
386 enum_fields,
387 e2e_config,
388 );
389 let _ = writeln!(out);
390 }
391
392 let _ = writeln!(out, "}}");
393 out
394}
395
396#[allow(clippy::too_many_arguments)]
397fn render_test_method(
398 out: &mut String,
399 fixture: &Fixture,
400 class_name: &str,
401 _function_name: &str,
402 _result_var: &str,
403 _args: &[crate::config::ArgMapping],
404 options_type: Option<&str>,
405 field_resolver: &FieldResolver,
406 result_is_simple: bool,
407 enum_fields: &HashSet<String>,
408 e2e_config: &E2eConfig,
409) {
410 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
412 let lang = "java";
413 let call_overrides = call_config.overrides.get(lang);
414 let effective_function_name = call_overrides
415 .and_then(|o| o.function.as_ref())
416 .cloned()
417 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
418 let effective_result_var = &call_config.result_var;
419 let effective_args = &call_config.args;
420 let function_name = effective_function_name.as_str();
421 let result_var = effective_result_var.as_str();
422 let args: &[crate::config::ArgMapping] = effective_args.as_slice();
423
424 let method_name = fixture.id.to_upper_camel_case();
425 let description = &fixture.description;
426 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
427
428 if fixture.mock_response.is_none() {
432 let _ = writeln!(out, " @Test");
433 let _ = writeln!(out, " void test{method_name}() {{");
434 let _ = writeln!(out, " // {description}");
435 let _ = writeln!(
436 out,
437 " org.junit.jupiter.api.Assumptions.assumeTrue(false, \"TODO: implement Java e2e tests via the spikard Java binding API\");"
438 );
439 let _ = writeln!(out, " }}");
440 return;
441 }
442
443 let effective_options_type: Option<String> = call_overrides
445 .and_then(|o| o.options_type.clone())
446 .or_else(|| options_type.map(|s| s.to_string()));
447 let effective_options_type = effective_options_type.as_deref();
448
449 let effective_result_is_simple = call_overrides.is_some_and(|o| o.result_is_simple) || result_is_simple;
451 let effective_result_is_bytes = call_overrides.is_some_and(|o| o.result_is_bytes);
452
453 let needs_deser = effective_options_type.is_some()
456 && args.iter().any(|arg| {
457 if arg.arg_type != "json_object" {
458 return false;
459 }
460 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
461 fixture.input.get(field).is_some_and(|v| !v.is_null() && !v.is_array())
462 });
463
464 let throws_clause = " throws Exception";
466
467 let _ = writeln!(out, " @Test");
468 let _ = writeln!(out, " void test{method_name}(){throws_clause} {{");
469 let _ = writeln!(out, " // {description}");
470
471 if let (true, Some(opts_type)) = (needs_deser, effective_options_type) {
473 for arg in args {
474 if arg.arg_type == "json_object" {
475 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
476 if let Some(val) = fixture.input.get(field) {
477 if !val.is_null() && !val.is_array() {
478 let normalized = super::normalize_json_keys_to_snake_case(val);
482 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
483 let var_name = &arg.name;
484 let _ = writeln!(
485 out,
486 " var {var_name} = MAPPER.readValue(\"{}\", {opts_type}.class);",
487 escape_java(&json_str)
488 );
489 }
490 }
491 }
492 }
493 }
494
495 let (mut setup_lines, args_str) =
496 build_args_and_setup(&fixture.input, args, class_name, effective_options_type, &fixture.id);
497
498 let mut visitor_arg = String::new();
500 if let Some(visitor_spec) = &fixture.visitor {
501 visitor_arg = build_java_visitor(&mut setup_lines, visitor_spec, class_name);
502 }
503
504 for line in &setup_lines {
505 let _ = writeln!(out, " {line}");
506 }
507
508 let final_args = if visitor_arg.is_empty() {
509 args_str
510 } else {
511 format!("{args_str}, {visitor_arg}")
512 };
513
514 if expects_error {
515 let _ = writeln!(
516 out,
517 " assertThrows(Exception.class, () -> {class_name}.{function_name}({final_args}));"
518 );
519 let _ = writeln!(out, " }}");
520 return;
521 }
522
523 let _ = writeln!(
524 out,
525 " var {result_var} = {class_name}.{function_name}({final_args});"
526 );
527
528 let needs_source_var = fixture
530 .assertions
531 .iter()
532 .any(|a| a.assertion_type == "method_result" && a.method.as_deref() == Some("run_query"));
533 if needs_source_var {
534 if let Some(source_arg) = args.iter().find(|a| a.field == "source_code") {
536 let field = source_arg.field.strip_prefix("input.").unwrap_or(&source_arg.field);
537 if let Some(val) = fixture.input.get(field) {
538 let java_val = json_to_java(val);
539 let _ = writeln!(out, " var source = {java_val}.getBytes();");
540 }
541 }
542 }
543
544 for assertion in &fixture.assertions {
545 render_assertion(
546 out,
547 assertion,
548 result_var,
549 class_name,
550 field_resolver,
551 effective_result_is_simple,
552 effective_result_is_bytes,
553 enum_fields,
554 );
555 }
556
557 let _ = writeln!(out, " }}");
558}
559
560fn build_args_and_setup(
564 input: &serde_json::Value,
565 args: &[crate::config::ArgMapping],
566 class_name: &str,
567 options_type: Option<&str>,
568 fixture_id: &str,
569) -> (Vec<String>, String) {
570 if args.is_empty() {
571 return (Vec::new(), String::new());
572 }
573
574 let mut setup_lines: Vec<String> = Vec::new();
575 let mut parts: Vec<String> = Vec::new();
576
577 for arg in args {
578 if arg.arg_type == "mock_url" {
579 setup_lines.push(format!(
580 "String {} = System.getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";",
581 arg.name,
582 ));
583 parts.push(arg.name.clone());
584 continue;
585 }
586
587 if arg.arg_type == "handle" {
588 let constructor_name = format!("create{}", arg.name.to_upper_camel_case());
590 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
591 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
592 if config_value.is_null()
593 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
594 {
595 setup_lines.push(format!("var {} = {class_name}.{constructor_name}(null);", arg.name,));
596 } else {
597 let json_str = serde_json::to_string(config_value).unwrap_or_default();
598 let name = &arg.name;
599 setup_lines.push(format!(
600 "var {name}Config = MAPPER.readValue(\"{}\", CrawlConfig.class);",
601 escape_java(&json_str),
602 ));
603 setup_lines.push(format!(
604 "var {} = {class_name}.{constructor_name}({name}Config);",
605 arg.name,
606 name = name,
607 ));
608 }
609 parts.push(arg.name.clone());
610 continue;
611 }
612
613 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
614 let val = input.get(field);
615 match val {
616 None | Some(serde_json::Value::Null) if arg.optional => {
617 if arg.arg_type == "json_object" {
621 if let Some(opts_type) = options_type {
622 parts.push(format!("MAPPER.readValue(\"{{}}\", {opts_type}.class)"));
623 } else {
624 parts.push("null".to_string());
625 }
626 } else {
627 parts.push("null".to_string());
628 }
629 }
630 None | Some(serde_json::Value::Null) => {
631 let default_val = match arg.arg_type.as_str() {
633 "string" | "file_path" => "\"\"".to_string(),
634 "int" | "integer" => "0".to_string(),
635 "float" | "number" => "0.0d".to_string(),
636 "bool" | "boolean" => "false".to_string(),
637 _ => "null".to_string(),
638 };
639 parts.push(default_val);
640 }
641 Some(v) => {
642 if arg.arg_type == "json_object" {
643 if v.is_array() {
646 let elem_type = arg.element_type.as_deref();
647 parts.push(json_to_java_typed(v, elem_type));
648 continue;
649 }
650 if options_type.is_some() {
652 parts.push(arg.name.clone());
653 continue;
654 }
655 parts.push(json_to_java(v));
656 continue;
657 }
658 if arg.arg_type == "bytes" {
660 let val = json_to_java(v);
661 parts.push(format!("{val}.getBytes()"));
662 continue;
663 }
664 if arg.arg_type == "file_path" {
666 let val = json_to_java(v);
667 parts.push(format!("java.nio.file.Path.of({val})"));
668 continue;
669 }
670 parts.push(json_to_java(v));
671 }
672 }
673 }
674
675 (setup_lines, parts.join(", "))
676}
677
678#[allow(clippy::too_many_arguments)]
679fn render_assertion(
680 out: &mut String,
681 assertion: &Assertion,
682 result_var: &str,
683 class_name: &str,
684 field_resolver: &FieldResolver,
685 result_is_simple: bool,
686 result_is_bytes: bool,
687 enum_fields: &HashSet<String>,
688) {
689 if let Some(f) = &assertion.field {
691 match f.as_str() {
692 "chunks_have_content" => {
694 let pred = format!(
695 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.content() != null && !c.content().isBlank())"
696 );
697 match assertion.assertion_type.as_str() {
698 "is_true" => {
699 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
700 }
701 "is_false" => {
702 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
703 }
704 _ => {
705 let _ = writeln!(
706 out,
707 " // skipped: unsupported assertion on synthetic field '{f}'"
708 );
709 }
710 }
711 return;
712 }
713 "chunks_have_heading_context" => {
714 let pred = format!(
715 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.metadata().headingContext().isPresent())"
716 );
717 match assertion.assertion_type.as_str() {
718 "is_true" => {
719 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
720 }
721 "is_false" => {
722 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
723 }
724 _ => {
725 let _ = writeln!(
726 out,
727 " // skipped: unsupported assertion on synthetic field '{f}'"
728 );
729 }
730 }
731 return;
732 }
733 "chunks_have_embeddings" => {
734 let pred = format!(
735 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.embedding() != null && !c.embedding().isEmpty())"
736 );
737 match assertion.assertion_type.as_str() {
738 "is_true" => {
739 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
740 }
741 "is_false" => {
742 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
743 }
744 _ => {
745 let _ = writeln!(
746 out,
747 " // skipped: unsupported assertion on synthetic field '{f}'"
748 );
749 }
750 }
751 return;
752 }
753 "first_chunk_starts_with_heading" => {
754 let pred = format!(
755 "{result_var}.chunks().orElse(java.util.List.of()).stream().findFirst().map(c -> c.metadata().headingContext().isPresent()).orElse(false)"
756 );
757 match assertion.assertion_type.as_str() {
758 "is_true" => {
759 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
760 }
761 "is_false" => {
762 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
763 }
764 _ => {
765 let _ = writeln!(
766 out,
767 " // skipped: unsupported assertion on synthetic field '{f}'"
768 );
769 }
770 }
771 return;
772 }
773 "embedding_dimensions" => {
777 let embed_list = if result_is_simple {
779 result_var.to_string()
780 } else {
781 format!("{result_var}.embeddings()")
782 };
783 let expr = format!("({embed_list}.isEmpty() ? 0 : {embed_list}.get(0).size())");
784 match assertion.assertion_type.as_str() {
785 "equals" => {
786 if let Some(val) = &assertion.value {
787 let java_val = json_to_java(val);
788 let _ = writeln!(out, " assertEquals({java_val}, {expr});");
789 }
790 }
791 "greater_than" => {
792 if let Some(val) = &assertion.value {
793 let java_val = json_to_java(val);
794 let _ = writeln!(
795 out,
796 " assertTrue({expr} > {java_val}, \"expected > {java_val}\");"
797 );
798 }
799 }
800 _ => {
801 let _ = writeln!(out, " // skipped: unsupported assertion on '{f}'");
802 }
803 }
804 return;
805 }
806 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
807 let embed_list = if result_is_simple {
809 result_var.to_string()
810 } else {
811 format!("{result_var}.embeddings()")
812 };
813 let pred = match f.as_str() {
814 "embeddings_valid" => {
815 format!("{embed_list}.stream().allMatch(e -> e != null && !e.isEmpty())")
816 }
817 "embeddings_finite" => {
818 format!("{embed_list}.stream().flatMap(java.util.Collection::stream).allMatch(Float::isFinite)")
819 }
820 "embeddings_non_zero" => {
821 format!("{embed_list}.stream().allMatch(e -> e.stream().anyMatch(v -> v != 0.0f))")
822 }
823 "embeddings_normalized" => format!(
824 "{embed_list}.stream().allMatch(e -> {{ double n = e.stream().mapToDouble(v -> v * v).sum(); return Math.abs(n - 1.0) < 1e-3; }})"
825 ),
826 _ => unreachable!(),
827 };
828 match assertion.assertion_type.as_str() {
829 "is_true" => {
830 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
831 }
832 "is_false" => {
833 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
834 }
835 _ => {
836 let _ = writeln!(out, " // skipped: unsupported assertion on '{f}'");
837 }
838 }
839 return;
840 }
841 "keywords" | "keywords_count" => {
843 let _ = writeln!(
844 out,
845 " // skipped: field '{f}' not available on Java ExtractionResult"
846 );
847 return;
848 }
849 "metadata" => {
852 match assertion.assertion_type.as_str() {
853 "not_empty" => {
854 let _ = writeln!(
855 out,
856 " assertTrue({result_var}.metadata().title().isPresent() || {result_var}.metadata().subject().isPresent() || !{result_var}.metadata().additional().isEmpty(), \"expected non-empty value\");"
857 );
858 return;
859 }
860 "is_empty" => {
861 let _ = writeln!(
862 out,
863 " assertFalse({result_var}.metadata().title().isPresent() || {result_var}.metadata().subject().isPresent() || !{result_var}.metadata().additional().isEmpty(), \"expected empty value\");"
864 );
865 return;
866 }
867 _ => {} }
869 }
870 _ => {}
871 }
872 }
873
874 if let Some(f) = &assertion.field {
876 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
877 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
878 return;
879 }
880 }
881
882 let field_is_enum = assertion
887 .field
888 .as_deref()
889 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
890
891 let field_expr = if result_is_simple {
892 result_var.to_string()
893 } else {
894 match &assertion.field {
895 Some(f) if !f.is_empty() => {
896 let accessor = field_resolver.accessor(f, "java", result_var);
897 let resolved = field_resolver.resolve(f);
898 if field_resolver.is_optional(resolved) && !field_resolver.has_map_access(f) {
901 match assertion.assertion_type.as_str() {
903 "not_empty" | "is_empty" => accessor,
906 "count_min" | "count_equals" => {
908 format!("{accessor}.orElse(java.util.List.of())")
909 }
910 "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" => {
912 if field_resolver.is_array(resolved) {
913 format!("{accessor}.orElse(java.util.List.of())")
914 } else {
915 format!("{accessor}.orElse(0L)")
916 }
917 }
918 _ if field_resolver.is_array(resolved) => {
919 format!("{accessor}.orElse(java.util.List.of())")
920 }
921 _ => format!("{accessor}.orElse(\"\")"),
922 }
923 } else {
924 accessor
925 }
926 }
927 _ => result_var.to_string(),
928 }
929 };
930
931 let string_expr = if field_is_enum {
935 format!("{field_expr}.getValue()")
936 } else {
937 field_expr.clone()
938 };
939
940 match assertion.assertion_type.as_str() {
941 "equals" => {
942 if let Some(expected) = &assertion.value {
943 let java_val = json_to_java(expected);
944 if expected.is_string() {
945 let _ = writeln!(out, " assertEquals({java_val}, {string_expr}.trim());");
946 } else {
947 let _ = writeln!(out, " assertEquals({java_val}, {field_expr});");
948 }
949 }
950 }
951 "contains" => {
952 if let Some(expected) = &assertion.value {
953 let java_val = json_to_java(expected);
954 let _ = writeln!(
955 out,
956 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
957 );
958 }
959 }
960 "contains_all" => {
961 if let Some(values) = &assertion.values {
962 for val in values {
963 let java_val = json_to_java(val);
964 let _ = writeln!(
965 out,
966 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
967 );
968 }
969 }
970 }
971 "not_contains" => {
972 if let Some(expected) = &assertion.value {
973 let java_val = json_to_java(expected);
974 let _ = writeln!(
975 out,
976 " assertFalse({string_expr}.contains({java_val}), \"expected NOT to contain: \" + {java_val});"
977 );
978 }
979 }
980 "not_empty" => {
981 let _ = writeln!(
982 out,
983 " assertFalse({field_expr}.isEmpty(), \"expected non-empty value\");"
984 );
985 }
986 "is_empty" => {
987 let _ = writeln!(
988 out,
989 " assertTrue({field_expr}.isEmpty(), \"expected empty value\");"
990 );
991 }
992 "contains_any" => {
993 if let Some(values) = &assertion.values {
994 let checks: Vec<String> = values
995 .iter()
996 .map(|v| {
997 let java_val = json_to_java(v);
998 format!("{string_expr}.contains({java_val})")
999 })
1000 .collect();
1001 let joined = checks.join(" || ");
1002 let _ = writeln!(
1003 out,
1004 " assertTrue({joined}, \"expected to contain at least one of the specified values\");"
1005 );
1006 }
1007 }
1008 "greater_than" => {
1009 if let Some(val) = &assertion.value {
1010 let java_val = json_to_java(val);
1011 let _ = writeln!(
1012 out,
1013 " assertTrue({field_expr} > {java_val}, \"expected > {java_val}\");"
1014 );
1015 }
1016 }
1017 "less_than" => {
1018 if let Some(val) = &assertion.value {
1019 let java_val = json_to_java(val);
1020 let _ = writeln!(
1021 out,
1022 " assertTrue({field_expr} < {java_val}, \"expected < {java_val}\");"
1023 );
1024 }
1025 }
1026 "greater_than_or_equal" => {
1027 if let Some(val) = &assertion.value {
1028 let java_val = json_to_java(val);
1029 let _ = writeln!(
1030 out,
1031 " assertTrue({field_expr} >= {java_val}, \"expected >= {java_val}\");"
1032 );
1033 }
1034 }
1035 "less_than_or_equal" => {
1036 if let Some(val) = &assertion.value {
1037 let java_val = json_to_java(val);
1038 let _ = writeln!(
1039 out,
1040 " assertTrue({field_expr} <= {java_val}, \"expected <= {java_val}\");"
1041 );
1042 }
1043 }
1044 "starts_with" => {
1045 if let Some(expected) = &assertion.value {
1046 let java_val = json_to_java(expected);
1047 let _ = writeln!(
1048 out,
1049 " assertTrue({string_expr}.startsWith({java_val}), \"expected to start with: \" + {java_val});"
1050 );
1051 }
1052 }
1053 "ends_with" => {
1054 if let Some(expected) = &assertion.value {
1055 let java_val = json_to_java(expected);
1056 let _ = writeln!(
1057 out,
1058 " assertTrue({string_expr}.endsWith({java_val}), \"expected to end with: \" + {java_val});"
1059 );
1060 }
1061 }
1062 "min_length" => {
1063 if let Some(val) = &assertion.value {
1064 if let Some(n) = val.as_u64() {
1065 let len_expr = if result_is_bytes {
1067 format!("{field_expr}.length")
1068 } else {
1069 format!("{field_expr}.length()")
1070 };
1071 let _ = writeln!(
1072 out,
1073 " assertTrue({len_expr} >= {n}, \"expected length >= {n}\");"
1074 );
1075 }
1076 }
1077 }
1078 "max_length" => {
1079 if let Some(val) = &assertion.value {
1080 if let Some(n) = val.as_u64() {
1081 let len_expr = if result_is_bytes {
1082 format!("{field_expr}.length")
1083 } else {
1084 format!("{field_expr}.length()")
1085 };
1086 let _ = writeln!(
1087 out,
1088 " assertTrue({len_expr} <= {n}, \"expected length <= {n}\");"
1089 );
1090 }
1091 }
1092 }
1093 "count_min" => {
1094 if let Some(val) = &assertion.value {
1095 if let Some(n) = val.as_u64() {
1096 let _ = writeln!(
1097 out,
1098 " assertTrue({field_expr}.size() >= {n}, \"expected at least {n} elements\");"
1099 );
1100 }
1101 }
1102 }
1103 "count_equals" => {
1104 if let Some(val) = &assertion.value {
1105 if let Some(n) = val.as_u64() {
1106 let _ = writeln!(
1107 out,
1108 " assertEquals({n}, {field_expr}.size(), \"expected exactly {n} elements\");"
1109 );
1110 }
1111 }
1112 }
1113 "is_true" => {
1114 let _ = writeln!(out, " assertTrue({field_expr}, \"expected true\");");
1115 }
1116 "is_false" => {
1117 let _ = writeln!(out, " assertFalse({field_expr}, \"expected false\");");
1118 }
1119 "method_result" => {
1120 if let Some(method_name) = &assertion.method {
1121 let call_expr = build_java_method_call(result_var, method_name, assertion.args.as_ref(), class_name);
1122 let check = assertion.check.as_deref().unwrap_or("is_true");
1123 let method_returns_collection =
1125 matches!(method_name.as_str(), "find_nodes_by_type" | "findNodesByType");
1126 match check {
1127 "equals" => {
1128 if let Some(val) = &assertion.value {
1129 if val.is_boolean() {
1130 if val.as_bool() == Some(true) {
1131 let _ = writeln!(out, " assertTrue({call_expr});");
1132 } else {
1133 let _ = writeln!(out, " assertFalse({call_expr});");
1134 }
1135 } else if method_returns_collection {
1136 let java_val = json_to_java(val);
1137 let _ = writeln!(out, " assertEquals({java_val}, {call_expr}.size());");
1138 } else {
1139 let java_val = json_to_java(val);
1140 let _ = writeln!(out, " assertEquals({java_val}, {call_expr});");
1141 }
1142 }
1143 }
1144 "is_true" => {
1145 let _ = writeln!(out, " assertTrue({call_expr});");
1146 }
1147 "is_false" => {
1148 let _ = writeln!(out, " assertFalse({call_expr});");
1149 }
1150 "greater_than_or_equal" => {
1151 if let Some(val) = &assertion.value {
1152 let n = val.as_u64().unwrap_or(0);
1153 let _ = writeln!(out, " assertTrue({call_expr} >= {n}, \"expected >= {n}\");");
1154 }
1155 }
1156 "count_min" => {
1157 if let Some(val) = &assertion.value {
1158 let n = val.as_u64().unwrap_or(0);
1159 let _ = writeln!(
1160 out,
1161 " assertTrue({call_expr}.size() >= {n}, \"expected at least {n} elements\");"
1162 );
1163 }
1164 }
1165 "is_error" => {
1166 let _ = writeln!(out, " assertThrows(Exception.class, () -> {{ {call_expr}; }});");
1167 }
1168 "contains" => {
1169 if let Some(val) = &assertion.value {
1170 let java_val = json_to_java(val);
1171 let _ = writeln!(
1172 out,
1173 " assertTrue({call_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
1174 );
1175 }
1176 }
1177 other_check => {
1178 panic!("Java e2e generator: unsupported method_result check type: {other_check}");
1179 }
1180 }
1181 } else {
1182 panic!("Java e2e generator: method_result assertion missing 'method' field");
1183 }
1184 }
1185 "matches_regex" => {
1186 if let Some(expected) = &assertion.value {
1187 let java_val = json_to_java(expected);
1188 let _ = writeln!(
1189 out,
1190 " assertTrue({string_expr}.matches({java_val}), \"expected value to match regex: \" + {java_val});"
1191 );
1192 }
1193 }
1194 "not_error" => {
1195 }
1197 "error" => {
1198 }
1200 other => {
1201 panic!("Java e2e generator: unsupported assertion type: {other}");
1202 }
1203 }
1204}
1205
1206fn build_java_method_call(
1210 result_var: &str,
1211 method_name: &str,
1212 args: Option<&serde_json::Value>,
1213 class_name: &str,
1214) -> String {
1215 match method_name {
1216 "root_child_count" => format!("{result_var}.rootNode().childCount()"),
1217 "root_node_type" => format!("{result_var}.rootNode().kind()"),
1218 "named_children_count" => format!("{result_var}.rootNode().namedChildCount()"),
1219 "has_error_nodes" => format!("{class_name}.treeHasErrorNodes({result_var})"),
1220 "error_count" | "tree_error_count" => format!("{class_name}.treeErrorCount({result_var})"),
1221 "tree_to_sexp" => format!("{class_name}.treeToSexp({result_var})"),
1222 "contains_node_type" => {
1223 let node_type = args
1224 .and_then(|a| a.get("node_type"))
1225 .and_then(|v| v.as_str())
1226 .unwrap_or("");
1227 format!("{class_name}.treeContainsNodeType({result_var}, \"{node_type}\")")
1228 }
1229 "find_nodes_by_type" => {
1230 let node_type = args
1231 .and_then(|a| a.get("node_type"))
1232 .and_then(|v| v.as_str())
1233 .unwrap_or("");
1234 format!("{class_name}.findNodesByType({result_var}, \"{node_type}\")")
1235 }
1236 "run_query" => {
1237 let query_source = args
1238 .and_then(|a| a.get("query_source"))
1239 .and_then(|v| v.as_str())
1240 .unwrap_or("");
1241 let language = args
1242 .and_then(|a| a.get("language"))
1243 .and_then(|v| v.as_str())
1244 .unwrap_or("");
1245 let escaped_query = escape_java(query_source);
1246 format!("{class_name}.runQuery({result_var}, \"{language}\", \"{escaped_query}\", source)")
1247 }
1248 _ => {
1249 format!("{result_var}.{}()", method_name.to_lower_camel_case())
1250 }
1251 }
1252}
1253
1254fn json_to_java(value: &serde_json::Value) -> String {
1256 json_to_java_typed(value, None)
1257}
1258
1259fn json_to_java_typed(value: &serde_json::Value, element_type: Option<&str>) -> String {
1262 match value {
1263 serde_json::Value::String(s) => format!("\"{}\"", escape_java(s)),
1264 serde_json::Value::Bool(b) => b.to_string(),
1265 serde_json::Value::Number(n) => {
1266 if n.is_f64() {
1267 match element_type {
1268 Some("f32" | "float" | "Float") => format!("{}f", n),
1269 _ => format!("{}d", n),
1270 }
1271 } else {
1272 n.to_string()
1273 }
1274 }
1275 serde_json::Value::Null => "null".to_string(),
1276 serde_json::Value::Array(arr) => {
1277 let items: Vec<String> = arr.iter().map(|v| json_to_java_typed(v, element_type)).collect();
1278 format!("java.util.List.of({})", items.join(", "))
1279 }
1280 serde_json::Value::Object(_) => {
1281 let json_str = serde_json::to_string(value).unwrap_or_default();
1282 format!("\"{}\"", escape_java(&json_str))
1283 }
1284 }
1285}
1286
1287fn build_java_visitor(
1293 setup_lines: &mut Vec<String>,
1294 visitor_spec: &crate::fixture::VisitorSpec,
1295 class_name: &str,
1296) -> String {
1297 setup_lines.push("class _TestVisitor implements TestVisitor {".to_string());
1298 for (method_name, action) in &visitor_spec.callbacks {
1299 emit_java_visitor_method(setup_lines, method_name, action, class_name);
1300 }
1301 setup_lines.push("}".to_string());
1302 setup_lines.push("var visitor = new _TestVisitor();".to_string());
1303 "visitor".to_string()
1304}
1305
1306fn emit_java_visitor_method(
1308 setup_lines: &mut Vec<String>,
1309 method_name: &str,
1310 action: &CallbackAction,
1311 _class_name: &str,
1312) {
1313 let camel_method = method_to_camel(method_name);
1314 let params = match method_name {
1315 "visit_link" => "VisitContext ctx, String href, String text, String title",
1316 "visit_image" => "VisitContext ctx, String src, String alt, String title",
1317 "visit_heading" => "VisitContext ctx, int level, String text, String id",
1318 "visit_code_block" => "VisitContext ctx, String lang, String code",
1319 "visit_code_inline"
1320 | "visit_strong"
1321 | "visit_emphasis"
1322 | "visit_strikethrough"
1323 | "visit_underline"
1324 | "visit_subscript"
1325 | "visit_superscript"
1326 | "visit_mark"
1327 | "visit_button"
1328 | "visit_summary"
1329 | "visit_figcaption"
1330 | "visit_definition_term"
1331 | "visit_definition_description" => "VisitContext ctx, String text",
1332 "visit_text" => "VisitContext ctx, String text",
1333 "visit_list_item" => "VisitContext ctx, boolean ordered, String marker, String text",
1334 "visit_blockquote" => "VisitContext ctx, String content, int depth",
1335 "visit_table_row" => "VisitContext ctx, java.util.List<String> cells, boolean isHeader",
1336 "visit_custom_element" => "VisitContext ctx, String tagName, String html",
1337 "visit_form" => "VisitContext ctx, String actionUrl, String method",
1338 "visit_input" => "VisitContext ctx, String inputType, String name, String value",
1339 "visit_audio" | "visit_video" | "visit_iframe" => "VisitContext ctx, String src",
1340 "visit_details" => "VisitContext ctx, boolean isOpen",
1341 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1342 "VisitContext ctx, String output"
1343 }
1344 "visit_list_start" => "VisitContext ctx, boolean ordered",
1345 "visit_list_end" => "VisitContext ctx, boolean ordered, String output",
1346 _ => "VisitContext ctx",
1347 };
1348
1349 setup_lines.push(format!(" @Override public VisitResult {camel_method}({params}) {{"));
1350 match action {
1351 CallbackAction::Skip => {
1352 setup_lines.push(" return VisitResult.skip();".to_string());
1353 }
1354 CallbackAction::Continue => {
1355 setup_lines.push(" return VisitResult.continue_();".to_string());
1356 }
1357 CallbackAction::PreserveHtml => {
1358 setup_lines.push(" return VisitResult.preserveHtml();".to_string());
1359 }
1360 CallbackAction::Custom { output } => {
1361 let escaped = escape_java(output);
1362 setup_lines.push(format!(" return VisitResult.custom(\"{escaped}\");"));
1363 }
1364 CallbackAction::CustomTemplate { template } => {
1365 let escaped = escape_java(template);
1366 setup_lines.push(format!(
1367 " return VisitResult.custom(String.format(\"{escaped}\"));"
1368 ));
1369 }
1370 }
1371 setup_lines.push(" }".to_string());
1372}
1373
1374fn method_to_camel(snake: &str) -> String {
1376 snake.to_lower_camel_case()
1377}