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 needs_object_mapper_for_options = options_type.is_some()
283 && fixtures.iter().any(|f| {
284 args.iter().any(|arg| {
285 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
286 arg.arg_type == "json_object" && f.input.get(field).is_some_and(|v| !v.is_null())
287 })
288 });
289 let needs_object_mapper_for_handle = fixtures.iter().any(|f| {
291 args.iter().filter(|a| a.arg_type == "handle").any(|a| {
292 let v = f.input.get(&a.field).unwrap_or(&serde_json::Value::Null);
293 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
294 })
295 });
296 let needs_object_mapper = needs_object_mapper_for_options || needs_object_mapper_for_handle;
297
298 let _ = writeln!(out, "import org.junit.jupiter.api.Test;");
299 let _ = writeln!(out, "import static org.junit.jupiter.api.Assertions.*;");
300 if !import_path.is_empty() {
301 let _ = writeln!(out, "import {import_path};");
302 }
303 if needs_object_mapper {
304 let _ = writeln!(out, "import com.fasterxml.jackson.databind.ObjectMapper;");
305 let _ = writeln!(out, "import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;");
306 }
307 if let Some(opts_type) = options_type {
309 if needs_object_mapper {
310 let opts_package = if !import_path.is_empty() {
312 let pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
313 format!("{pkg}.{opts_type}")
314 } else {
315 opts_type.to_string()
316 };
317 let _ = writeln!(out, "import {opts_package};");
318 }
319 }
320 if needs_object_mapper_for_handle && !import_path.is_empty() {
322 let pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
323 let _ = writeln!(out, "import {pkg}.CrawlConfig;");
324 }
325 let _ = writeln!(out);
326
327 let _ = writeln!(out, "/** E2e tests for category: {category}. */");
328 let _ = writeln!(out, "class {test_class_name} {{");
329
330 if needs_object_mapper {
331 let _ = writeln!(out);
332 let _ = writeln!(
333 out,
334 " private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());"
335 );
336 }
337
338 for fixture in fixtures {
339 render_test_method(
340 &mut out,
341 fixture,
342 simple_class,
343 function_name,
344 result_var,
345 args,
346 options_type,
347 field_resolver,
348 result_is_simple,
349 enum_fields,
350 e2e_config,
351 );
352 let _ = writeln!(out);
353 }
354
355 let _ = writeln!(out, "}}");
356 out
357}
358
359#[allow(clippy::too_many_arguments)]
360fn render_test_method(
361 out: &mut String,
362 fixture: &Fixture,
363 class_name: &str,
364 _function_name: &str,
365 _result_var: &str,
366 _args: &[crate::config::ArgMapping],
367 options_type: Option<&str>,
368 field_resolver: &FieldResolver,
369 result_is_simple: bool,
370 enum_fields: &HashSet<String>,
371 e2e_config: &E2eConfig,
372) {
373 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
375 let lang = "java";
376 let call_overrides = call_config.overrides.get(lang);
377 let effective_function_name = call_overrides
378 .and_then(|o| o.function.as_ref())
379 .cloned()
380 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
381 let effective_result_var = &call_config.result_var;
382 let effective_args = &call_config.args;
383 let function_name = effective_function_name.as_str();
384 let result_var = effective_result_var.as_str();
385 let args: &[crate::config::ArgMapping] = effective_args.as_slice();
386
387 let method_name = fixture.id.to_upper_camel_case();
388 let description = &fixture.description;
389 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
390
391 let needs_deser = options_type.is_some()
393 && args
394 .iter()
395 .any(|arg| arg.arg_type == "json_object" && fixture.input.get(&arg.field).is_some_and(|v| !v.is_null()));
396
397 let throws_clause = " throws Exception";
399
400 let _ = writeln!(out, " @Test");
401 let _ = writeln!(out, " void test{method_name}(){throws_clause} {{");
402 let _ = writeln!(out, " // {description}");
403
404 if let (true, Some(opts_type)) = (needs_deser, options_type) {
406 for arg in args {
407 if arg.arg_type == "json_object" {
408 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
409 if let Some(val) = fixture.input.get(field) {
410 if !val.is_null() {
411 let normalized = super::normalize_json_keys_to_snake_case(val);
415 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
416 let var_name = &arg.name;
417 let _ = writeln!(
418 out,
419 " var {var_name} = MAPPER.readValue(\"{}\", {opts_type}.class);",
420 escape_java(&json_str)
421 );
422 }
423 }
424 }
425 }
426 }
427
428 let (mut setup_lines, args_str) = build_args_and_setup(&fixture.input, args, class_name, options_type, &fixture.id);
429
430 let mut visitor_arg = String::new();
432 if let Some(visitor_spec) = &fixture.visitor {
433 visitor_arg = build_java_visitor(&mut setup_lines, visitor_spec, class_name);
434 }
435
436 for line in &setup_lines {
437 let _ = writeln!(out, " {line}");
438 }
439
440 let final_args = if visitor_arg.is_empty() {
441 args_str
442 } else {
443 format!("{args_str}, {visitor_arg}")
444 };
445
446 if expects_error {
447 let _ = writeln!(
448 out,
449 " assertThrows(Exception.class, () -> {class_name}.{function_name}({final_args}));"
450 );
451 let _ = writeln!(out, " }}");
452 return;
453 }
454
455 let _ = writeln!(
456 out,
457 " var {result_var} = {class_name}.{function_name}({final_args});"
458 );
459
460 let needs_source_var = fixture
462 .assertions
463 .iter()
464 .any(|a| a.assertion_type == "method_result" && a.method.as_deref() == Some("run_query"));
465 if needs_source_var {
466 if let Some(source_arg) = args.iter().find(|a| a.field == "source_code") {
468 let field = source_arg.field.strip_prefix("input.").unwrap_or(&source_arg.field);
469 if let Some(val) = fixture.input.get(field) {
470 let java_val = json_to_java(val);
471 let _ = writeln!(out, " var source = {java_val}.getBytes();");
472 }
473 }
474 }
475
476 for assertion in &fixture.assertions {
477 render_assertion(
478 out,
479 assertion,
480 result_var,
481 class_name,
482 field_resolver,
483 result_is_simple,
484 enum_fields,
485 );
486 }
487
488 let _ = writeln!(out, " }}");
489}
490
491fn build_args_and_setup(
495 input: &serde_json::Value,
496 args: &[crate::config::ArgMapping],
497 class_name: &str,
498 options_type: Option<&str>,
499 fixture_id: &str,
500) -> (Vec<String>, String) {
501 if args.is_empty() {
502 return (Vec::new(), String::new());
503 }
504
505 let mut setup_lines: Vec<String> = Vec::new();
506 let mut parts: Vec<String> = Vec::new();
507
508 for arg in args {
509 if arg.arg_type == "mock_url" {
510 setup_lines.push(format!(
511 "String {} = System.getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";",
512 arg.name,
513 ));
514 parts.push(arg.name.clone());
515 continue;
516 }
517
518 if arg.arg_type == "handle" {
519 let constructor_name = format!("create{}", arg.name.to_upper_camel_case());
521 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
522 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
523 if config_value.is_null()
524 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
525 {
526 setup_lines.push(format!("var {} = {class_name}.{constructor_name}(null);", arg.name,));
527 } else {
528 let json_str = serde_json::to_string(config_value).unwrap_or_default();
529 let name = &arg.name;
530 setup_lines.push(format!(
531 "var {name}Config = MAPPER.readValue(\"{}\", CrawlConfig.class);",
532 escape_java(&json_str),
533 ));
534 setup_lines.push(format!(
535 "var {} = {class_name}.{constructor_name}({name}Config);",
536 arg.name,
537 name = name,
538 ));
539 }
540 parts.push(arg.name.clone());
541 continue;
542 }
543
544 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
545 let val = input.get(field);
546 match val {
547 None | Some(serde_json::Value::Null) if arg.optional => {
548 continue;
550 }
551 None | Some(serde_json::Value::Null) => {
552 let default_val = match arg.arg_type.as_str() {
554 "string" => "\"\"".to_string(),
555 "int" | "integer" => "0".to_string(),
556 "float" | "number" => "0.0d".to_string(),
557 "bool" | "boolean" => "false".to_string(),
558 _ => "null".to_string(),
559 };
560 parts.push(default_val);
561 }
562 Some(v) => {
563 if arg.arg_type == "json_object" && options_type.is_some() {
565 parts.push(arg.name.clone());
566 continue;
567 }
568 if arg.arg_type == "bytes" {
570 let val = json_to_java(v);
571 parts.push(format!("{val}.getBytes()"));
572 continue;
573 }
574 parts.push(json_to_java(v));
575 }
576 }
577 }
578
579 (setup_lines, parts.join(", "))
580}
581
582fn render_assertion(
583 out: &mut String,
584 assertion: &Assertion,
585 result_var: &str,
586 class_name: &str,
587 field_resolver: &FieldResolver,
588 result_is_simple: bool,
589 enum_fields: &HashSet<String>,
590) {
591 if let Some(f) = &assertion.field {
593 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
594 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
595 return;
596 }
597 }
598
599 let field_is_enum = assertion
604 .field
605 .as_deref()
606 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
607
608 let field_expr = if result_is_simple {
609 result_var.to_string()
610 } else {
611 match &assertion.field {
612 Some(f) if !f.is_empty() => {
613 let accessor = field_resolver.accessor(f, "java", result_var);
614 let resolved = field_resolver.resolve(f);
615 if field_resolver.is_optional(resolved) && !field_resolver.has_map_access(f) {
618 format!("{accessor}.orElse(\"\")")
619 } else {
620 accessor
621 }
622 }
623 _ => result_var.to_string(),
624 }
625 };
626
627 let string_expr = if field_is_enum {
631 format!("{field_expr}.getValue()")
632 } else {
633 field_expr.clone()
634 };
635
636 match assertion.assertion_type.as_str() {
637 "equals" => {
638 if let Some(expected) = &assertion.value {
639 let java_val = json_to_java(expected);
640 if expected.is_string() {
641 let _ = writeln!(out, " assertEquals({java_val}, {string_expr}.trim());");
642 } else {
643 let _ = writeln!(out, " assertEquals({java_val}, {field_expr});");
644 }
645 }
646 }
647 "contains" => {
648 if let Some(expected) = &assertion.value {
649 let java_val = json_to_java(expected);
650 let _ = writeln!(
651 out,
652 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
653 );
654 }
655 }
656 "contains_all" => {
657 if let Some(values) = &assertion.values {
658 for val in values {
659 let java_val = json_to_java(val);
660 let _ = writeln!(
661 out,
662 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
663 );
664 }
665 }
666 }
667 "not_contains" => {
668 if let Some(expected) = &assertion.value {
669 let java_val = json_to_java(expected);
670 let _ = writeln!(
671 out,
672 " assertFalse({string_expr}.contains({java_val}), \"expected NOT to contain: \" + {java_val});"
673 );
674 }
675 }
676 "not_empty" => {
677 let _ = writeln!(
678 out,
679 " assertFalse({field_expr}.isEmpty(), \"expected non-empty value\");"
680 );
681 }
682 "is_empty" => {
683 let _ = writeln!(
684 out,
685 " assertTrue({field_expr}.isEmpty(), \"expected empty value\");"
686 );
687 }
688 "contains_any" => {
689 if let Some(values) = &assertion.values {
690 let checks: Vec<String> = values
691 .iter()
692 .map(|v| {
693 let java_val = json_to_java(v);
694 format!("{string_expr}.contains({java_val})")
695 })
696 .collect();
697 let joined = checks.join(" || ");
698 let _ = writeln!(
699 out,
700 " assertTrue({joined}, \"expected to contain at least one of the specified values\");"
701 );
702 }
703 }
704 "greater_than" => {
705 if let Some(val) = &assertion.value {
706 let java_val = json_to_java(val);
707 let _ = writeln!(
708 out,
709 " assertTrue({field_expr} > {java_val}, \"expected > {java_val}\");"
710 );
711 }
712 }
713 "less_than" => {
714 if let Some(val) = &assertion.value {
715 let java_val = json_to_java(val);
716 let _ = writeln!(
717 out,
718 " assertTrue({field_expr} < {java_val}, \"expected < {java_val}\");"
719 );
720 }
721 }
722 "greater_than_or_equal" => {
723 if let Some(val) = &assertion.value {
724 let java_val = json_to_java(val);
725 let _ = writeln!(
726 out,
727 " assertTrue({field_expr} >= {java_val}, \"expected >= {java_val}\");"
728 );
729 }
730 }
731 "less_than_or_equal" => {
732 if let Some(val) = &assertion.value {
733 let java_val = json_to_java(val);
734 let _ = writeln!(
735 out,
736 " assertTrue({field_expr} <= {java_val}, \"expected <= {java_val}\");"
737 );
738 }
739 }
740 "starts_with" => {
741 if let Some(expected) = &assertion.value {
742 let java_val = json_to_java(expected);
743 let _ = writeln!(
744 out,
745 " assertTrue({string_expr}.startsWith({java_val}), \"expected to start with: \" + {java_val});"
746 );
747 }
748 }
749 "ends_with" => {
750 if let Some(expected) = &assertion.value {
751 let java_val = json_to_java(expected);
752 let _ = writeln!(
753 out,
754 " assertTrue({string_expr}.endsWith({java_val}), \"expected to end with: \" + {java_val});"
755 );
756 }
757 }
758 "min_length" => {
759 if let Some(val) = &assertion.value {
760 if let Some(n) = val.as_u64() {
761 let _ = writeln!(
762 out,
763 " assertTrue({field_expr}.length() >= {n}, \"expected length >= {n}\");"
764 );
765 }
766 }
767 }
768 "max_length" => {
769 if let Some(val) = &assertion.value {
770 if let Some(n) = val.as_u64() {
771 let _ = writeln!(
772 out,
773 " assertTrue({field_expr}.length() <= {n}, \"expected length <= {n}\");"
774 );
775 }
776 }
777 }
778 "count_min" => {
779 if let Some(val) = &assertion.value {
780 if let Some(n) = val.as_u64() {
781 let _ = writeln!(
782 out,
783 " assertTrue({field_expr}.size() >= {n}, \"expected at least {n} elements\");"
784 );
785 }
786 }
787 }
788 "count_equals" => {
789 if let Some(val) = &assertion.value {
790 if let Some(n) = val.as_u64() {
791 let _ = writeln!(
792 out,
793 " assertEquals({n}, {field_expr}.size(), \"expected exactly {n} elements\");"
794 );
795 }
796 }
797 }
798 "is_true" => {
799 let _ = writeln!(out, " assertTrue({field_expr}, \"expected true\");");
800 }
801 "is_false" => {
802 let _ = writeln!(out, " assertFalse({field_expr}, \"expected false\");");
803 }
804 "method_result" => {
805 if let Some(method_name) = &assertion.method {
806 let call_expr = build_java_method_call(result_var, method_name, assertion.args.as_ref(), class_name);
807 let check = assertion.check.as_deref().unwrap_or("is_true");
808 let method_returns_collection =
810 matches!(method_name.as_str(), "find_nodes_by_type" | "findNodesByType");
811 match check {
812 "equals" => {
813 if let Some(val) = &assertion.value {
814 if val.is_boolean() {
815 if val.as_bool() == Some(true) {
816 let _ = writeln!(out, " assertTrue({call_expr});");
817 } else {
818 let _ = writeln!(out, " assertFalse({call_expr});");
819 }
820 } else if method_returns_collection {
821 let java_val = json_to_java(val);
822 let _ = writeln!(out, " assertEquals({java_val}, {call_expr}.size());");
823 } else {
824 let java_val = json_to_java(val);
825 let _ = writeln!(out, " assertEquals({java_val}, {call_expr});");
826 }
827 }
828 }
829 "is_true" => {
830 let _ = writeln!(out, " assertTrue({call_expr});");
831 }
832 "is_false" => {
833 let _ = writeln!(out, " assertFalse({call_expr});");
834 }
835 "greater_than_or_equal" => {
836 if let Some(val) = &assertion.value {
837 let n = val.as_u64().unwrap_or(0);
838 let _ = writeln!(out, " assertTrue({call_expr} >= {n}, \"expected >= {n}\");");
839 }
840 }
841 "count_min" => {
842 if let Some(val) = &assertion.value {
843 let n = val.as_u64().unwrap_or(0);
844 let _ = writeln!(
845 out,
846 " assertTrue({call_expr}.size() >= {n}, \"expected at least {n} elements\");"
847 );
848 }
849 }
850 "is_error" => {
851 let _ = writeln!(out, " assertThrows(Exception.class, () -> {{ {call_expr}; }});");
852 }
853 "contains" => {
854 if let Some(val) = &assertion.value {
855 let java_val = json_to_java(val);
856 let _ = writeln!(
857 out,
858 " assertTrue({call_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
859 );
860 }
861 }
862 other_check => {
863 panic!("Java e2e generator: unsupported method_result check type: {other_check}");
864 }
865 }
866 } else {
867 panic!("Java e2e generator: method_result assertion missing 'method' field");
868 }
869 }
870 "matches_regex" => {
871 if let Some(expected) = &assertion.value {
872 let java_val = json_to_java(expected);
873 let _ = writeln!(
874 out,
875 " assertTrue({string_expr}.matches({java_val}), \"expected value to match regex: \" + {java_val});"
876 );
877 }
878 }
879 "not_error" => {
880 }
882 "error" => {
883 }
885 other => {
886 panic!("Java e2e generator: unsupported assertion type: {other}");
887 }
888 }
889}
890
891fn build_java_method_call(
895 result_var: &str,
896 method_name: &str,
897 args: Option<&serde_json::Value>,
898 class_name: &str,
899) -> String {
900 match method_name {
901 "root_child_count" => format!("{result_var}.rootNode().childCount()"),
902 "root_node_type" => format!("{result_var}.rootNode().kind()"),
903 "named_children_count" => format!("{result_var}.rootNode().namedChildCount()"),
904 "has_error_nodes" => format!("{class_name}.treeHasErrorNodes({result_var})"),
905 "error_count" | "tree_error_count" => format!("{class_name}.treeErrorCount({result_var})"),
906 "tree_to_sexp" => format!("{class_name}.treeToSexp({result_var})"),
907 "contains_node_type" => {
908 let node_type = args
909 .and_then(|a| a.get("node_type"))
910 .and_then(|v| v.as_str())
911 .unwrap_or("");
912 format!("{class_name}.treeContainsNodeType({result_var}, \"{node_type}\")")
913 }
914 "find_nodes_by_type" => {
915 let node_type = args
916 .and_then(|a| a.get("node_type"))
917 .and_then(|v| v.as_str())
918 .unwrap_or("");
919 format!("{class_name}.findNodesByType({result_var}, \"{node_type}\")")
920 }
921 "run_query" => {
922 let query_source = args
923 .and_then(|a| a.get("query_source"))
924 .and_then(|v| v.as_str())
925 .unwrap_or("");
926 let language = args
927 .and_then(|a| a.get("language"))
928 .and_then(|v| v.as_str())
929 .unwrap_or("");
930 let escaped_query = escape_java(query_source);
931 format!("{class_name}.runQuery({result_var}, \"{language}\", \"{escaped_query}\", source)")
932 }
933 _ => {
934 format!("{result_var}.{}()", method_name.to_lower_camel_case())
935 }
936 }
937}
938
939fn json_to_java(value: &serde_json::Value) -> String {
941 match value {
942 serde_json::Value::String(s) => format!("\"{}\"", escape_java(s)),
943 serde_json::Value::Bool(b) => b.to_string(),
944 serde_json::Value::Number(n) => {
945 if n.is_f64() {
946 format!("{}d", n)
947 } else {
948 n.to_string()
949 }
950 }
951 serde_json::Value::Null => "null".to_string(),
952 serde_json::Value::Array(arr) => {
953 let items: Vec<String> = arr.iter().map(json_to_java).collect();
954 format!("java.util.List.of({})", items.join(", "))
955 }
956 serde_json::Value::Object(_) => {
957 let json_str = serde_json::to_string(value).unwrap_or_default();
958 format!("\"{}\"", escape_java(&json_str))
959 }
960 }
961}
962
963fn build_java_visitor(
969 setup_lines: &mut Vec<String>,
970 visitor_spec: &crate::fixture::VisitorSpec,
971 class_name: &str,
972) -> String {
973 setup_lines.push("class _TestVisitor implements TestVisitor {".to_string());
974 for (method_name, action) in &visitor_spec.callbacks {
975 emit_java_visitor_method(setup_lines, method_name, action, class_name);
976 }
977 setup_lines.push("}".to_string());
978 setup_lines.push("var visitor = new _TestVisitor();".to_string());
979 "visitor".to_string()
980}
981
982fn emit_java_visitor_method(
984 setup_lines: &mut Vec<String>,
985 method_name: &str,
986 action: &CallbackAction,
987 _class_name: &str,
988) {
989 let camel_method = method_to_camel(method_name);
990 let params = match method_name {
991 "visit_link" => "VisitContext ctx, String href, String text, String title",
992 "visit_image" => "VisitContext ctx, String src, String alt, String title",
993 "visit_heading" => "VisitContext ctx, int level, String text, String id",
994 "visit_code_block" => "VisitContext ctx, String lang, String code",
995 "visit_code_inline"
996 | "visit_strong"
997 | "visit_emphasis"
998 | "visit_strikethrough"
999 | "visit_underline"
1000 | "visit_subscript"
1001 | "visit_superscript"
1002 | "visit_mark"
1003 | "visit_button"
1004 | "visit_summary"
1005 | "visit_figcaption"
1006 | "visit_definition_term"
1007 | "visit_definition_description" => "VisitContext ctx, String text",
1008 "visit_text" => "VisitContext ctx, String text",
1009 "visit_list_item" => "VisitContext ctx, boolean ordered, String marker, String text",
1010 "visit_blockquote" => "VisitContext ctx, String content, int depth",
1011 "visit_table_row" => "VisitContext ctx, java.util.List<String> cells, boolean isHeader",
1012 "visit_custom_element" => "VisitContext ctx, String tagName, String html",
1013 "visit_form" => "VisitContext ctx, String actionUrl, String method",
1014 "visit_input" => "VisitContext ctx, String inputType, String name, String value",
1015 "visit_audio" | "visit_video" | "visit_iframe" => "VisitContext ctx, String src",
1016 "visit_details" => "VisitContext ctx, boolean isOpen",
1017 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1018 "VisitContext ctx, String output"
1019 }
1020 "visit_list_start" => "VisitContext ctx, boolean ordered",
1021 "visit_list_end" => "VisitContext ctx, boolean ordered, String output",
1022 _ => "VisitContext ctx",
1023 };
1024
1025 setup_lines.push(format!(" @Override public VisitResult {camel_method}({params}) {{"));
1026 match action {
1027 CallbackAction::Skip => {
1028 setup_lines.push(" return VisitResult.skip();".to_string());
1029 }
1030 CallbackAction::Continue => {
1031 setup_lines.push(" return VisitResult.continue_();".to_string());
1032 }
1033 CallbackAction::PreserveHtml => {
1034 setup_lines.push(" return VisitResult.preserveHtml();".to_string());
1035 }
1036 CallbackAction::Custom { output } => {
1037 let escaped = escape_java(output);
1038 setup_lines.push(format!(" return VisitResult.custom(\"{escaped}\");"));
1039 }
1040 CallbackAction::CustomTemplate { template } => {
1041 let escaped = escape_java(template);
1042 setup_lines.push(format!(
1043 " return VisitResult.custom(String.format(\"{escaped}\"));"
1044 ));
1045 }
1046 }
1047 setup_lines.push(" }".to_string());
1048}
1049
1050fn method_to_camel(snake: &str) -> String {
1052 snake.to_lower_camel_case()
1053}