1use crate::config::E2eConfig;
7use crate::escape::{escape_java, sanitize_filename};
8use crate::field_access::FieldResolver;
9use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup, HttpFixture};
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;
21use super::client;
22
23pub struct JavaCodegen;
25
26impl E2eCodegen for JavaCodegen {
27 fn generate(
28 &self,
29 groups: &[FixtureGroup],
30 e2e_config: &E2eConfig,
31 alef_config: &AlefConfig,
32 ) -> Result<Vec<GeneratedFile>> {
33 let lang = self.language_name();
34 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
35
36 let mut files = Vec::new();
37
38 let call = &e2e_config.call;
40 let overrides = call.overrides.get(lang);
41 let _module_path = overrides
42 .and_then(|o| o.module.as_ref())
43 .cloned()
44 .unwrap_or_else(|| call.module.clone());
45 let function_name = overrides
46 .and_then(|o| o.function.as_ref())
47 .cloned()
48 .unwrap_or_else(|| call.function.clone());
49 let class_name = overrides
50 .and_then(|o| o.class.as_ref())
51 .cloned()
52 .unwrap_or_else(|| alef_config.crate_config.name.to_upper_camel_case());
53 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
54 let result_var = &call.result_var;
55
56 let java_pkg = e2e_config.resolve_package("java");
58 let pkg_name = java_pkg
59 .as_ref()
60 .and_then(|p| p.name.as_ref())
61 .cloned()
62 .unwrap_or_else(|| alef_config.crate_config.name.clone());
63
64 let java_group_id = alef_config.java_group_id();
66 let pkg_version = alef_config.resolved_version().unwrap_or_else(|| "0.1.0".to_string());
67
68 files.push(GeneratedFile {
70 path: output_base.join("pom.xml"),
71 content: render_pom_xml(&pkg_name, &java_group_id, &pkg_version, e2e_config.dep_mode),
72 generated_header: false,
73 });
74
75 let mut test_base = output_base.join("src").join("test").join("java");
79 for segment in java_group_id.split('.') {
80 test_base = test_base.join(segment);
81 }
82 let test_base = test_base.join("e2e");
83
84 let options_type = overrides.and_then(|o| o.options_type.clone());
86 let field_resolver = FieldResolver::new(
87 &e2e_config.fields,
88 &e2e_config.fields_optional,
89 &e2e_config.result_fields,
90 &e2e_config.fields_array,
91 );
92
93 for group in groups {
94 let active: Vec<&Fixture> = group
95 .fixtures
96 .iter()
97 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
98 .collect();
99
100 if active.is_empty() {
101 continue;
102 }
103
104 let class_file_name = format!("{}Test.java", sanitize_filename(&group.category).to_upper_camel_case());
105 let content = render_test_file(
106 &group.category,
107 &active,
108 &class_name,
109 &function_name,
110 &java_group_id,
111 result_var,
112 &e2e_config.call.args,
113 options_type.as_deref(),
114 &field_resolver,
115 result_is_simple,
116 &e2e_config.fields_enum,
117 e2e_config,
118 );
119 files.push(GeneratedFile {
120 path: test_base.join(class_file_name),
121 content,
122 generated_header: true,
123 });
124 }
125
126 Ok(files)
127 }
128
129 fn language_name(&self) -> &'static str {
130 "java"
131 }
132}
133
134fn render_pom_xml(
139 pkg_name: &str,
140 java_group_id: &str,
141 pkg_version: &str,
142 dep_mode: crate::config::DependencyMode,
143) -> String {
144 let (dep_group_id, dep_artifact_id) = if let Some((g, a)) = pkg_name.split_once(':') {
146 (g, a)
147 } else {
148 (java_group_id, pkg_name)
149 };
150 let artifact_id = format!("{dep_artifact_id}-e2e-java");
151 let dep_block = match dep_mode {
152 crate::config::DependencyMode::Registry => {
153 format!(
154 r#" <dependency>
155 <groupId>{dep_group_id}</groupId>
156 <artifactId>{dep_artifact_id}</artifactId>
157 <version>{pkg_version}</version>
158 </dependency>"#
159 )
160 }
161 crate::config::DependencyMode::Local => {
162 format!(
163 r#" <dependency>
164 <groupId>{dep_group_id}</groupId>
165 <artifactId>{dep_artifact_id}</artifactId>
166 <version>{pkg_version}</version>
167 <scope>system</scope>
168 <systemPath>${{project.basedir}}/../../packages/java/target/{dep_artifact_id}-{pkg_version}.jar</systemPath>
169 </dependency>"#
170 )
171 }
172 };
173 format!(
174 r#"<?xml version="1.0" encoding="UTF-8"?>
175<project xmlns="http://maven.apache.org/POM/4.0.0"
176 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
177 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
178 <modelVersion>4.0.0</modelVersion>
179
180 <groupId>{java_group_id}</groupId>
181 <artifactId>{artifact_id}</artifactId>
182 <version>0.1.0</version>
183
184 <properties>
185 <maven.compiler.source>25</maven.compiler.source>
186 <maven.compiler.target>25</maven.compiler.target>
187 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
188 <junit.version>{junit}</junit.version>
189 </properties>
190
191 <dependencies>
192{dep_block}
193 <dependency>
194 <groupId>com.fasterxml.jackson.core</groupId>
195 <artifactId>jackson-databind</artifactId>
196 <version>{jackson}</version>
197 </dependency>
198 <dependency>
199 <groupId>com.fasterxml.jackson.datatype</groupId>
200 <artifactId>jackson-datatype-jdk8</artifactId>
201 <version>{jackson}</version>
202 </dependency>
203 <dependency>
204 <groupId>org.junit.jupiter</groupId>
205 <artifactId>junit-jupiter</artifactId>
206 <version>${{junit.version}}</version>
207 <scope>test</scope>
208 </dependency>
209 </dependencies>
210
211 <build>
212 <plugins>
213 <plugin>
214 <groupId>org.codehaus.mojo</groupId>
215 <artifactId>build-helper-maven-plugin</artifactId>
216 <version>{build_helper}</version>
217 <executions>
218 <execution>
219 <id>add-test-source</id>
220 <phase>generate-test-sources</phase>
221 <goals>
222 <goal>add-test-source</goal>
223 </goals>
224 <configuration>
225 <sources>
226 <source>src/test/java</source>
227 </sources>
228 </configuration>
229 </execution>
230 </executions>
231 </plugin>
232 <plugin>
233 <groupId>org.apache.maven.plugins</groupId>
234 <artifactId>maven-surefire-plugin</artifactId>
235 <version>{maven_surefire}</version>
236 <configuration>
237 <argLine>--enable-preview --enable-native-access=ALL-UNNAMED -Djava.library.path=${{project.basedir}}/../../target/release</argLine>
238 <workingDirectory>${{project.basedir}}/../../test_documents</workingDirectory>
239 </configuration>
240 </plugin>
241 </plugins>
242 </build>
243</project>
244"#,
245 junit = tv::maven::JUNIT,
246 jackson = tv::maven::JACKSON_E2E,
247 build_helper = tv::maven::BUILD_HELPER_MAVEN_PLUGIN,
248 maven_surefire = tv::maven::MAVEN_SUREFIRE_PLUGIN_E2E,
249 )
250}
251
252#[allow(clippy::too_many_arguments)]
253fn render_test_file(
254 category: &str,
255 fixtures: &[&Fixture],
256 class_name: &str,
257 function_name: &str,
258 java_group_id: &str,
259 result_var: &str,
260 args: &[crate::config::ArgMapping],
261 options_type: Option<&str>,
262 field_resolver: &FieldResolver,
263 result_is_simple: bool,
264 enum_fields: &HashSet<String>,
265 e2e_config: &E2eConfig,
266) -> String {
267 let mut out = String::new();
268 out.push_str(&hash::header(CommentStyle::DoubleSlash));
269 let test_class_name = format!("{}Test", sanitize_filename(category).to_upper_camel_case());
270
271 let (import_path, simple_class) = if class_name.contains('.') {
274 let simple = class_name.rsplit('.').next().unwrap_or(class_name);
275 (class_name, simple)
276 } else {
277 ("", class_name)
278 };
279
280 let _ = writeln!(out, "package {java_group_id}.e2e;");
281 let _ = writeln!(out);
282
283 let lang_for_om = "java";
287 let needs_object_mapper_for_options = fixtures.iter().any(|f| {
288 let call_cfg = e2e_config.resolve_call(f.call.as_deref());
289 let eff_opts = call_cfg
290 .overrides
291 .get(lang_for_om)
292 .and_then(|o| o.options_type.as_deref())
293 .or(options_type);
294 if eff_opts.is_none() {
295 return false;
296 }
297 call_cfg.args.iter().any(|arg| {
298 if arg.arg_type != "json_object" {
299 return false;
300 }
301 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
302 let val = f.input.get(field);
303 match val {
306 None | Some(serde_json::Value::Null) => arg.optional, Some(v) => !v.is_array(), }
309 })
310 });
311 let needs_object_mapper_for_handle = fixtures.iter().any(|f| {
313 args.iter().filter(|a| a.arg_type == "handle").any(|a| {
314 let v = f.input.get(&a.field).unwrap_or(&serde_json::Value::Null);
315 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
316 })
317 });
318 let has_http_fixtures = fixtures.iter().any(|f| f.http.is_some());
320 let needs_object_mapper = needs_object_mapper_for_options || needs_object_mapper_for_handle || has_http_fixtures;
321
322 let mut all_options_types: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
324 if let Some(t) = options_type {
325 all_options_types.insert(t.to_string());
326 }
327 for f in fixtures.iter() {
328 let call_cfg = e2e_config.resolve_call(f.call.as_deref());
329 if let Some(ov) = call_cfg.overrides.get(lang_for_om) {
330 if let Some(t) = &ov.options_type {
331 all_options_types.insert(t.clone());
332 }
333 }
334 }
335
336 let _ = writeln!(out, "import org.junit.jupiter.api.Test;");
337 let _ = writeln!(out, "import static org.junit.jupiter.api.Assertions.*;");
338 if !import_path.is_empty() {
339 let _ = writeln!(out, "import {import_path};");
340 }
341 if needs_object_mapper {
342 let _ = writeln!(out, "import com.fasterxml.jackson.databind.ObjectMapper;");
343 let _ = writeln!(out, "import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;");
344 }
345 if needs_object_mapper && !all_options_types.is_empty() {
347 let opts_pkg = if !import_path.is_empty() {
348 import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("")
349 } else {
350 ""
351 };
352 for opts_type in &all_options_types {
353 let qualified = if opts_pkg.is_empty() {
354 opts_type.clone()
355 } else {
356 format!("{opts_pkg}.{opts_type}")
357 };
358 let _ = writeln!(out, "import {qualified};");
359 }
360 }
361 if needs_object_mapper_for_handle && !import_path.is_empty() {
363 let pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
364 let _ = writeln!(out, "import {pkg}.CrawlConfig;");
365 }
366 let has_visitor_fixtures = fixtures.iter().any(|f| f.visitor.is_some());
368 if has_visitor_fixtures && !import_path.is_empty() {
369 let binding_pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
370 if !binding_pkg.is_empty() {
371 let _ = writeln!(out, "import {binding_pkg}.TestVisitor;");
372 let _ = writeln!(out, "import {binding_pkg}.VisitContext;");
373 let _ = writeln!(out, "import {binding_pkg}.VisitResult;");
374 }
375 }
376 let _ = writeln!(out);
377
378 let _ = writeln!(out, "/** E2e tests for category: {category}. */");
379 let _ = writeln!(out, "class {test_class_name} {{");
380
381 if needs_object_mapper {
382 let _ = writeln!(out);
383 let _ = writeln!(
384 out,
385 " private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());"
386 );
387 }
388
389 for fixture in fixtures {
390 render_test_method(
391 &mut out,
392 fixture,
393 simple_class,
394 function_name,
395 result_var,
396 args,
397 options_type,
398 field_resolver,
399 result_is_simple,
400 enum_fields,
401 e2e_config,
402 );
403 let _ = writeln!(out);
404 }
405
406 let _ = writeln!(out, "}}");
407 out
408}
409
410struct JavaTestClientRenderer;
418
419impl client::TestClientRenderer for JavaTestClientRenderer {
420 fn language_name(&self) -> &'static str {
421 "java"
422 }
423
424 fn sanitize_test_name(&self, id: &str) -> String {
428 id.to_upper_camel_case()
429 }
430
431 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
437 let _ = writeln!(out, " @Test");
438 if let Some(reason) = skip_reason {
439 let escaped_reason = escape_java(reason);
440 let _ = writeln!(out, " void test{fn_name}() {{");
441 let _ = writeln!(out, " // {description}");
442 let _ = writeln!(
443 out,
444 " org.junit.jupiter.api.Assumptions.assumeTrue(false, \"{escaped_reason}\");"
445 );
446 } else {
447 let _ = writeln!(out, " void test{fn_name}() throws Exception {{");
448 let _ = writeln!(out, " // {description}");
449 let _ = writeln!(out, " String baseUrl = System.getenv(\"MOCK_SERVER_URL\");");
451 let _ = writeln!(out, " if (baseUrl == null) baseUrl = \"http://localhost:8080\";");
452 }
453 }
454
455 fn render_test_close(&self, out: &mut String) {
457 let _ = writeln!(out, " }}");
458 }
459
460 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
466 const JAVA_RESTRICTED_HEADERS: &[&str] = &["connection", "content-length", "expect", "host", "upgrade"];
468
469 let method = ctx.method.to_uppercase();
470
471 let path = if ctx.query_params.is_empty() {
473 ctx.path.to_string()
474 } else {
475 let pairs: Vec<String> = ctx
476 .query_params
477 .iter()
478 .map(|(k, v)| {
479 let val_str = match v {
480 serde_json::Value::String(s) => s.clone(),
481 other => other.to_string(),
482 };
483 format!("{}={}", k, escape_java(&val_str))
484 })
485 .collect();
486 format!("{}?{}", ctx.path, pairs.join("&"))
487 };
488 let _ = writeln!(
489 out,
490 " java.net.URI uri = java.net.URI.create(baseUrl + \"{path}\");"
491 );
492
493 let body_publisher = if let Some(body) = ctx.body {
494 let json = serde_json::to_string(body).unwrap_or_default();
495 let escaped = escape_java(&json);
496 format!("java.net.http.HttpRequest.BodyPublishers.ofString(\"{escaped}\")")
497 } else {
498 "java.net.http.HttpRequest.BodyPublishers.noBody()".to_string()
499 };
500
501 let _ = writeln!(out, " var builder = java.net.http.HttpRequest.newBuilder(uri)");
502 let _ = writeln!(out, " .method(\"{method}\", {body_publisher});");
503
504 if ctx.body.is_some() {
506 let content_type = ctx.content_type.unwrap_or("application/json");
507 if !ctx.headers.keys().any(|k| k.to_lowercase() == "content-type") {
509 let _ = writeln!(
510 out,
511 " builder = builder.header(\"Content-Type\", \"{content_type}\");"
512 );
513 }
514 }
515
516 for (name, value) in ctx.headers {
518 if JAVA_RESTRICTED_HEADERS.contains(&name.to_lowercase().as_str()) {
519 continue;
520 }
521 let escaped_name = escape_java(name);
522 let escaped_value = escape_java(value);
523 let _ = writeln!(
524 out,
525 " builder = builder.header(\"{escaped_name}\", \"{escaped_value}\");"
526 );
527 }
528
529 if !ctx.cookies.is_empty() {
531 let cookie_str: Vec<String> = ctx.cookies.iter().map(|(k, v)| format!("{k}={v}")).collect();
532 let cookie_header = escape_java(&cookie_str.join("; "));
533 let _ = writeln!(
534 out,
535 " builder = builder.header(\"Cookie\", \"{cookie_header}\");"
536 );
537 }
538
539 let response_var = ctx.response_var;
540 let _ = writeln!(
541 out,
542 " var {response_var} = java.net.http.HttpClient.newHttpClient()"
543 );
544 let _ = writeln!(
545 out,
546 " .send(builder.build(), java.net.http.HttpResponse.BodyHandlers.ofString());"
547 );
548 }
549
550 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
552 let _ = writeln!(
553 out,
554 " assertEquals({status}, {response_var}.statusCode(), \"status code mismatch\");"
555 );
556 }
557
558 fn render_assert_header(&self, out: &mut String, response_var: &str, name: &str, expected: &str) {
562 let escaped_name = escape_java(name);
563 match expected {
564 "<<present>>" => {
565 let _ = writeln!(
566 out,
567 " assertTrue({response_var}.headers().firstValue(\"{escaped_name}\").isPresent(), \"header {escaped_name} should be present\");"
568 );
569 }
570 "<<absent>>" => {
571 let _ = writeln!(
572 out,
573 " assertTrue({response_var}.headers().firstValue(\"{escaped_name}\").isEmpty(), \"header {escaped_name} should be absent\");"
574 );
575 }
576 "<<uuid>>" => {
577 let _ = writeln!(
578 out,
579 " assertTrue({response_var}.headers().firstValue(\"{escaped_name}\").orElse(\"\").matches(\"[0-9a-fA-F]{{8}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{12}}\"), \"header {escaped_name} should be a UUID\");"
580 );
581 }
582 literal => {
583 let escaped_value = escape_java(literal);
584 let _ = writeln!(
585 out,
586 " assertTrue({response_var}.headers().firstValue(\"{escaped_name}\").orElse(\"\").contains(\"{escaped_value}\"), \"header {escaped_name} mismatch\");"
587 );
588 }
589 }
590 }
591
592 fn render_assert_json_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
594 match expected {
595 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
596 let json_str = serde_json::to_string(expected).unwrap_or_default();
597 let escaped = escape_java(&json_str);
598 let _ = writeln!(out, " var bodyJson = MAPPER.readTree({response_var}.body());");
599 let _ = writeln!(out, " var expectedJson = MAPPER.readTree(\"{escaped}\");");
600 let _ = writeln!(out, " assertEquals(expectedJson, bodyJson, \"body mismatch\");");
601 }
602 serde_json::Value::String(s) => {
603 let escaped = escape_java(s);
604 let _ = writeln!(
605 out,
606 " assertEquals(\"{escaped}\", {response_var}.body().trim(), \"body mismatch\");"
607 );
608 }
609 other => {
610 let escaped = escape_java(&other.to_string());
611 let _ = writeln!(
612 out,
613 " assertEquals(\"{escaped}\", {response_var}.body().trim(), \"body mismatch\");"
614 );
615 }
616 }
617 }
618
619 fn render_assert_partial_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
621 if let Some(obj) = expected.as_object() {
622 let _ = writeln!(out, " var partialJson = MAPPER.readTree({response_var}.body());");
623 for (key, val) in obj {
624 let escaped_key = escape_java(key);
625 let json_str = serde_json::to_string(val).unwrap_or_default();
626 let escaped_val = escape_java(&json_str);
627 let _ = writeln!(
628 out,
629 " assertEquals(MAPPER.readTree(\"{escaped_val}\"), partialJson.get(\"{escaped_key}\"), \"body field '{escaped_key}' mismatch\");"
630 );
631 }
632 }
633 }
634
635 fn render_assert_validation_errors(
637 &self,
638 out: &mut String,
639 response_var: &str,
640 errors: &[crate::fixture::ValidationErrorExpectation],
641 ) {
642 let _ = writeln!(out, " var veBody = {response_var}.body();");
643 for err in errors {
644 let escaped_msg = escape_java(&err.msg);
645 let _ = writeln!(
646 out,
647 " assertTrue(veBody.contains(\"{escaped_msg}\"), \"expected validation error message: {escaped_msg}\");"
648 );
649 }
650 }
651}
652
653fn render_http_test_method(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
660 if http.expected_response.status_code == 101 {
663 let method_name = fixture.id.to_upper_camel_case();
664 let description = &fixture.description;
665 let _ = writeln!(out, " @Test");
666 let _ = writeln!(out, " void test{method_name}() {{");
667 let _ = writeln!(out, " // {description}");
668 let _ = writeln!(
669 out,
670 " org.junit.jupiter.api.Assumptions.assumeTrue(false, \"Skipped: Java HttpClient cannot handle 101 Switching Protocols responses\");"
671 );
672 let _ = writeln!(out, " }}");
673 return;
674 }
675
676 client::http_call::render_http_test(out, &JavaTestClientRenderer, fixture);
677}
678
679#[allow(clippy::too_many_arguments)]
680fn render_test_method(
681 out: &mut String,
682 fixture: &Fixture,
683 class_name: &str,
684 _function_name: &str,
685 _result_var: &str,
686 _args: &[crate::config::ArgMapping],
687 options_type: Option<&str>,
688 field_resolver: &FieldResolver,
689 result_is_simple: bool,
690 enum_fields: &HashSet<String>,
691 e2e_config: &E2eConfig,
692) {
693 if let Some(http) = &fixture.http {
695 render_http_test_method(out, fixture, http);
696 return;
697 }
698
699 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
701 let lang = "java";
702 let call_overrides = call_config.overrides.get(lang);
703 let effective_function_name = call_overrides
704 .and_then(|o| o.function.as_ref())
705 .cloned()
706 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
707 let effective_result_var = &call_config.result_var;
708 let effective_args = &call_config.args;
709 let function_name = effective_function_name.as_str();
710 let result_var = effective_result_var.as_str();
711 let args: &[crate::config::ArgMapping] = effective_args.as_slice();
712
713 let method_name = fixture.id.to_upper_camel_case();
714 let description = &fixture.description;
715 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
716
717 if call_overrides.is_none() {
719 let _ = writeln!(out, " @Test");
720 let _ = writeln!(out, " void test{method_name}() {{");
721 let _ = writeln!(out, " // {description}");
722 let _ = writeln!(
723 out,
724 " org.junit.jupiter.api.Assumptions.assumeTrue(false, \"TODO: implement Java e2e test for fixture '{}'\");",
725 fixture.id
726 );
727 let _ = writeln!(out, " }}");
728 return;
729 }
730
731 let effective_options_type: Option<String> = call_overrides
733 .and_then(|o| o.options_type.clone())
734 .or_else(|| options_type.map(|s| s.to_string()));
735 let effective_options_type = effective_options_type.as_deref();
736
737 let effective_result_is_simple = call_overrides.is_some_and(|o| o.result_is_simple) || result_is_simple;
739 let effective_result_is_bytes = call_overrides.is_some_and(|o| o.result_is_bytes);
740
741 let needs_deser = effective_options_type.is_some()
744 && args.iter().any(|arg| {
745 if arg.arg_type != "json_object" {
746 return false;
747 }
748 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
749 fixture.input.get(field).is_some_and(|v| !v.is_null() && !v.is_array())
750 });
751
752 let throws_clause = " throws Exception";
754
755 let _ = writeln!(out, " @Test");
756 let _ = writeln!(out, " void test{method_name}(){throws_clause} {{");
757 let _ = writeln!(out, " // {description}");
758
759 if let (true, Some(opts_type)) = (needs_deser, effective_options_type) {
761 for arg in args {
762 if arg.arg_type == "json_object" {
763 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
764 if let Some(val) = fixture.input.get(field) {
765 if !val.is_null() && !val.is_array() {
766 let normalized = super::normalize_json_keys_to_snake_case(val);
770 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
771 let var_name = &arg.name;
772 let _ = writeln!(
773 out,
774 " var {var_name} = MAPPER.readValue(\"{}\", {opts_type}.class);",
775 escape_java(&json_str)
776 );
777 }
778 }
779 }
780 }
781 }
782
783 let (mut setup_lines, args_str) =
784 build_args_and_setup(&fixture.input, args, class_name, effective_options_type, &fixture.id);
785
786 let mut visitor_arg = String::new();
788 if let Some(visitor_spec) = &fixture.visitor {
789 visitor_arg = build_java_visitor(&mut setup_lines, visitor_spec, class_name);
790 }
791
792 for line in &setup_lines {
793 let _ = writeln!(out, " {line}");
794 }
795
796 let final_args = if visitor_arg.is_empty() {
797 args_str
798 } else {
799 format!("{args_str}, {visitor_arg}")
800 };
801
802 if expects_error {
803 let _ = writeln!(
804 out,
805 " assertThrows(Exception.class, () -> {class_name}.{function_name}({final_args}));"
806 );
807 let _ = writeln!(out, " }}");
808 return;
809 }
810
811 let _ = writeln!(
812 out,
813 " var {result_var} = {class_name}.{function_name}({final_args});"
814 );
815
816 let needs_source_var = fixture
818 .assertions
819 .iter()
820 .any(|a| a.assertion_type == "method_result" && a.method.as_deref() == Some("run_query"));
821 if needs_source_var {
822 if let Some(source_arg) = args.iter().find(|a| a.field == "source_code") {
824 let field = source_arg.field.strip_prefix("input.").unwrap_or(&source_arg.field);
825 if let Some(val) = fixture.input.get(field) {
826 let java_val = json_to_java(val);
827 let _ = writeln!(out, " var source = {java_val}.getBytes();");
828 }
829 }
830 }
831
832 for assertion in &fixture.assertions {
833 render_assertion(
834 out,
835 assertion,
836 result_var,
837 class_name,
838 field_resolver,
839 effective_result_is_simple,
840 effective_result_is_bytes,
841 enum_fields,
842 );
843 }
844
845 let _ = writeln!(out, " }}");
846}
847
848fn build_args_and_setup(
852 input: &serde_json::Value,
853 args: &[crate::config::ArgMapping],
854 class_name: &str,
855 options_type: Option<&str>,
856 fixture_id: &str,
857) -> (Vec<String>, String) {
858 if args.is_empty() {
859 return (Vec::new(), String::new());
860 }
861
862 let mut setup_lines: Vec<String> = Vec::new();
863 let mut parts: Vec<String> = Vec::new();
864
865 for arg in args {
866 if arg.arg_type == "mock_url" {
867 setup_lines.push(format!(
868 "String {} = System.getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";",
869 arg.name,
870 ));
871 parts.push(arg.name.clone());
872 continue;
873 }
874
875 if arg.arg_type == "handle" {
876 let constructor_name = format!("create{}", arg.name.to_upper_camel_case());
878 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
879 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
880 if config_value.is_null()
881 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
882 {
883 setup_lines.push(format!("var {} = {class_name}.{constructor_name}(null);", arg.name,));
884 } else {
885 let json_str = serde_json::to_string(config_value).unwrap_or_default();
886 let name = &arg.name;
887 setup_lines.push(format!(
888 "var {name}Config = MAPPER.readValue(\"{}\", CrawlConfig.class);",
889 escape_java(&json_str),
890 ));
891 setup_lines.push(format!(
892 "var {} = {class_name}.{constructor_name}({name}Config);",
893 arg.name,
894 name = name,
895 ));
896 }
897 parts.push(arg.name.clone());
898 continue;
899 }
900
901 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
902 let val = input.get(field);
903 match val {
904 None | Some(serde_json::Value::Null) if arg.optional => {
905 if arg.arg_type == "json_object" {
909 if let Some(opts_type) = options_type {
910 parts.push(format!("MAPPER.readValue(\"{{}}\", {opts_type}.class)"));
911 } else {
912 parts.push("null".to_string());
913 }
914 } else {
915 parts.push("null".to_string());
916 }
917 }
918 None | Some(serde_json::Value::Null) => {
919 let default_val = match arg.arg_type.as_str() {
921 "string" | "file_path" => "\"\"".to_string(),
922 "int" | "integer" => "0".to_string(),
923 "float" | "number" => "0.0d".to_string(),
924 "bool" | "boolean" => "false".to_string(),
925 _ => "null".to_string(),
926 };
927 parts.push(default_val);
928 }
929 Some(v) => {
930 if arg.arg_type == "json_object" {
931 if v.is_array() {
934 let elem_type = arg.element_type.as_deref();
935 parts.push(json_to_java_typed(v, elem_type));
936 continue;
937 }
938 if options_type.is_some() {
940 parts.push(arg.name.clone());
941 continue;
942 }
943 parts.push(json_to_java(v));
944 continue;
945 }
946 if arg.arg_type == "bytes" {
948 let val = json_to_java(v);
949 parts.push(format!("{val}.getBytes()"));
950 continue;
951 }
952 if arg.arg_type == "file_path" {
954 let val = json_to_java(v);
955 parts.push(format!("java.nio.file.Path.of({val})"));
956 continue;
957 }
958 parts.push(json_to_java(v));
959 }
960 }
961 }
962
963 (setup_lines, parts.join(", "))
964}
965
966#[allow(clippy::too_many_arguments)]
967fn render_assertion(
968 out: &mut String,
969 assertion: &Assertion,
970 result_var: &str,
971 class_name: &str,
972 field_resolver: &FieldResolver,
973 result_is_simple: bool,
974 result_is_bytes: bool,
975 enum_fields: &HashSet<String>,
976) {
977 if let Some(f) = &assertion.field {
979 match f.as_str() {
980 "chunks_have_content" => {
982 let pred = format!(
983 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.content() != null && !c.content().isBlank())"
984 );
985 match assertion.assertion_type.as_str() {
986 "is_true" => {
987 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
988 }
989 "is_false" => {
990 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
991 }
992 _ => {
993 let _ = writeln!(
994 out,
995 " // skipped: unsupported assertion on synthetic field '{f}'"
996 );
997 }
998 }
999 return;
1000 }
1001 "chunks_have_heading_context" => {
1002 let pred = format!(
1003 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.metadata().headingContext().isPresent())"
1004 );
1005 match assertion.assertion_type.as_str() {
1006 "is_true" => {
1007 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
1008 }
1009 "is_false" => {
1010 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
1011 }
1012 _ => {
1013 let _ = writeln!(
1014 out,
1015 " // skipped: unsupported assertion on synthetic field '{f}'"
1016 );
1017 }
1018 }
1019 return;
1020 }
1021 "chunks_have_embeddings" => {
1022 let pred = format!(
1023 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.embedding() != null && !c.embedding().isEmpty())"
1024 );
1025 match assertion.assertion_type.as_str() {
1026 "is_true" => {
1027 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
1028 }
1029 "is_false" => {
1030 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
1031 }
1032 _ => {
1033 let _ = writeln!(
1034 out,
1035 " // skipped: unsupported assertion on synthetic field '{f}'"
1036 );
1037 }
1038 }
1039 return;
1040 }
1041 "first_chunk_starts_with_heading" => {
1042 let pred = format!(
1043 "{result_var}.chunks().orElse(java.util.List.of()).stream().findFirst().map(c -> c.metadata().headingContext().isPresent()).orElse(false)"
1044 );
1045 match assertion.assertion_type.as_str() {
1046 "is_true" => {
1047 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
1048 }
1049 "is_false" => {
1050 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
1051 }
1052 _ => {
1053 let _ = writeln!(
1054 out,
1055 " // skipped: unsupported assertion on synthetic field '{f}'"
1056 );
1057 }
1058 }
1059 return;
1060 }
1061 "embedding_dimensions" => {
1065 let embed_list = if result_is_simple {
1067 result_var.to_string()
1068 } else {
1069 format!("{result_var}.embeddings()")
1070 };
1071 let expr = format!("({embed_list}.isEmpty() ? 0 : {embed_list}.get(0).size())");
1072 match assertion.assertion_type.as_str() {
1073 "equals" => {
1074 if let Some(val) = &assertion.value {
1075 let java_val = json_to_java(val);
1076 let _ = writeln!(out, " assertEquals({java_val}, {expr});");
1077 }
1078 }
1079 "greater_than" => {
1080 if let Some(val) = &assertion.value {
1081 let java_val = json_to_java(val);
1082 let _ = writeln!(
1083 out,
1084 " assertTrue({expr} > {java_val}, \"expected > {java_val}\");"
1085 );
1086 }
1087 }
1088 _ => {
1089 let _ = writeln!(out, " // skipped: unsupported assertion on '{f}'");
1090 }
1091 }
1092 return;
1093 }
1094 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1095 let embed_list = if result_is_simple {
1097 result_var.to_string()
1098 } else {
1099 format!("{result_var}.embeddings()")
1100 };
1101 let pred = match f.as_str() {
1102 "embeddings_valid" => {
1103 format!("{embed_list}.stream().allMatch(e -> e != null && !e.isEmpty())")
1104 }
1105 "embeddings_finite" => {
1106 format!("{embed_list}.stream().flatMap(java.util.Collection::stream).allMatch(Float::isFinite)")
1107 }
1108 "embeddings_non_zero" => {
1109 format!("{embed_list}.stream().allMatch(e -> e.stream().anyMatch(v -> v != 0.0f))")
1110 }
1111 "embeddings_normalized" => format!(
1112 "{embed_list}.stream().allMatch(e -> {{ double n = e.stream().mapToDouble(v -> v * v).sum(); return Math.abs(n - 1.0) < 1e-3; }})"
1113 ),
1114 _ => unreachable!(),
1115 };
1116 match assertion.assertion_type.as_str() {
1117 "is_true" => {
1118 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
1119 }
1120 "is_false" => {
1121 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
1122 }
1123 _ => {
1124 let _ = writeln!(out, " // skipped: unsupported assertion on '{f}'");
1125 }
1126 }
1127 return;
1128 }
1129 "keywords" | "keywords_count" => {
1131 let _ = writeln!(
1132 out,
1133 " // skipped: field '{f}' not available on Java ExtractionResult"
1134 );
1135 return;
1136 }
1137 "metadata" => {
1140 match assertion.assertion_type.as_str() {
1141 "not_empty" => {
1142 let _ = writeln!(
1143 out,
1144 " assertTrue({result_var}.metadata().title().isPresent() || {result_var}.metadata().subject().isPresent() || !{result_var}.metadata().additional().isEmpty(), \"expected non-empty value\");"
1145 );
1146 return;
1147 }
1148 "is_empty" => {
1149 let _ = writeln!(
1150 out,
1151 " assertFalse({result_var}.metadata().title().isPresent() || {result_var}.metadata().subject().isPresent() || !{result_var}.metadata().additional().isEmpty(), \"expected empty value\");"
1152 );
1153 return;
1154 }
1155 _ => {} }
1157 }
1158 _ => {}
1159 }
1160 }
1161
1162 if let Some(f) = &assertion.field {
1164 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1165 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1166 return;
1167 }
1168 }
1169
1170 let field_is_enum = assertion
1175 .field
1176 .as_deref()
1177 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1178
1179 let field_expr = if result_is_simple {
1180 result_var.to_string()
1181 } else {
1182 match &assertion.field {
1183 Some(f) if !f.is_empty() => {
1184 let accessor = field_resolver.accessor(f, "java", result_var);
1185 let resolved = field_resolver.resolve(f);
1186 if field_resolver.is_optional(resolved) && !field_resolver.has_map_access(f) {
1189 match assertion.assertion_type.as_str() {
1191 "not_empty" | "is_empty" => accessor,
1194 "count_min" | "count_equals" => {
1196 format!("{accessor}.orElse(java.util.List.of())")
1197 }
1198 "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" => {
1200 if field_resolver.is_array(resolved) {
1201 format!("{accessor}.orElse(java.util.List.of())")
1202 } else {
1203 format!("{accessor}.orElse(0L)")
1204 }
1205 }
1206 _ if field_resolver.is_array(resolved) => {
1207 format!("{accessor}.orElse(java.util.List.of())")
1208 }
1209 _ => format!("{accessor}.orElse(\"\")"),
1210 }
1211 } else {
1212 accessor
1213 }
1214 }
1215 _ => result_var.to_string(),
1216 }
1217 };
1218
1219 let string_expr = if field_is_enum {
1223 format!("{field_expr}.getValue()")
1224 } else {
1225 field_expr.clone()
1226 };
1227
1228 match assertion.assertion_type.as_str() {
1229 "equals" => {
1230 if let Some(expected) = &assertion.value {
1231 let java_val = json_to_java(expected);
1232 if expected.is_string() {
1233 let _ = writeln!(out, " assertEquals({java_val}, {string_expr}.trim());");
1234 } else {
1235 let _ = writeln!(out, " assertEquals({java_val}, {field_expr});");
1236 }
1237 }
1238 }
1239 "contains" => {
1240 if let Some(expected) = &assertion.value {
1241 let java_val = json_to_java(expected);
1242 let _ = writeln!(
1243 out,
1244 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
1245 );
1246 }
1247 }
1248 "contains_all" => {
1249 if let Some(values) = &assertion.values {
1250 for val in values {
1251 let java_val = json_to_java(val);
1252 let _ = writeln!(
1253 out,
1254 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
1255 );
1256 }
1257 }
1258 }
1259 "not_contains" => {
1260 if let Some(expected) = &assertion.value {
1261 let java_val = json_to_java(expected);
1262 let _ = writeln!(
1263 out,
1264 " assertFalse({string_expr}.contains({java_val}), \"expected NOT to contain: \" + {java_val});"
1265 );
1266 }
1267 }
1268 "not_empty" => {
1269 let _ = writeln!(
1270 out,
1271 " assertFalse({field_expr}.isEmpty(), \"expected non-empty value\");"
1272 );
1273 }
1274 "is_empty" => {
1275 let _ = writeln!(
1276 out,
1277 " assertTrue({field_expr}.isEmpty(), \"expected empty value\");"
1278 );
1279 }
1280 "contains_any" => {
1281 if let Some(values) = &assertion.values {
1282 let checks: Vec<String> = values
1283 .iter()
1284 .map(|v| {
1285 let java_val = json_to_java(v);
1286 format!("{string_expr}.contains({java_val})")
1287 })
1288 .collect();
1289 let joined = checks.join(" || ");
1290 let _ = writeln!(
1291 out,
1292 " assertTrue({joined}, \"expected to contain at least one of the specified values\");"
1293 );
1294 }
1295 }
1296 "greater_than" => {
1297 if let Some(val) = &assertion.value {
1298 let java_val = json_to_java(val);
1299 let _ = writeln!(
1300 out,
1301 " assertTrue({field_expr} > {java_val}, \"expected > {java_val}\");"
1302 );
1303 }
1304 }
1305 "less_than" => {
1306 if let Some(val) = &assertion.value {
1307 let java_val = json_to_java(val);
1308 let _ = writeln!(
1309 out,
1310 " assertTrue({field_expr} < {java_val}, \"expected < {java_val}\");"
1311 );
1312 }
1313 }
1314 "greater_than_or_equal" => {
1315 if let Some(val) = &assertion.value {
1316 let java_val = json_to_java(val);
1317 let _ = writeln!(
1318 out,
1319 " assertTrue({field_expr} >= {java_val}, \"expected >= {java_val}\");"
1320 );
1321 }
1322 }
1323 "less_than_or_equal" => {
1324 if let Some(val) = &assertion.value {
1325 let java_val = json_to_java(val);
1326 let _ = writeln!(
1327 out,
1328 " assertTrue({field_expr} <= {java_val}, \"expected <= {java_val}\");"
1329 );
1330 }
1331 }
1332 "starts_with" => {
1333 if let Some(expected) = &assertion.value {
1334 let java_val = json_to_java(expected);
1335 let _ = writeln!(
1336 out,
1337 " assertTrue({string_expr}.startsWith({java_val}), \"expected to start with: \" + {java_val});"
1338 );
1339 }
1340 }
1341 "ends_with" => {
1342 if let Some(expected) = &assertion.value {
1343 let java_val = json_to_java(expected);
1344 let _ = writeln!(
1345 out,
1346 " assertTrue({string_expr}.endsWith({java_val}), \"expected to end with: \" + {java_val});"
1347 );
1348 }
1349 }
1350 "min_length" => {
1351 if let Some(val) = &assertion.value {
1352 if let Some(n) = val.as_u64() {
1353 let len_expr = if result_is_bytes {
1355 format!("{field_expr}.length")
1356 } else {
1357 format!("{field_expr}.length()")
1358 };
1359 let _ = writeln!(
1360 out,
1361 " assertTrue({len_expr} >= {n}, \"expected length >= {n}\");"
1362 );
1363 }
1364 }
1365 }
1366 "max_length" => {
1367 if let Some(val) = &assertion.value {
1368 if let Some(n) = val.as_u64() {
1369 let len_expr = if result_is_bytes {
1370 format!("{field_expr}.length")
1371 } else {
1372 format!("{field_expr}.length()")
1373 };
1374 let _ = writeln!(
1375 out,
1376 " assertTrue({len_expr} <= {n}, \"expected length <= {n}\");"
1377 );
1378 }
1379 }
1380 }
1381 "count_min" => {
1382 if let Some(val) = &assertion.value {
1383 if let Some(n) = val.as_u64() {
1384 let _ = writeln!(
1385 out,
1386 " assertTrue({field_expr}.size() >= {n}, \"expected at least {n} elements\");"
1387 );
1388 }
1389 }
1390 }
1391 "count_equals" => {
1392 if let Some(val) = &assertion.value {
1393 if let Some(n) = val.as_u64() {
1394 let _ = writeln!(
1395 out,
1396 " assertEquals({n}, {field_expr}.size(), \"expected exactly {n} elements\");"
1397 );
1398 }
1399 }
1400 }
1401 "is_true" => {
1402 let _ = writeln!(out, " assertTrue({field_expr}, \"expected true\");");
1403 }
1404 "is_false" => {
1405 let _ = writeln!(out, " assertFalse({field_expr}, \"expected false\");");
1406 }
1407 "method_result" => {
1408 if let Some(method_name) = &assertion.method {
1409 let call_expr = build_java_method_call(result_var, method_name, assertion.args.as_ref(), class_name);
1410 let check = assertion.check.as_deref().unwrap_or("is_true");
1411 let method_returns_collection =
1413 matches!(method_name.as_str(), "find_nodes_by_type" | "findNodesByType");
1414 match check {
1415 "equals" => {
1416 if let Some(val) = &assertion.value {
1417 if val.is_boolean() {
1418 if val.as_bool() == Some(true) {
1419 let _ = writeln!(out, " assertTrue({call_expr});");
1420 } else {
1421 let _ = writeln!(out, " assertFalse({call_expr});");
1422 }
1423 } else if method_returns_collection {
1424 let java_val = json_to_java(val);
1425 let _ = writeln!(out, " assertEquals({java_val}, {call_expr}.size());");
1426 } else {
1427 let java_val = json_to_java(val);
1428 let _ = writeln!(out, " assertEquals({java_val}, {call_expr});");
1429 }
1430 }
1431 }
1432 "is_true" => {
1433 let _ = writeln!(out, " assertTrue({call_expr});");
1434 }
1435 "is_false" => {
1436 let _ = writeln!(out, " assertFalse({call_expr});");
1437 }
1438 "greater_than_or_equal" => {
1439 if let Some(val) = &assertion.value {
1440 let n = val.as_u64().unwrap_or(0);
1441 let _ = writeln!(out, " assertTrue({call_expr} >= {n}, \"expected >= {n}\");");
1442 }
1443 }
1444 "count_min" => {
1445 if let Some(val) = &assertion.value {
1446 let n = val.as_u64().unwrap_or(0);
1447 let _ = writeln!(
1448 out,
1449 " assertTrue({call_expr}.size() >= {n}, \"expected at least {n} elements\");"
1450 );
1451 }
1452 }
1453 "is_error" => {
1454 let _ = writeln!(out, " assertThrows(Exception.class, () -> {{ {call_expr}; }});");
1455 }
1456 "contains" => {
1457 if let Some(val) = &assertion.value {
1458 let java_val = json_to_java(val);
1459 let _ = writeln!(
1460 out,
1461 " assertTrue({call_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
1462 );
1463 }
1464 }
1465 other_check => {
1466 panic!("Java e2e generator: unsupported method_result check type: {other_check}");
1467 }
1468 }
1469 } else {
1470 panic!("Java e2e generator: method_result assertion missing 'method' field");
1471 }
1472 }
1473 "matches_regex" => {
1474 if let Some(expected) = &assertion.value {
1475 let java_val = json_to_java(expected);
1476 let _ = writeln!(
1477 out,
1478 " assertTrue({string_expr}.matches({java_val}), \"expected value to match regex: \" + {java_val});"
1479 );
1480 }
1481 }
1482 "not_error" => {
1483 }
1485 "error" => {
1486 }
1488 other => {
1489 panic!("Java e2e generator: unsupported assertion type: {other}");
1490 }
1491 }
1492}
1493
1494fn build_java_method_call(
1498 result_var: &str,
1499 method_name: &str,
1500 args: Option<&serde_json::Value>,
1501 class_name: &str,
1502) -> String {
1503 match method_name {
1504 "root_child_count" => format!("{result_var}.rootNode().childCount()"),
1505 "root_node_type" => format!("{result_var}.rootNode().kind()"),
1506 "named_children_count" => format!("{result_var}.rootNode().namedChildCount()"),
1507 "has_error_nodes" => format!("{class_name}.treeHasErrorNodes({result_var})"),
1508 "error_count" | "tree_error_count" => format!("{class_name}.treeErrorCount({result_var})"),
1509 "tree_to_sexp" => format!("{class_name}.treeToSexp({result_var})"),
1510 "contains_node_type" => {
1511 let node_type = args
1512 .and_then(|a| a.get("node_type"))
1513 .and_then(|v| v.as_str())
1514 .unwrap_or("");
1515 format!("{class_name}.treeContainsNodeType({result_var}, \"{node_type}\")")
1516 }
1517 "find_nodes_by_type" => {
1518 let node_type = args
1519 .and_then(|a| a.get("node_type"))
1520 .and_then(|v| v.as_str())
1521 .unwrap_or("");
1522 format!("{class_name}.findNodesByType({result_var}, \"{node_type}\")")
1523 }
1524 "run_query" => {
1525 let query_source = args
1526 .and_then(|a| a.get("query_source"))
1527 .and_then(|v| v.as_str())
1528 .unwrap_or("");
1529 let language = args
1530 .and_then(|a| a.get("language"))
1531 .and_then(|v| v.as_str())
1532 .unwrap_or("");
1533 let escaped_query = escape_java(query_source);
1534 format!("{class_name}.runQuery({result_var}, \"{language}\", \"{escaped_query}\", source)")
1535 }
1536 _ => {
1537 format!("{result_var}.{}()", method_name.to_lower_camel_case())
1538 }
1539 }
1540}
1541
1542fn json_to_java(value: &serde_json::Value) -> String {
1544 json_to_java_typed(value, None)
1545}
1546
1547fn json_to_java_typed(value: &serde_json::Value, element_type: Option<&str>) -> String {
1550 match value {
1551 serde_json::Value::String(s) => format!("\"{}\"", escape_java(s)),
1552 serde_json::Value::Bool(b) => b.to_string(),
1553 serde_json::Value::Number(n) => {
1554 if n.is_f64() {
1555 match element_type {
1556 Some("f32" | "float" | "Float") => format!("{}f", n),
1557 _ => format!("{}d", n),
1558 }
1559 } else {
1560 n.to_string()
1561 }
1562 }
1563 serde_json::Value::Null => "null".to_string(),
1564 serde_json::Value::Array(arr) => {
1565 let items: Vec<String> = arr.iter().map(|v| json_to_java_typed(v, element_type)).collect();
1566 format!("java.util.List.of({})", items.join(", "))
1567 }
1568 serde_json::Value::Object(_) => {
1569 let json_str = serde_json::to_string(value).unwrap_or_default();
1570 format!("\"{}\"", escape_java(&json_str))
1571 }
1572 }
1573}
1574
1575fn build_java_visitor(
1581 setup_lines: &mut Vec<String>,
1582 visitor_spec: &crate::fixture::VisitorSpec,
1583 class_name: &str,
1584) -> String {
1585 setup_lines.push("class _TestVisitor implements TestVisitor {".to_string());
1586 for (method_name, action) in &visitor_spec.callbacks {
1587 emit_java_visitor_method(setup_lines, method_name, action, class_name);
1588 }
1589 setup_lines.push("}".to_string());
1590 setup_lines.push("var visitor = new _TestVisitor();".to_string());
1591 "visitor".to_string()
1592}
1593
1594fn emit_java_visitor_method(
1596 setup_lines: &mut Vec<String>,
1597 method_name: &str,
1598 action: &CallbackAction,
1599 _class_name: &str,
1600) {
1601 let camel_method = method_to_camel(method_name);
1602 let params = match method_name {
1603 "visit_link" => "VisitContext ctx, String href, String text, String title",
1604 "visit_image" => "VisitContext ctx, String src, String alt, String title",
1605 "visit_heading" => "VisitContext ctx, int level, String text, String id",
1606 "visit_code_block" => "VisitContext ctx, String lang, String code",
1607 "visit_code_inline"
1608 | "visit_strong"
1609 | "visit_emphasis"
1610 | "visit_strikethrough"
1611 | "visit_underline"
1612 | "visit_subscript"
1613 | "visit_superscript"
1614 | "visit_mark"
1615 | "visit_button"
1616 | "visit_summary"
1617 | "visit_figcaption"
1618 | "visit_definition_term"
1619 | "visit_definition_description" => "VisitContext ctx, String text",
1620 "visit_text" => "VisitContext ctx, String text",
1621 "visit_list_item" => "VisitContext ctx, boolean ordered, String marker, String text",
1622 "visit_blockquote" => "VisitContext ctx, String content, long depth",
1623 "visit_table_row" => "VisitContext ctx, java.util.List<String> cells, boolean isHeader",
1624 "visit_custom_element" => "VisitContext ctx, String tagName, String html",
1625 "visit_form" => "VisitContext ctx, String actionUrl, String method",
1626 "visit_input" => "VisitContext ctx, String inputType, String name, String value",
1627 "visit_audio" | "visit_video" | "visit_iframe" => "VisitContext ctx, String src",
1628 "visit_details" => "VisitContext ctx, boolean isOpen",
1629 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1630 "VisitContext ctx, String output"
1631 }
1632 "visit_list_start" => "VisitContext ctx, boolean ordered",
1633 "visit_list_end" => "VisitContext ctx, boolean ordered, String output",
1634 _ => "VisitContext ctx",
1635 };
1636
1637 setup_lines.push(format!(" @Override public VisitResult {camel_method}({params}) {{"));
1638 match action {
1639 CallbackAction::Skip => {
1640 setup_lines.push(" return VisitResult.skip();".to_string());
1641 }
1642 CallbackAction::Continue => {
1643 setup_lines.push(" return VisitResult.continue_();".to_string());
1644 }
1645 CallbackAction::PreserveHtml => {
1646 setup_lines.push(" return VisitResult.preserveHtml();".to_string());
1647 }
1648 CallbackAction::Custom { output } => {
1649 let escaped = escape_java(output);
1650 setup_lines.push(format!(" return VisitResult.custom(\"{escaped}\");"));
1651 }
1652 CallbackAction::CustomTemplate { template } => {
1653 let mut format_str = String::with_capacity(template.len());
1657 let mut format_args: Vec<String> = Vec::new();
1658 let mut chars = template.chars().peekable();
1659 while let Some(ch) = chars.next() {
1660 if ch == '{' {
1661 let mut name = String::new();
1663 let mut closed = false;
1664 for inner in chars.by_ref() {
1665 if inner == '}' {
1666 closed = true;
1667 break;
1668 }
1669 name.push(inner);
1670 }
1671 if closed && !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
1672 let camel_name = name.as_str().to_lower_camel_case();
1673 format_args.push(camel_name);
1674 format_str.push_str("%s");
1675 } else {
1676 format_str.push('{');
1678 format_str.push_str(&name);
1679 if closed {
1680 format_str.push('}');
1681 }
1682 }
1683 } else {
1684 format_str.push(ch);
1685 }
1686 }
1687 let escaped = escape_java(&format_str);
1688 if format_args.is_empty() {
1689 setup_lines.push(format!(" return VisitResult.custom(\"{escaped}\");"));
1690 } else {
1691 let args_str = format_args.join(", ");
1692 setup_lines.push(format!(
1693 " return VisitResult.custom(String.format(\"{escaped}\", {args_str}));"
1694 ));
1695 }
1696 }
1697 }
1698 setup_lines.push(" }".to_string());
1699}
1700
1701fn method_to_camel(snake: &str) -> String {
1703 snake.to_lower_camel_case()
1704}