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