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 test_base = output_base
76 .join("src")
77 .join("test")
78 .join("java")
79 .join("dev")
80 .join("kreuzberg")
81 .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 result_var,
110 &e2e_config.call.args,
111 options_type.as_deref(),
112 &field_resolver,
113 result_is_simple,
114 &e2e_config.fields_enum,
115 e2e_config,
116 );
117 files.push(GeneratedFile {
118 path: test_base.join(class_file_name),
119 content,
120 generated_header: true,
121 });
122 }
123
124 Ok(files)
125 }
126
127 fn language_name(&self) -> &'static str {
128 "java"
129 }
130}
131
132fn render_pom_xml(
137 pkg_name: &str,
138 java_group_id: &str,
139 pkg_version: &str,
140 dep_mode: crate::config::DependencyMode,
141) -> String {
142 let (dep_group_id, dep_artifact_id) = if let Some((g, a)) = pkg_name.split_once(':') {
144 (g, a)
145 } else {
146 (java_group_id, pkg_name)
147 };
148 let artifact_id = format!("{dep_artifact_id}-e2e-java");
149 let dep_block = match dep_mode {
150 crate::config::DependencyMode::Registry => {
151 format!(
152 r#" <dependency>
153 <groupId>{dep_group_id}</groupId>
154 <artifactId>{dep_artifact_id}</artifactId>
155 <version>{pkg_version}</version>
156 </dependency>"#
157 )
158 }
159 crate::config::DependencyMode::Local => {
160 format!(
161 r#" <dependency>
162 <groupId>{dep_group_id}</groupId>
163 <artifactId>{dep_artifact_id}</artifactId>
164 <version>{pkg_version}</version>
165 <scope>system</scope>
166 <systemPath>${{project.basedir}}/../../packages/java/target/{dep_artifact_id}-{pkg_version}.jar</systemPath>
167 </dependency>"#
168 )
169 }
170 };
171 format!(
172 r#"<?xml version="1.0" encoding="UTF-8"?>
173<project xmlns="http://maven.apache.org/POM/4.0.0"
174 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
175 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
176 <modelVersion>4.0.0</modelVersion>
177
178 <groupId>dev.kreuzberg</groupId>
179 <artifactId>{artifact_id}</artifactId>
180 <version>0.1.0</version>
181
182 <properties>
183 <maven.compiler.source>25</maven.compiler.source>
184 <maven.compiler.target>25</maven.compiler.target>
185 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
186 <junit.version>{junit}</junit.version>
187 </properties>
188
189 <dependencies>
190{dep_block}
191 <dependency>
192 <groupId>com.fasterxml.jackson.core</groupId>
193 <artifactId>jackson-databind</artifactId>
194 <version>{jackson}</version>
195 </dependency>
196 <dependency>
197 <groupId>com.fasterxml.jackson.datatype</groupId>
198 <artifactId>jackson-datatype-jdk8</artifactId>
199 <version>{jackson}</version>
200 </dependency>
201 <dependency>
202 <groupId>org.junit.jupiter</groupId>
203 <artifactId>junit-jupiter</artifactId>
204 <version>${{junit.version}}</version>
205 <scope>test</scope>
206 </dependency>
207 </dependencies>
208
209 <build>
210 <plugins>
211 <plugin>
212 <groupId>org.codehaus.mojo</groupId>
213 <artifactId>build-helper-maven-plugin</artifactId>
214 <version>{build_helper}</version>
215 <executions>
216 <execution>
217 <id>add-test-source</id>
218 <phase>generate-test-sources</phase>
219 <goals>
220 <goal>add-test-source</goal>
221 </goals>
222 <configuration>
223 <sources>
224 <source>src/test/java</source>
225 </sources>
226 </configuration>
227 </execution>
228 </executions>
229 </plugin>
230 <plugin>
231 <groupId>org.apache.maven.plugins</groupId>
232 <artifactId>maven-surefire-plugin</artifactId>
233 <version>{maven_surefire}</version>
234 <configuration>
235 <argLine>--enable-preview --enable-native-access=ALL-UNNAMED -Djava.library.path=../../target/release</argLine>
236 </configuration>
237 </plugin>
238 </plugins>
239 </build>
240</project>
241"#,
242 junit = tv::maven::JUNIT,
243 jackson = tv::maven::JACKSON_E2E,
244 build_helper = tv::maven::BUILD_HELPER_MAVEN_PLUGIN,
245 maven_surefire = tv::maven::MAVEN_SUREFIRE_PLUGIN_E2E,
246 )
247}
248
249#[allow(clippy::too_many_arguments)]
250fn render_test_file(
251 category: &str,
252 fixtures: &[&Fixture],
253 class_name: &str,
254 function_name: &str,
255 result_var: &str,
256 args: &[crate::config::ArgMapping],
257 options_type: Option<&str>,
258 field_resolver: &FieldResolver,
259 result_is_simple: bool,
260 enum_fields: &HashSet<String>,
261 e2e_config: &E2eConfig,
262) -> String {
263 let mut out = String::new();
264 out.push_str(&hash::header(CommentStyle::DoubleSlash));
265 let test_class_name = format!("{}Test", sanitize_filename(category).to_upper_camel_case());
266
267 let (import_path, simple_class) = if class_name.contains('.') {
270 let simple = class_name.rsplit('.').next().unwrap_or(class_name);
271 (class_name, simple)
272 } else {
273 ("", class_name)
274 };
275
276 let _ = writeln!(out, "package dev.kreuzberg.e2e;");
277 let _ = writeln!(out);
278
279 let needs_object_mapper_for_options = options_type.is_some()
281 && fixtures.iter().any(|f| {
282 args.iter().any(|arg| {
283 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
284 arg.arg_type == "json_object" && f.input.get(field).is_some_and(|v| !v.is_null())
285 })
286 });
287 let needs_object_mapper_for_handle = fixtures.iter().any(|f| {
289 args.iter().filter(|a| a.arg_type == "handle").any(|a| {
290 let v = f.input.get(&a.field).unwrap_or(&serde_json::Value::Null);
291 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
292 })
293 });
294 let needs_object_mapper = needs_object_mapper_for_options || needs_object_mapper_for_handle;
295
296 let _ = writeln!(out, "import org.junit.jupiter.api.Test;");
297 let _ = writeln!(out, "import static org.junit.jupiter.api.Assertions.*;");
298 if !import_path.is_empty() {
299 let _ = writeln!(out, "import {import_path};");
300 }
301 if needs_object_mapper {
302 let _ = writeln!(out, "import com.fasterxml.jackson.databind.ObjectMapper;");
303 let _ = writeln!(out, "import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;");
304 }
305 if let Some(opts_type) = options_type {
307 if needs_object_mapper {
308 let opts_package = if !import_path.is_empty() {
310 let pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
311 format!("{pkg}.{opts_type}")
312 } else {
313 opts_type.to_string()
314 };
315 let _ = writeln!(out, "import {opts_package};");
316 }
317 }
318 if needs_object_mapper_for_handle && !import_path.is_empty() {
320 let pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
321 let _ = writeln!(out, "import {pkg}.CrawlConfig;");
322 }
323 let _ = writeln!(out);
324
325 let _ = writeln!(out, "/** E2e tests for category: {category}. */");
326 let _ = writeln!(out, "class {test_class_name} {{");
327
328 if needs_object_mapper {
329 let _ = writeln!(out);
330 let _ = writeln!(
331 out,
332 " private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());"
333 );
334 }
335
336 for fixture in fixtures {
337 render_test_method(
338 &mut out,
339 fixture,
340 simple_class,
341 function_name,
342 result_var,
343 args,
344 options_type,
345 field_resolver,
346 result_is_simple,
347 enum_fields,
348 e2e_config,
349 );
350 let _ = writeln!(out);
351 }
352
353 let _ = writeln!(out, "}}");
354 out
355}
356
357#[allow(clippy::too_many_arguments)]
358fn render_test_method(
359 out: &mut String,
360 fixture: &Fixture,
361 class_name: &str,
362 _function_name: &str,
363 _result_var: &str,
364 _args: &[crate::config::ArgMapping],
365 options_type: Option<&str>,
366 field_resolver: &FieldResolver,
367 result_is_simple: bool,
368 enum_fields: &HashSet<String>,
369 e2e_config: &E2eConfig,
370) {
371 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
373 let lang = "java";
374 let call_overrides = call_config.overrides.get(lang);
375 let effective_function_name = call_overrides
376 .and_then(|o| o.function.as_ref())
377 .cloned()
378 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
379 let effective_result_var = &call_config.result_var;
380 let effective_args = &call_config.args;
381 let function_name = effective_function_name.as_str();
382 let result_var = effective_result_var.as_str();
383 let args: &[crate::config::ArgMapping] = effective_args.as_slice();
384
385 let method_name = fixture.id.to_upper_camel_case();
386 let description = &fixture.description;
387 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
388
389 let needs_deser = options_type.is_some()
391 && args
392 .iter()
393 .any(|arg| arg.arg_type == "json_object" && fixture.input.get(&arg.field).is_some_and(|v| !v.is_null()));
394
395 let throws_clause = " throws Exception";
397
398 let _ = writeln!(out, " @Test");
399 let _ = writeln!(out, " void test{method_name}(){throws_clause} {{");
400 let _ = writeln!(out, " // {description}");
401
402 if let (true, Some(opts_type)) = (needs_deser, options_type) {
404 for arg in args {
405 if arg.arg_type == "json_object" {
406 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
407 if let Some(val) = fixture.input.get(field) {
408 if !val.is_null() {
409 let normalized = super::normalize_json_keys_to_snake_case(val);
413 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
414 let var_name = &arg.name;
415 let _ = writeln!(
416 out,
417 " var {var_name} = MAPPER.readValue(\"{}\", {opts_type}.class);",
418 escape_java(&json_str)
419 );
420 }
421 }
422 }
423 }
424 }
425
426 let (mut setup_lines, args_str) = build_args_and_setup(&fixture.input, args, class_name, options_type, &fixture.id);
427
428 let mut visitor_arg = String::new();
430 if let Some(visitor_spec) = &fixture.visitor {
431 visitor_arg = build_java_visitor(&mut setup_lines, visitor_spec, class_name);
432 }
433
434 for line in &setup_lines {
435 let _ = writeln!(out, " {line}");
436 }
437
438 let final_args = if visitor_arg.is_empty() {
439 args_str
440 } else {
441 format!("{args_str}, {visitor_arg}")
442 };
443
444 if expects_error {
445 let _ = writeln!(
446 out,
447 " assertThrows(Exception.class, () -> {class_name}.{function_name}({final_args}));"
448 );
449 let _ = writeln!(out, " }}");
450 return;
451 }
452
453 let _ = writeln!(
454 out,
455 " var {result_var} = {class_name}.{function_name}({final_args});"
456 );
457
458 let needs_source_var = fixture
460 .assertions
461 .iter()
462 .any(|a| a.assertion_type == "method_result" && a.method.as_deref() == Some("run_query"));
463 if needs_source_var {
464 if let Some(source_arg) = args.iter().find(|a| a.field == "source_code") {
466 let field = source_arg.field.strip_prefix("input.").unwrap_or(&source_arg.field);
467 if let Some(val) = fixture.input.get(field) {
468 let java_val = json_to_java(val);
469 let _ = writeln!(out, " var source = {java_val}.getBytes();");
470 }
471 }
472 }
473
474 for assertion in &fixture.assertions {
475 render_assertion(
476 out,
477 assertion,
478 result_var,
479 class_name,
480 field_resolver,
481 result_is_simple,
482 enum_fields,
483 );
484 }
485
486 let _ = writeln!(out, " }}");
487}
488
489fn build_args_and_setup(
493 input: &serde_json::Value,
494 args: &[crate::config::ArgMapping],
495 class_name: &str,
496 options_type: Option<&str>,
497 fixture_id: &str,
498) -> (Vec<String>, String) {
499 if args.is_empty() {
500 return (Vec::new(), String::new());
501 }
502
503 let mut setup_lines: Vec<String> = Vec::new();
504 let mut parts: Vec<String> = Vec::new();
505
506 for arg in args {
507 if arg.arg_type == "mock_url" {
508 setup_lines.push(format!(
509 "String {} = System.getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";",
510 arg.name,
511 ));
512 parts.push(arg.name.clone());
513 continue;
514 }
515
516 if arg.arg_type == "handle" {
517 let constructor_name = format!("create{}", arg.name.to_upper_camel_case());
519 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
520 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
521 if config_value.is_null()
522 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
523 {
524 setup_lines.push(format!("var {} = {class_name}.{constructor_name}(null);", arg.name,));
525 } else {
526 let json_str = serde_json::to_string(config_value).unwrap_or_default();
527 let name = &arg.name;
528 setup_lines.push(format!(
529 "var {name}Config = MAPPER.readValue(\"{}\", CrawlConfig.class);",
530 escape_java(&json_str),
531 ));
532 setup_lines.push(format!(
533 "var {} = {class_name}.{constructor_name}({name}Config);",
534 arg.name,
535 name = name,
536 ));
537 }
538 parts.push(arg.name.clone());
539 continue;
540 }
541
542 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
543 let val = input.get(field);
544 match val {
545 None | Some(serde_json::Value::Null) if arg.optional => {
546 continue;
548 }
549 None | Some(serde_json::Value::Null) => {
550 let default_val = match arg.arg_type.as_str() {
552 "string" => "\"\"".to_string(),
553 "int" | "integer" => "0".to_string(),
554 "float" | "number" => "0.0d".to_string(),
555 "bool" | "boolean" => "false".to_string(),
556 _ => "null".to_string(),
557 };
558 parts.push(default_val);
559 }
560 Some(v) => {
561 if arg.arg_type == "json_object" && options_type.is_some() {
563 parts.push(arg.name.clone());
564 continue;
565 }
566 if arg.arg_type == "bytes" {
568 let val = json_to_java(v);
569 parts.push(format!("{val}.getBytes()"));
570 continue;
571 }
572 parts.push(json_to_java(v));
573 }
574 }
575 }
576
577 (setup_lines, parts.join(", "))
578}
579
580fn render_assertion(
581 out: &mut String,
582 assertion: &Assertion,
583 result_var: &str,
584 class_name: &str,
585 field_resolver: &FieldResolver,
586 result_is_simple: bool,
587 enum_fields: &HashSet<String>,
588) {
589 if let Some(f) = &assertion.field {
591 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
592 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
593 return;
594 }
595 }
596
597 let field_is_enum = assertion
602 .field
603 .as_deref()
604 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
605
606 let field_expr = if result_is_simple {
607 result_var.to_string()
608 } else {
609 match &assertion.field {
610 Some(f) if !f.is_empty() => {
611 let accessor = field_resolver.accessor(f, "java", result_var);
612 let resolved = field_resolver.resolve(f);
613 if field_resolver.is_optional(resolved) && !field_resolver.has_map_access(f) {
616 format!("{accessor}.orElse(\"\")")
617 } else {
618 accessor
619 }
620 }
621 _ => result_var.to_string(),
622 }
623 };
624
625 let string_expr = if field_is_enum {
629 format!("{field_expr}.getValue()")
630 } else {
631 field_expr.clone()
632 };
633
634 match assertion.assertion_type.as_str() {
635 "equals" => {
636 if let Some(expected) = &assertion.value {
637 let java_val = json_to_java(expected);
638 if expected.is_string() {
639 let _ = writeln!(out, " assertEquals({java_val}, {string_expr}.trim());");
640 } else {
641 let _ = writeln!(out, " assertEquals({java_val}, {field_expr});");
642 }
643 }
644 }
645 "contains" => {
646 if let Some(expected) = &assertion.value {
647 let java_val = json_to_java(expected);
648 let _ = writeln!(
649 out,
650 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
651 );
652 }
653 }
654 "contains_all" => {
655 if let Some(values) = &assertion.values {
656 for val in values {
657 let java_val = json_to_java(val);
658 let _ = writeln!(
659 out,
660 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
661 );
662 }
663 }
664 }
665 "not_contains" => {
666 if let Some(expected) = &assertion.value {
667 let java_val = json_to_java(expected);
668 let _ = writeln!(
669 out,
670 " assertFalse({string_expr}.contains({java_val}), \"expected NOT to contain: \" + {java_val});"
671 );
672 }
673 }
674 "not_empty" => {
675 let _ = writeln!(
676 out,
677 " assertFalse({field_expr}.isEmpty(), \"expected non-empty value\");"
678 );
679 }
680 "is_empty" => {
681 let _ = writeln!(
682 out,
683 " assertTrue({field_expr}.isEmpty(), \"expected empty value\");"
684 );
685 }
686 "contains_any" => {
687 if let Some(values) = &assertion.values {
688 let checks: Vec<String> = values
689 .iter()
690 .map(|v| {
691 let java_val = json_to_java(v);
692 format!("{string_expr}.contains({java_val})")
693 })
694 .collect();
695 let joined = checks.join(" || ");
696 let _ = writeln!(
697 out,
698 " assertTrue({joined}, \"expected to contain at least one of the specified values\");"
699 );
700 }
701 }
702 "greater_than" => {
703 if let Some(val) = &assertion.value {
704 let java_val = json_to_java(val);
705 let _ = writeln!(
706 out,
707 " assertTrue({field_expr} > {java_val}, \"expected > {java_val}\");"
708 );
709 }
710 }
711 "less_than" => {
712 if let Some(val) = &assertion.value {
713 let java_val = json_to_java(val);
714 let _ = writeln!(
715 out,
716 " assertTrue({field_expr} < {java_val}, \"expected < {java_val}\");"
717 );
718 }
719 }
720 "greater_than_or_equal" => {
721 if let Some(val) = &assertion.value {
722 let java_val = json_to_java(val);
723 let _ = writeln!(
724 out,
725 " assertTrue({field_expr} >= {java_val}, \"expected >= {java_val}\");"
726 );
727 }
728 }
729 "less_than_or_equal" => {
730 if let Some(val) = &assertion.value {
731 let java_val = json_to_java(val);
732 let _ = writeln!(
733 out,
734 " assertTrue({field_expr} <= {java_val}, \"expected <= {java_val}\");"
735 );
736 }
737 }
738 "starts_with" => {
739 if let Some(expected) = &assertion.value {
740 let java_val = json_to_java(expected);
741 let _ = writeln!(
742 out,
743 " assertTrue({string_expr}.startsWith({java_val}), \"expected to start with: \" + {java_val});"
744 );
745 }
746 }
747 "ends_with" => {
748 if let Some(expected) = &assertion.value {
749 let java_val = json_to_java(expected);
750 let _ = writeln!(
751 out,
752 " assertTrue({string_expr}.endsWith({java_val}), \"expected to end with: \" + {java_val});"
753 );
754 }
755 }
756 "min_length" => {
757 if let Some(val) = &assertion.value {
758 if let Some(n) = val.as_u64() {
759 let _ = writeln!(
760 out,
761 " assertTrue({field_expr}.length() >= {n}, \"expected length >= {n}\");"
762 );
763 }
764 }
765 }
766 "max_length" => {
767 if let Some(val) = &assertion.value {
768 if let Some(n) = val.as_u64() {
769 let _ = writeln!(
770 out,
771 " assertTrue({field_expr}.length() <= {n}, \"expected length <= {n}\");"
772 );
773 }
774 }
775 }
776 "count_min" => {
777 if let Some(val) = &assertion.value {
778 if let Some(n) = val.as_u64() {
779 let _ = writeln!(
780 out,
781 " assertTrue({field_expr}.size() >= {n}, \"expected at least {n} elements\");"
782 );
783 }
784 }
785 }
786 "count_equals" => {
787 if let Some(val) = &assertion.value {
788 if let Some(n) = val.as_u64() {
789 let _ = writeln!(
790 out,
791 " assertEquals({n}, {field_expr}.size(), \"expected exactly {n} elements\");"
792 );
793 }
794 }
795 }
796 "is_true" => {
797 let _ = writeln!(out, " assertTrue({field_expr}, \"expected true\");");
798 }
799 "is_false" => {
800 let _ = writeln!(out, " assertFalse({field_expr}, \"expected false\");");
801 }
802 "method_result" => {
803 if let Some(method_name) = &assertion.method {
804 let call_expr = build_java_method_call(result_var, method_name, assertion.args.as_ref(), class_name);
805 let check = assertion.check.as_deref().unwrap_or("is_true");
806 let method_returns_collection =
808 matches!(method_name.as_str(), "find_nodes_by_type" | "findNodesByType");
809 match check {
810 "equals" => {
811 if let Some(val) = &assertion.value {
812 if val.is_boolean() {
813 if val.as_bool() == Some(true) {
814 let _ = writeln!(out, " assertTrue({call_expr});");
815 } else {
816 let _ = writeln!(out, " assertFalse({call_expr});");
817 }
818 } else if method_returns_collection {
819 let java_val = json_to_java(val);
820 let _ = writeln!(out, " assertEquals({java_val}, {call_expr}.size());");
821 } else {
822 let java_val = json_to_java(val);
823 let _ = writeln!(out, " assertEquals({java_val}, {call_expr});");
824 }
825 }
826 }
827 "is_true" => {
828 let _ = writeln!(out, " assertTrue({call_expr});");
829 }
830 "is_false" => {
831 let _ = writeln!(out, " assertFalse({call_expr});");
832 }
833 "greater_than_or_equal" => {
834 if let Some(val) = &assertion.value {
835 let n = val.as_u64().unwrap_or(0);
836 let _ = writeln!(out, " assertTrue({call_expr} >= {n}, \"expected >= {n}\");");
837 }
838 }
839 "count_min" => {
840 if let Some(val) = &assertion.value {
841 let n = val.as_u64().unwrap_or(0);
842 let _ = writeln!(
843 out,
844 " assertTrue({call_expr}.size() >= {n}, \"expected at least {n} elements\");"
845 );
846 }
847 }
848 "is_error" => {
849 let _ = writeln!(out, " assertThrows(Exception.class, () -> {{ {call_expr}; }});");
850 }
851 "contains" => {
852 if let Some(val) = &assertion.value {
853 let java_val = json_to_java(val);
854 let _ = writeln!(
855 out,
856 " assertTrue({call_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
857 );
858 }
859 }
860 other_check => {
861 panic!("Java e2e generator: unsupported method_result check type: {other_check}");
862 }
863 }
864 } else {
865 panic!("Java e2e generator: method_result assertion missing 'method' field");
866 }
867 }
868 "matches_regex" => {
869 if let Some(expected) = &assertion.value {
870 let java_val = json_to_java(expected);
871 let _ = writeln!(
872 out,
873 " assertTrue({string_expr}.matches({java_val}), \"expected value to match regex: \" + {java_val});"
874 );
875 }
876 }
877 "not_error" => {
878 }
880 "error" => {
881 }
883 other => {
884 panic!("Java e2e generator: unsupported assertion type: {other}");
885 }
886 }
887}
888
889fn build_java_method_call(
893 result_var: &str,
894 method_name: &str,
895 args: Option<&serde_json::Value>,
896 class_name: &str,
897) -> String {
898 match method_name {
899 "root_child_count" => format!("{result_var}.rootNode().childCount()"),
900 "root_node_type" => format!("{result_var}.rootNode().kind()"),
901 "named_children_count" => format!("{result_var}.rootNode().namedChildCount()"),
902 "has_error_nodes" => format!("{class_name}.treeHasErrorNodes({result_var})"),
903 "error_count" | "tree_error_count" => format!("{class_name}.treeErrorCount({result_var})"),
904 "tree_to_sexp" => format!("{class_name}.treeToSexp({result_var})"),
905 "contains_node_type" => {
906 let node_type = args
907 .and_then(|a| a.get("node_type"))
908 .and_then(|v| v.as_str())
909 .unwrap_or("");
910 format!("{class_name}.treeContainsNodeType({result_var}, \"{node_type}\")")
911 }
912 "find_nodes_by_type" => {
913 let node_type = args
914 .and_then(|a| a.get("node_type"))
915 .and_then(|v| v.as_str())
916 .unwrap_or("");
917 format!("{class_name}.findNodesByType({result_var}, \"{node_type}\")")
918 }
919 "run_query" => {
920 let query_source = args
921 .and_then(|a| a.get("query_source"))
922 .and_then(|v| v.as_str())
923 .unwrap_or("");
924 let language = args
925 .and_then(|a| a.get("language"))
926 .and_then(|v| v.as_str())
927 .unwrap_or("");
928 let escaped_query = escape_java(query_source);
929 format!("{class_name}.runQuery({result_var}, \"{language}\", \"{escaped_query}\", source)")
930 }
931 _ => {
932 format!("{result_var}.{}()", method_name.to_lower_camel_case())
933 }
934 }
935}
936
937fn json_to_java(value: &serde_json::Value) -> String {
939 match value {
940 serde_json::Value::String(s) => format!("\"{}\"", escape_java(s)),
941 serde_json::Value::Bool(b) => b.to_string(),
942 serde_json::Value::Number(n) => {
943 if n.is_f64() {
944 format!("{}d", n)
945 } else {
946 n.to_string()
947 }
948 }
949 serde_json::Value::Null => "null".to_string(),
950 serde_json::Value::Array(arr) => {
951 let items: Vec<String> = arr.iter().map(json_to_java).collect();
952 format!("java.util.List.of({})", items.join(", "))
953 }
954 serde_json::Value::Object(_) => {
955 let json_str = serde_json::to_string(value).unwrap_or_default();
956 format!("\"{}\"", escape_java(&json_str))
957 }
958 }
959}
960
961fn build_java_visitor(
967 setup_lines: &mut Vec<String>,
968 visitor_spec: &crate::fixture::VisitorSpec,
969 class_name: &str,
970) -> String {
971 setup_lines.push("class _TestVisitor implements TestVisitor {".to_string());
972 for (method_name, action) in &visitor_spec.callbacks {
973 emit_java_visitor_method(setup_lines, method_name, action, class_name);
974 }
975 setup_lines.push("}".to_string());
976 setup_lines.push("var visitor = new _TestVisitor();".to_string());
977 "visitor".to_string()
978}
979
980fn emit_java_visitor_method(
982 setup_lines: &mut Vec<String>,
983 method_name: &str,
984 action: &CallbackAction,
985 _class_name: &str,
986) {
987 let camel_method = method_to_camel(method_name);
988 let params = match method_name {
989 "visit_link" => "VisitContext ctx, String href, String text, String title",
990 "visit_image" => "VisitContext ctx, String src, String alt, String title",
991 "visit_heading" => "VisitContext ctx, int level, String text, String id",
992 "visit_code_block" => "VisitContext ctx, String lang, String code",
993 "visit_code_inline"
994 | "visit_strong"
995 | "visit_emphasis"
996 | "visit_strikethrough"
997 | "visit_underline"
998 | "visit_subscript"
999 | "visit_superscript"
1000 | "visit_mark"
1001 | "visit_button"
1002 | "visit_summary"
1003 | "visit_figcaption"
1004 | "visit_definition_term"
1005 | "visit_definition_description" => "VisitContext ctx, String text",
1006 "visit_text" => "VisitContext ctx, String text",
1007 "visit_list_item" => "VisitContext ctx, boolean ordered, String marker, String text",
1008 "visit_blockquote" => "VisitContext ctx, String content, int depth",
1009 "visit_table_row" => "VisitContext ctx, java.util.List<String> cells, boolean isHeader",
1010 "visit_custom_element" => "VisitContext ctx, String tagName, String html",
1011 "visit_form" => "VisitContext ctx, String actionUrl, String method",
1012 "visit_input" => "VisitContext ctx, String inputType, String name, String value",
1013 "visit_audio" | "visit_video" | "visit_iframe" => "VisitContext ctx, String src",
1014 "visit_details" => "VisitContext ctx, boolean isOpen",
1015 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1016 "VisitContext ctx, String output"
1017 }
1018 "visit_list_start" => "VisitContext ctx, boolean ordered",
1019 "visit_list_end" => "VisitContext ctx, boolean ordered, String output",
1020 _ => "VisitContext ctx",
1021 };
1022
1023 setup_lines.push(format!(" @Override public VisitResult {camel_method}({params}) {{"));
1024 match action {
1025 CallbackAction::Skip => {
1026 setup_lines.push(" return VisitResult.skip();".to_string());
1027 }
1028 CallbackAction::Continue => {
1029 setup_lines.push(" return VisitResult.continue_();".to_string());
1030 }
1031 CallbackAction::PreserveHtml => {
1032 setup_lines.push(" return VisitResult.preserveHtml();".to_string());
1033 }
1034 CallbackAction::Custom { output } => {
1035 let escaped = escape_java(output);
1036 setup_lines.push(format!(" return VisitResult.custom(\"{escaped}\");"));
1037 }
1038 CallbackAction::CustomTemplate { template } => {
1039 let escaped = escape_java(template);
1040 setup_lines.push(format!(
1041 " return VisitResult.custom(String.format(\"{escaped}\"));"
1042 ));
1043 }
1044 }
1045 setup_lines.push(" }".to_string());
1046}
1047
1048fn method_to_camel(snake: &str) -> String {
1050 snake.to_lower_camel_case()
1051}