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