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::ResolvedCrateConfig;
12use alef_core::hash::{self, CommentStyle};
13use alef_core::template_versions as tv;
14use anyhow::Result;
15use heck::{ToLowerCamelCase, ToUpperCamelCase};
16use std::fmt::Write as FmtWrite;
17use std::path::PathBuf;
18
19use super::E2eCodegen;
20use super::client;
21
22pub struct JavaCodegen;
24
25impl E2eCodegen for JavaCodegen {
26 fn generate(
27 &self,
28 groups: &[FixtureGroup],
29 e2e_config: &E2eConfig,
30 config: &ResolvedCrateConfig,
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(|| 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(|| config.name.clone());
62
63 let java_group_id = config.java_group_id();
65 let pkg_version = 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
86 let empty_enum_fields = std::collections::HashMap::new();
88 let java_enum_fields = overrides.as_ref().map(|o| &o.enum_fields).unwrap_or(&empty_enum_fields);
89
90 let mut effective_nested_types = default_java_nested_types();
92 if let Some(overrides_map) = overrides.map(|o| &o.nested_types) {
93 effective_nested_types.extend(overrides_map.clone());
94 }
95
96 let nested_types_optional = overrides.map(|o| o.nested_types_optional).unwrap_or(true);
98
99 let field_resolver = FieldResolver::new(
100 &e2e_config.fields,
101 &e2e_config.fields_optional,
102 &e2e_config.result_fields,
103 &e2e_config.fields_array,
104 &std::collections::HashSet::new(),
105 );
106
107 for group in groups {
108 let active: Vec<&Fixture> = group
109 .fixtures
110 .iter()
111 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
112 .collect();
113
114 if active.is_empty() {
115 continue;
116 }
117
118 let class_file_name = format!("{}Test.java", sanitize_filename(&group.category).to_upper_camel_case());
119 let content = render_test_file(
120 &group.category,
121 &active,
122 &class_name,
123 &function_name,
124 &java_group_id,
125 result_var,
126 &e2e_config.call.args,
127 options_type.as_deref(),
128 &field_resolver,
129 result_is_simple,
130 java_enum_fields,
131 e2e_config,
132 &effective_nested_types,
133 nested_types_optional,
134 );
135 files.push(GeneratedFile {
136 path: test_base.join(class_file_name),
137 content,
138 generated_header: true,
139 });
140 }
141
142 Ok(files)
143 }
144
145 fn language_name(&self) -> &'static str {
146 "java"
147 }
148}
149
150fn render_pom_xml(
155 pkg_name: &str,
156 java_group_id: &str,
157 pkg_version: &str,
158 dep_mode: crate::config::DependencyMode,
159) -> String {
160 let (dep_group_id, dep_artifact_id) = if let Some((g, a)) = pkg_name.split_once(':') {
162 (g, a)
163 } else {
164 (java_group_id, pkg_name)
165 };
166 let artifact_id = format!("{dep_artifact_id}-e2e-java");
167 let dep_block = match dep_mode {
168 crate::config::DependencyMode::Registry => {
169 format!(
170 r#" <dependency>
171 <groupId>{dep_group_id}</groupId>
172 <artifactId>{dep_artifact_id}</artifactId>
173 <version>{pkg_version}</version>
174 </dependency>"#
175 )
176 }
177 crate::config::DependencyMode::Local => {
178 format!(
179 r#" <dependency>
180 <groupId>{dep_group_id}</groupId>
181 <artifactId>{dep_artifact_id}</artifactId>
182 <version>{pkg_version}</version>
183 <scope>system</scope>
184 <systemPath>${{project.basedir}}/../../packages/java/target/{dep_artifact_id}-{pkg_version}.jar</systemPath>
185 </dependency>"#
186 )
187 }
188 };
189 format!(
190 r#"<?xml version="1.0" encoding="UTF-8"?>
191<project xmlns="http://maven.apache.org/POM/4.0.0"
192 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
193 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
194 <modelVersion>4.0.0</modelVersion>
195
196 <groupId>{java_group_id}</groupId>
197 <artifactId>{artifact_id}</artifactId>
198 <version>0.1.0</version>
199
200 <properties>
201 <maven.compiler.source>25</maven.compiler.source>
202 <maven.compiler.target>25</maven.compiler.target>
203 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
204 <junit.version>{junit}</junit.version>
205 </properties>
206
207 <dependencies>
208{dep_block}
209 <dependency>
210 <groupId>com.fasterxml.jackson.core</groupId>
211 <artifactId>jackson-databind</artifactId>
212 <version>{jackson}</version>
213 </dependency>
214 <dependency>
215 <groupId>com.fasterxml.jackson.datatype</groupId>
216 <artifactId>jackson-datatype-jdk8</artifactId>
217 <version>{jackson}</version>
218 </dependency>
219 <dependency>
220 <groupId>org.jetbrains</groupId>
221 <artifactId>annotations</artifactId>
222 <version>24.1.0</version>
223 </dependency>
224 <dependency>
225 <groupId>org.junit.jupiter</groupId>
226 <artifactId>junit-jupiter</artifactId>
227 <version>${{junit.version}}</version>
228 <scope>test</scope>
229 </dependency>
230 </dependencies>
231
232 <build>
233 <plugins>
234 <plugin>
235 <groupId>org.codehaus.mojo</groupId>
236 <artifactId>build-helper-maven-plugin</artifactId>
237 <version>{build_helper}</version>
238 <executions>
239 <execution>
240 <id>add-test-source</id>
241 <phase>generate-test-sources</phase>
242 <goals>
243 <goal>add-test-source</goal>
244 </goals>
245 <configuration>
246 <sources>
247 <source>src/test/java</source>
248 </sources>
249 </configuration>
250 </execution>
251 </executions>
252 </plugin>
253 <plugin>
254 <groupId>org.apache.maven.plugins</groupId>
255 <artifactId>maven-surefire-plugin</artifactId>
256 <version>{maven_surefire}</version>
257 <configuration>
258 <argLine>--enable-preview --enable-native-access=ALL-UNNAMED -Djava.library.path=${{project.basedir}}/../../target/release</argLine>
259 <workingDirectory>${{project.basedir}}/../../test_documents</workingDirectory>
260 </configuration>
261 </plugin>
262 </plugins>
263 </build>
264</project>
265"#,
266 junit = tv::maven::JUNIT,
267 jackson = tv::maven::JACKSON_E2E,
268 build_helper = tv::maven::BUILD_HELPER_MAVEN_PLUGIN,
269 maven_surefire = tv::maven::MAVEN_SUREFIRE_PLUGIN_E2E,
270 )
271}
272
273#[allow(clippy::too_many_arguments)]
274fn render_test_file(
275 category: &str,
276 fixtures: &[&Fixture],
277 class_name: &str,
278 function_name: &str,
279 java_group_id: &str,
280 result_var: &str,
281 args: &[crate::config::ArgMapping],
282 options_type: Option<&str>,
283 field_resolver: &FieldResolver,
284 result_is_simple: bool,
285 enum_fields: &std::collections::HashMap<String, String>,
286 e2e_config: &E2eConfig,
287 nested_types: &std::collections::HashMap<String, String>,
288 nested_types_optional: bool,
289) -> String {
290 let mut out = String::new();
291 out.push_str(&hash::header(CommentStyle::DoubleSlash));
292 let test_class_name = format!("{}Test", sanitize_filename(category).to_upper_camel_case());
293
294 let (import_path, simple_class) = if class_name.contains('.') {
297 let simple = class_name.rsplit('.').next().unwrap_or(class_name);
298 (class_name, simple)
299 } else {
300 ("", class_name)
301 };
302
303 let _ = writeln!(out, "package {java_group_id}.e2e;");
304 let _ = writeln!(out);
305
306 let lang_for_om = "java";
310 let _needs_object_mapper_for_options = false;
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_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 for arg in &call_cfg.args {
336 if let Some(elem_type) = &arg.element_type {
337 if elem_type == "BatchBytesItem" || elem_type == "BatchFileItem" {
338 all_options_types.insert(elem_type.clone());
339 }
340 }
341 }
342 }
343
344 let _ = writeln!(out, "import org.junit.jupiter.api.Test;");
345 let _ = writeln!(out, "import static org.junit.jupiter.api.Assertions.*;");
346 if !import_path.is_empty() {
347 let _ = writeln!(out, "import {import_path};");
348 }
349 if needs_object_mapper {
350 let _ = writeln!(out, "import com.fasterxml.jackson.databind.ObjectMapper;");
351 let _ = writeln!(out, "import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;");
352 }
353 let mut enum_types_used: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
355 let mut nested_types_used: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
359 for f in fixtures.iter() {
360 let call_cfg = e2e_config.resolve_call(f.call.as_deref());
361 for arg in &call_cfg.args {
362 if arg.arg_type == "json_object" {
363 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
364 if let Some(val) = f.input.get(field) {
365 if !val.is_null() && !val.is_array() {
366 if let Some(obj) = val.as_object() {
367 collect_enum_and_nested_types(obj, enum_fields, &mut enum_types_used);
368 collect_nested_type_names(obj, nested_types, &mut nested_types_used);
369 }
370 }
371 }
372 }
373 }
374 }
375
376 if !all_options_types.is_empty() {
378 let opts_pkg = if !import_path.is_empty() {
379 import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("")
380 } else {
381 ""
382 };
383 for opts_type in &all_options_types {
384 let qualified = if opts_pkg.is_empty() {
385 opts_type.clone()
386 } else {
387 format!("{opts_pkg}.{opts_type}")
388 };
389 let _ = writeln!(out, "import {qualified};");
390 }
391 }
392
393 if !enum_types_used.is_empty() && !import_path.is_empty() {
395 let binding_pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
396 for enum_type in &enum_types_used {
397 let _ = writeln!(out, "import {binding_pkg}.{enum_type};");
398 }
399 }
400
401 if !nested_types_used.is_empty() && !import_path.is_empty() {
406 let binding_pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
407 for type_name in &nested_types_used {
408 let _ = writeln!(out, "import {binding_pkg}.{type_name};");
409 }
410 }
411
412 if needs_object_mapper_for_handle && !import_path.is_empty() {
414 let pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
415 let _ = writeln!(out, "import {pkg}.CrawlConfig;");
416 }
417 let has_visitor_fixtures = fixtures.iter().any(|f| f.visitor.is_some());
419 if has_visitor_fixtures && !import_path.is_empty() {
420 let binding_pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
421 if !binding_pkg.is_empty() {
422 let _ = writeln!(out, "import {binding_pkg}.Visitor;");
423 let _ = writeln!(out, "import {binding_pkg}.NodeContext;");
424 let _ = writeln!(out, "import {binding_pkg}.VisitResult;");
425 }
426 }
427 if !all_options_types.is_empty() {
429 let _ = writeln!(out, "import java.util.Optional;");
430 }
431 let _ = writeln!(out);
432
433 let _ = writeln!(out, "/** E2e tests for category: {category}. */");
434 let _ = writeln!(out, "class {test_class_name} {{");
435
436 if needs_object_mapper {
437 let _ = writeln!(out);
438 let _ = writeln!(
439 out,
440 " private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());"
441 );
442 }
443
444 for fixture in fixtures {
445 render_test_method(
446 &mut out,
447 fixture,
448 simple_class,
449 function_name,
450 result_var,
451 args,
452 options_type,
453 field_resolver,
454 result_is_simple,
455 enum_fields,
456 e2e_config,
457 nested_types,
458 nested_types_optional,
459 );
460 let _ = writeln!(out);
461 }
462
463 let _ = writeln!(out, "}}");
464 out
465}
466
467struct JavaTestClientRenderer;
475
476impl client::TestClientRenderer for JavaTestClientRenderer {
477 fn language_name(&self) -> &'static str {
478 "java"
479 }
480
481 fn sanitize_test_name(&self, id: &str) -> String {
485 id.to_upper_camel_case()
486 }
487
488 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
494 let _ = writeln!(out, " @Test");
495 if let Some(reason) = skip_reason {
496 let escaped_reason = escape_java(reason);
497 let _ = writeln!(out, " void test{fn_name}() {{");
498 let _ = writeln!(out, " // {description}");
499 let _ = writeln!(
500 out,
501 " org.junit.jupiter.api.Assumptions.assumeTrue(false, \"{escaped_reason}\");"
502 );
503 } else {
504 let _ = writeln!(out, " void test{fn_name}() throws Exception {{");
505 let _ = writeln!(out, " // {description}");
506 let _ = writeln!(out, " String baseUrl = System.getenv(\"MOCK_SERVER_URL\");");
508 let _ = writeln!(out, " if (baseUrl == null) baseUrl = \"http://localhost:8080\";");
509 }
510 }
511
512 fn render_test_close(&self, out: &mut String) {
514 let _ = writeln!(out, " }}");
515 }
516
517 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
523 const JAVA_RESTRICTED_HEADERS: &[&str] = &["connection", "content-length", "expect", "host", "upgrade"];
525
526 let method = ctx.method.to_uppercase();
527
528 let path = if ctx.query_params.is_empty() {
530 ctx.path.to_string()
531 } else {
532 let pairs: Vec<String> = ctx
533 .query_params
534 .iter()
535 .map(|(k, v)| {
536 let val_str = match v {
537 serde_json::Value::String(s) => s.clone(),
538 other => other.to_string(),
539 };
540 format!("{}={}", k, escape_java(&val_str))
541 })
542 .collect();
543 format!("{}?{}", ctx.path, pairs.join("&"))
544 };
545 let _ = writeln!(
546 out,
547 " java.net.URI uri = java.net.URI.create(baseUrl + \"{path}\");"
548 );
549
550 let body_publisher = if let Some(body) = ctx.body {
551 let json = serde_json::to_string(body).unwrap_or_default();
552 let escaped = escape_java(&json);
553 format!("java.net.http.HttpRequest.BodyPublishers.ofString(\"{escaped}\")")
554 } else {
555 "java.net.http.HttpRequest.BodyPublishers.noBody()".to_string()
556 };
557
558 let _ = writeln!(out, " var builder = java.net.http.HttpRequest.newBuilder(uri)");
559 let _ = writeln!(out, " .method(\"{method}\", {body_publisher});");
560
561 if ctx.body.is_some() {
563 let content_type = ctx.content_type.unwrap_or("application/json");
564 if !ctx.headers.keys().any(|k| k.to_lowercase() == "content-type") {
566 let _ = writeln!(
567 out,
568 " builder = builder.header(\"Content-Type\", \"{content_type}\");"
569 );
570 }
571 }
572
573 for (name, value) in ctx.headers {
575 if JAVA_RESTRICTED_HEADERS.contains(&name.to_lowercase().as_str()) {
576 continue;
577 }
578 let escaped_name = escape_java(name);
579 let escaped_value = escape_java(value);
580 let _ = writeln!(
581 out,
582 " builder = builder.header(\"{escaped_name}\", \"{escaped_value}\");"
583 );
584 }
585
586 if !ctx.cookies.is_empty() {
588 let cookie_str: Vec<String> = ctx.cookies.iter().map(|(k, v)| format!("{k}={v}")).collect();
589 let cookie_header = escape_java(&cookie_str.join("; "));
590 let _ = writeln!(
591 out,
592 " builder = builder.header(\"Cookie\", \"{cookie_header}\");"
593 );
594 }
595
596 let response_var = ctx.response_var;
597 let _ = writeln!(
598 out,
599 " var {response_var} = java.net.http.HttpClient.newHttpClient()"
600 );
601 let _ = writeln!(
602 out,
603 " .send(builder.build(), java.net.http.HttpResponse.BodyHandlers.ofString());"
604 );
605 }
606
607 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
609 let _ = writeln!(
610 out,
611 " assertEquals({status}, {response_var}.statusCode(), \"status code mismatch\");"
612 );
613 }
614
615 fn render_assert_header(&self, out: &mut String, response_var: &str, name: &str, expected: &str) {
619 let escaped_name = escape_java(name);
620 match expected {
621 "<<present>>" => {
622 let _ = writeln!(
623 out,
624 " assertTrue({response_var}.headers().firstValue(\"{escaped_name}\").isPresent(), \"header {escaped_name} should be present\");"
625 );
626 }
627 "<<absent>>" => {
628 let _ = writeln!(
629 out,
630 " assertTrue({response_var}.headers().firstValue(\"{escaped_name}\").isEmpty(), \"header {escaped_name} should be absent\");"
631 );
632 }
633 "<<uuid>>" => {
634 let _ = writeln!(
635 out,
636 " 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\");"
637 );
638 }
639 literal => {
640 let escaped_value = escape_java(literal);
641 let _ = writeln!(
642 out,
643 " assertTrue({response_var}.headers().firstValue(\"{escaped_name}\").orElse(\"\").contains(\"{escaped_value}\"), \"header {escaped_name} mismatch\");"
644 );
645 }
646 }
647 }
648
649 fn render_assert_json_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
651 match expected {
652 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
653 let json_str = serde_json::to_string(expected).unwrap_or_default();
654 let escaped = escape_java(&json_str);
655 let _ = writeln!(out, " var bodyJson = MAPPER.readTree({response_var}.body());");
656 let _ = writeln!(out, " var expectedJson = MAPPER.readTree(\"{escaped}\");");
657 let _ = writeln!(out, " assertEquals(expectedJson, bodyJson, \"body mismatch\");");
658 }
659 serde_json::Value::String(s) => {
660 let escaped = escape_java(s);
661 let _ = writeln!(
662 out,
663 " assertEquals(\"{escaped}\", {response_var}.body().trim(), \"body mismatch\");"
664 );
665 }
666 other => {
667 let escaped = escape_java(&other.to_string());
668 let _ = writeln!(
669 out,
670 " assertEquals(\"{escaped}\", {response_var}.body().trim(), \"body mismatch\");"
671 );
672 }
673 }
674 }
675
676 fn render_assert_partial_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
678 if let Some(obj) = expected.as_object() {
679 let _ = writeln!(out, " var partialJson = MAPPER.readTree({response_var}.body());");
680 for (key, val) in obj {
681 let escaped_key = escape_java(key);
682 let json_str = serde_json::to_string(val).unwrap_or_default();
683 let escaped_val = escape_java(&json_str);
684 let _ = writeln!(
685 out,
686 " assertEquals(MAPPER.readTree(\"{escaped_val}\"), partialJson.get(\"{escaped_key}\"), \"body field '{escaped_key}' mismatch\");"
687 );
688 }
689 }
690 }
691
692 fn render_assert_validation_errors(
694 &self,
695 out: &mut String,
696 response_var: &str,
697 errors: &[crate::fixture::ValidationErrorExpectation],
698 ) {
699 let _ = writeln!(out, " var veBody = {response_var}.body();");
700 for err in errors {
701 let escaped_msg = escape_java(&err.msg);
702 let _ = writeln!(
703 out,
704 " assertTrue(veBody.contains(\"{escaped_msg}\"), \"expected validation error message: {escaped_msg}\");"
705 );
706 }
707 }
708}
709
710fn render_http_test_method(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
717 if http.expected_response.status_code == 101 {
720 let method_name = fixture.id.to_upper_camel_case();
721 let description = &fixture.description;
722 let _ = writeln!(out, " @Test");
723 let _ = writeln!(out, " void test{method_name}() {{");
724 let _ = writeln!(out, " // {description}");
725 let _ = writeln!(
726 out,
727 " org.junit.jupiter.api.Assumptions.assumeTrue(false, \"Skipped: Java HttpClient cannot handle 101 Switching Protocols responses\");"
728 );
729 let _ = writeln!(out, " }}");
730 return;
731 }
732
733 client::http_call::render_http_test(out, &JavaTestClientRenderer, fixture);
734}
735
736#[allow(clippy::too_many_arguments)]
737fn render_test_method(
738 out: &mut String,
739 fixture: &Fixture,
740 class_name: &str,
741 _function_name: &str,
742 _result_var: &str,
743 _args: &[crate::config::ArgMapping],
744 options_type: Option<&str>,
745 field_resolver: &FieldResolver,
746 result_is_simple: bool,
747 enum_fields: &std::collections::HashMap<String, String>,
748 e2e_config: &E2eConfig,
749 nested_types: &std::collections::HashMap<String, String>,
750 nested_types_optional: bool,
751) {
752 if let Some(http) = &fixture.http {
754 render_http_test_method(out, fixture, http);
755 return;
756 }
757
758 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
760 let lang = "java";
761 let call_overrides = call_config.overrides.get(lang);
762 let effective_function_name = call_overrides
763 .and_then(|o| o.function.as_ref())
764 .cloned()
765 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
766 let effective_result_var = &call_config.result_var;
767 let effective_args = &call_config.args;
768 let function_name = effective_function_name.as_str();
769 let result_var = effective_result_var.as_str();
770 let args: &[crate::config::ArgMapping] = effective_args.as_slice();
771
772 let method_name = fixture.id.to_upper_camel_case();
773 let description = &fixture.description;
774 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
775
776 let effective_options_type: Option<String> = call_overrides
778 .and_then(|o| o.options_type.clone())
779 .or_else(|| options_type.map(|s| s.to_string()));
780 let effective_options_type = effective_options_type.as_deref();
781
782 let effective_result_is_simple =
784 call_overrides.is_some_and(|o| o.result_is_simple) || call_config.result_is_simple || result_is_simple;
785 let effective_result_is_bytes = call_overrides.is_some_and(|o| o.result_is_bytes);
786
787 let needs_deser = effective_options_type.is_some()
790 && args.iter().any(|arg| {
791 if arg.arg_type != "json_object" {
792 return false;
793 }
794 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
795 fixture.input.get(field).is_some_and(|v| !v.is_null() && !v.is_array())
796 });
797
798 let throws_clause = " throws Exception";
800
801 let _ = writeln!(out, " @Test");
802 let _ = writeln!(out, " void test{method_name}(){throws_clause} {{");
803 let _ = writeln!(out, " // {description}");
804
805 if let (true, Some(opts_type)) = (needs_deser, effective_options_type) {
807 for arg in args {
808 if arg.arg_type == "json_object" {
809 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
810 if let Some(val) = fixture.input.get(field) {
811 if !val.is_null() && !val.is_array() {
812 if let Some(obj) = val.as_object() {
813 let empty_path_fields: Vec<String> = Vec::new();
815 let path_fields = call_overrides.map(|o| &o.path_fields).unwrap_or(&empty_path_fields);
816 let builder_expr = java_builder_expression(
817 obj,
818 opts_type,
819 enum_fields,
820 nested_types,
821 nested_types_optional,
822 path_fields,
823 );
824 let var_name = &arg.name;
825 let _ = writeln!(out, " var {var_name} = {builder_expr};");
826 }
827 }
828 }
829 }
830 }
831 }
832
833 let (mut setup_lines, args_str) =
834 build_args_and_setup(&fixture.input, args, class_name, effective_options_type, &fixture.id);
835
836 let mut visitor_var = String::new();
838 let mut has_visitor_fixture = false;
839 if let Some(visitor_spec) = &fixture.visitor {
840 visitor_var = build_java_visitor(&mut setup_lines, visitor_spec, class_name);
841 has_visitor_fixture = true;
842 }
843
844 for line in &setup_lines {
845 let _ = writeln!(out, " {line}");
846 }
847
848 let final_args = if has_visitor_fixture {
850 if args_str.is_empty() {
851 format!("new ConversionOptions().withVisitor({})", visitor_var)
853 } else if args_str.contains("new ConversionOptions")
854 || args_str.contains("ConversionOptionsBuilder")
855 || args_str.contains(".builder()")
856 {
857 if args_str.contains(".build()") {
860 let idx = args_str.rfind(".build()").unwrap();
862 format!("{}.withVisitor({}){}", &args_str[..idx], visitor_var, &args_str[idx..])
863 } else {
864 format!("{}.withVisitor({})", args_str, visitor_var)
866 }
867 } else if args_str.ends_with(", null") {
868 let base = &args_str[..args_str.len() - 6];
870 format!("{}, new ConversionOptions().withVisitor({})", base, visitor_var)
871 } else {
872 format!("{}, new ConversionOptions().withVisitor({})", args_str, visitor_var)
874 }
875 } else {
876 args_str
877 };
878
879 if expects_error {
880 let _ = writeln!(
881 out,
882 " assertThrows(Exception.class, () -> {class_name}.{function_name}({final_args}));"
883 );
884 let _ = writeln!(out, " }}");
885 return;
886 }
887
888 if call_config.returns_void {
889 let _ = writeln!(out, " {class_name}.{function_name}({final_args});");
890 let _ = writeln!(out, " }}");
891 return;
892 }
893
894 let _ = writeln!(
895 out,
896 " var {result_var} = {class_name}.{function_name}({final_args});"
897 );
898
899 let needs_source_var = fixture
901 .assertions
902 .iter()
903 .any(|a| a.assertion_type == "method_result" && a.method.as_deref() == Some("run_query"));
904 if needs_source_var {
905 if let Some(source_arg) = args.iter().find(|a| a.field == "source_code") {
907 let field = source_arg.field.strip_prefix("input.").unwrap_or(&source_arg.field);
908 if let Some(val) = fixture.input.get(field) {
909 let java_val = json_to_java(val);
910 let _ = writeln!(out, " var source = {java_val}.getBytes();");
911 }
912 }
913 }
914
915 for assertion in &fixture.assertions {
916 render_assertion(
917 out,
918 assertion,
919 result_var,
920 class_name,
921 field_resolver,
922 effective_result_is_simple,
923 effective_result_is_bytes,
924 enum_fields,
925 );
926 }
927
928 let _ = writeln!(out, " }}");
929}
930
931fn build_args_and_setup(
935 input: &serde_json::Value,
936 args: &[crate::config::ArgMapping],
937 class_name: &str,
938 options_type: Option<&str>,
939 fixture_id: &str,
940) -> (Vec<String>, String) {
941 if args.is_empty() {
942 return (Vec::new(), String::new());
943 }
944
945 let mut setup_lines: Vec<String> = Vec::new();
946 let mut parts: Vec<String> = Vec::new();
947
948 for arg in args {
949 if arg.arg_type == "mock_url" {
950 setup_lines.push(format!(
951 "String {} = System.getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";",
952 arg.name,
953 ));
954 parts.push(arg.name.clone());
955 continue;
956 }
957
958 if arg.arg_type == "handle" {
959 let constructor_name = format!("create{}", arg.name.to_upper_camel_case());
961 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
962 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
963 if config_value.is_null()
964 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
965 {
966 setup_lines.push(format!("var {} = {class_name}.{constructor_name}(null);", arg.name,));
967 } else {
968 let json_str = serde_json::to_string(config_value).unwrap_or_default();
969 let name = &arg.name;
970 setup_lines.push(format!(
971 "var {name}Config = MAPPER.readValue(\"{}\", CrawlConfig.class);",
972 escape_java(&json_str),
973 ));
974 setup_lines.push(format!(
975 "var {} = {class_name}.{constructor_name}({name}Config);",
976 arg.name,
977 name = name,
978 ));
979 }
980 parts.push(arg.name.clone());
981 continue;
982 }
983
984 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
985 let val = input.get(field);
986 match val {
987 None | Some(serde_json::Value::Null) if arg.optional => {
988 if arg.arg_type == "json_object" {
992 if let Some(opts_type) = options_type {
993 parts.push(format!("{opts_type}.builder().build()"));
994 } else {
995 parts.push("null".to_string());
996 }
997 } else {
998 parts.push("null".to_string());
999 }
1000 }
1001 None | Some(serde_json::Value::Null) => {
1002 let default_val = match arg.arg_type.as_str() {
1004 "string" | "file_path" => "\"\"".to_string(),
1005 "int" | "integer" => "0".to_string(),
1006 "float" | "number" => "0.0d".to_string(),
1007 "bool" | "boolean" => "false".to_string(),
1008 _ => "null".to_string(),
1009 };
1010 parts.push(default_val);
1011 }
1012 Some(v) => {
1013 if arg.arg_type == "json_object" {
1014 if v.is_array() {
1017 if let Some(elem_type) = &arg.element_type {
1018 if elem_type == "BatchBytesItem" || elem_type == "BatchFileItem" {
1019 parts.push(emit_java_batch_item_array(v, elem_type));
1020 continue;
1021 }
1022 }
1023 let elem_type = arg.element_type.as_deref();
1025 parts.push(json_to_java_typed(v, elem_type));
1026 continue;
1027 }
1028 if options_type.is_some() {
1030 parts.push(arg.name.clone());
1031 continue;
1032 }
1033 parts.push(json_to_java(v));
1034 continue;
1035 }
1036 if arg.arg_type == "bytes" {
1038 let val = json_to_java(v);
1039 parts.push(format!("{val}.getBytes()"));
1040 continue;
1041 }
1042 if arg.arg_type == "file_path" {
1044 let val = json_to_java(v);
1045 parts.push(format!("java.nio.file.Path.of({val})"));
1046 continue;
1047 }
1048 parts.push(json_to_java(v));
1049 }
1050 }
1051 }
1052
1053 (setup_lines, parts.join(", "))
1054}
1055
1056#[allow(clippy::too_many_arguments)]
1057fn render_assertion(
1058 out: &mut String,
1059 assertion: &Assertion,
1060 result_var: &str,
1061 class_name: &str,
1062 field_resolver: &FieldResolver,
1063 result_is_simple: bool,
1064 result_is_bytes: bool,
1065 enum_fields: &std::collections::HashMap<String, String>,
1066) {
1067 if let Some(f) = &assertion.field {
1069 match f.as_str() {
1070 "chunks_have_content" => {
1072 let pred = format!(
1073 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.content() != null && !c.content().isBlank())"
1074 );
1075 match assertion.assertion_type.as_str() {
1076 "is_true" => {
1077 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
1078 }
1079 "is_false" => {
1080 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
1081 }
1082 _ => {
1083 let _ = writeln!(
1084 out,
1085 " // skipped: unsupported assertion on synthetic field '{f}'"
1086 );
1087 }
1088 }
1089 return;
1090 }
1091 "chunks_have_heading_context" => {
1092 let pred = format!(
1093 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.metadata().headingContext().isPresent())"
1094 );
1095 match assertion.assertion_type.as_str() {
1096 "is_true" => {
1097 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
1098 }
1099 "is_false" => {
1100 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
1101 }
1102 _ => {
1103 let _ = writeln!(
1104 out,
1105 " // skipped: unsupported assertion on synthetic field '{f}'"
1106 );
1107 }
1108 }
1109 return;
1110 }
1111 "chunks_have_embeddings" => {
1112 let pred = format!(
1113 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.embedding() != null && !c.embedding().isEmpty())"
1114 );
1115 match assertion.assertion_type.as_str() {
1116 "is_true" => {
1117 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
1118 }
1119 "is_false" => {
1120 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
1121 }
1122 _ => {
1123 let _ = writeln!(
1124 out,
1125 " // skipped: unsupported assertion on synthetic field '{f}'"
1126 );
1127 }
1128 }
1129 return;
1130 }
1131 "first_chunk_starts_with_heading" => {
1132 let pred = format!(
1133 "{result_var}.chunks().orElse(java.util.List.of()).stream().findFirst().map(c -> c.metadata().headingContext().isPresent()).orElse(false)"
1134 );
1135 match assertion.assertion_type.as_str() {
1136 "is_true" => {
1137 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
1138 }
1139 "is_false" => {
1140 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
1141 }
1142 _ => {
1143 let _ = writeln!(
1144 out,
1145 " // skipped: unsupported assertion on synthetic field '{f}'"
1146 );
1147 }
1148 }
1149 return;
1150 }
1151 "embedding_dimensions" => {
1155 let embed_list = if result_is_simple {
1157 result_var.to_string()
1158 } else {
1159 format!("{result_var}.embeddings()")
1160 };
1161 let expr = format!("({embed_list}.isEmpty() ? 0 : {embed_list}.get(0).size())");
1162 match assertion.assertion_type.as_str() {
1163 "equals" => {
1164 if let Some(val) = &assertion.value {
1165 let java_val = json_to_java(val);
1166 let _ = writeln!(out, " assertEquals({java_val}, {expr});");
1167 }
1168 }
1169 "greater_than" => {
1170 if let Some(val) = &assertion.value {
1171 let java_val = json_to_java(val);
1172 let _ = writeln!(
1173 out,
1174 " assertTrue({expr} > {java_val}, \"expected > {java_val}\");"
1175 );
1176 }
1177 }
1178 _ => {
1179 let _ = writeln!(out, " // skipped: unsupported assertion on '{f}'");
1180 }
1181 }
1182 return;
1183 }
1184 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1185 let embed_list = if result_is_simple {
1187 result_var.to_string()
1188 } else {
1189 format!("{result_var}.embeddings()")
1190 };
1191 let pred = match f.as_str() {
1192 "embeddings_valid" => {
1193 format!("{embed_list}.stream().allMatch(e -> e != null && !e.isEmpty())")
1194 }
1195 "embeddings_finite" => {
1196 format!("{embed_list}.stream().flatMap(java.util.Collection::stream).allMatch(Float::isFinite)")
1197 }
1198 "embeddings_non_zero" => {
1199 format!("{embed_list}.stream().allMatch(e -> e.stream().anyMatch(v -> v != 0.0f))")
1200 }
1201 "embeddings_normalized" => format!(
1202 "{embed_list}.stream().allMatch(e -> {{ double n = e.stream().mapToDouble(v -> v * v).sum(); return Math.abs(n - 1.0) < 1e-3; }})"
1203 ),
1204 _ => unreachable!(),
1205 };
1206 match assertion.assertion_type.as_str() {
1207 "is_true" => {
1208 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
1209 }
1210 "is_false" => {
1211 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
1212 }
1213 _ => {
1214 let _ = writeln!(out, " // skipped: unsupported assertion on '{f}'");
1215 }
1216 }
1217 return;
1218 }
1219 "keywords" | "keywords_count" => {
1221 let _ = writeln!(
1222 out,
1223 " // skipped: field '{f}' not available on Java ExtractionResult"
1224 );
1225 return;
1226 }
1227 "metadata" => {
1230 match assertion.assertion_type.as_str() {
1231 "not_empty" => {
1232 let _ = writeln!(
1233 out,
1234 " assertTrue({result_var}.metadata().title().isPresent() || {result_var}.metadata().subject().isPresent() || !{result_var}.metadata().additional().isEmpty(), \"expected non-empty value\");"
1235 );
1236 return;
1237 }
1238 "is_empty" => {
1239 let _ = writeln!(
1240 out,
1241 " assertFalse({result_var}.metadata().title().isPresent() || {result_var}.metadata().subject().isPresent() || !{result_var}.metadata().additional().isEmpty(), \"expected empty value\");"
1242 );
1243 return;
1244 }
1245 _ => {} }
1247 }
1248 _ => {}
1249 }
1250 }
1251
1252 if let Some(f) = &assertion.field {
1254 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1255 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1256 return;
1257 }
1258 }
1259
1260 let field_is_enum = assertion
1265 .field
1266 .as_deref()
1267 .is_some_and(|f| enum_fields.contains_key(f) || enum_fields.contains_key(field_resolver.resolve(f)));
1268
1269 let field_is_array = assertion
1273 .field
1274 .as_deref()
1275 .is_some_and(|f| field_resolver.is_array(field_resolver.resolve(f)));
1276
1277 let field_expr = if result_is_simple {
1278 result_var.to_string()
1279 } else {
1280 match &assertion.field {
1281 Some(f) if !f.is_empty() => {
1282 let accessor = field_resolver.accessor(f, "java", result_var);
1283 let resolved = field_resolver.resolve(f);
1284 if field_resolver.is_optional(resolved) && !field_resolver.has_map_access(f) {
1291 let optional_expr = format!("java.util.Optional.ofNullable({accessor})");
1294 match assertion.assertion_type.as_str() {
1295 "not_empty" | "is_empty" => optional_expr,
1298 "count_min" | "count_equals" => {
1300 format!("{optional_expr}.orElse(java.util.List.of())")
1301 }
1302 "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" => {
1304 if field_resolver.is_array(resolved) {
1305 format!("{optional_expr}.orElse(java.util.List.of())")
1306 } else {
1307 format!("{optional_expr}.orElse(0L)")
1308 }
1309 }
1310 "equals" => {
1313 if let Some(expected) = &assertion.value {
1314 if expected.is_number() {
1315 format!("{optional_expr}.orElse(0L)")
1316 } else {
1317 format!("{optional_expr}.orElse(\"\")")
1318 }
1319 } else {
1320 format!("{optional_expr}.orElse(\"\")")
1321 }
1322 }
1323 _ if field_resolver.is_array(resolved) => {
1324 format!("{optional_expr}.orElse(java.util.List.of())")
1325 }
1326 _ => format!("{optional_expr}.orElse(\"\")"),
1327 }
1328 } else {
1329 accessor
1330 }
1331 }
1332 _ => result_var.to_string(),
1333 }
1334 };
1335
1336 let string_expr = if field_is_enum {
1340 format!("{field_expr}.getValue()")
1341 } else {
1342 field_expr.clone()
1343 };
1344
1345 match assertion.assertion_type.as_str() {
1346 "equals" => {
1347 if let Some(expected) = &assertion.value {
1348 let java_val = json_to_java(expected);
1349 if expected.is_string() {
1350 let _ = writeln!(out, " assertEquals({java_val}, {string_expr}.trim());");
1351 } else if expected.is_number() && field_expr.contains(".orElse(\"\")") {
1352 let fixed_expr = field_expr.replace(".orElse(\"\")", ".orElse(0L)");
1356 let _ = writeln!(out, " assertEquals({java_val}, {fixed_expr});");
1357 } else {
1358 let _ = writeln!(out, " assertEquals({java_val}, {field_expr});");
1359 }
1360 }
1361 }
1362 "contains" => {
1363 if let Some(expected) = &assertion.value {
1364 let java_val = json_to_java(expected);
1365 let check_expr = if field_is_array {
1369 format!("{string_expr}.toString()")
1370 } else {
1371 string_expr.clone()
1372 };
1373 let _ = writeln!(
1374 out,
1375 " assertTrue({check_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
1376 );
1377 }
1378 }
1379 "contains_all" => {
1380 if let Some(values) = &assertion.values {
1381 for val in values {
1382 let java_val = json_to_java(val);
1383 let check_expr = if field_is_array {
1384 format!("{string_expr}.toString()")
1385 } else {
1386 string_expr.clone()
1387 };
1388 let _ = writeln!(
1389 out,
1390 " assertTrue({check_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
1391 );
1392 }
1393 }
1394 }
1395 "not_contains" => {
1396 if let Some(expected) = &assertion.value {
1397 let java_val = json_to_java(expected);
1398 let check_expr = if field_is_array {
1399 format!("{string_expr}.toString()")
1400 } else {
1401 string_expr.clone()
1402 };
1403 let _ = writeln!(
1404 out,
1405 " assertFalse({check_expr}.contains({java_val}), \"expected NOT to contain: \" + {java_val});"
1406 );
1407 }
1408 }
1409 "not_empty" => {
1410 let _ = writeln!(
1411 out,
1412 " assertFalse({field_expr} == null || {field_expr}.isEmpty(), \"expected non-empty value\");"
1413 );
1414 }
1415 "is_empty" => {
1416 let _ = writeln!(
1417 out,
1418 " assertTrue({field_expr} == null || {field_expr}.isEmpty(), \"expected empty value\");"
1419 );
1420 }
1421 "contains_any" => {
1422 if let Some(values) = &assertion.values {
1423 let checks: Vec<String> = values
1424 .iter()
1425 .map(|v| {
1426 let java_val = json_to_java(v);
1427 format!("{string_expr}.contains({java_val})")
1428 })
1429 .collect();
1430 let joined = checks.join(" || ");
1431 let _ = writeln!(
1432 out,
1433 " assertTrue({joined}, \"expected to contain at least one of the specified values\");"
1434 );
1435 }
1436 }
1437 "greater_than" => {
1438 if let Some(val) = &assertion.value {
1439 let java_val = json_to_java(val);
1440 let _ = writeln!(
1441 out,
1442 " assertTrue({field_expr} > {java_val}, \"expected > {java_val}\");"
1443 );
1444 }
1445 }
1446 "less_than" => {
1447 if let Some(val) = &assertion.value {
1448 let java_val = json_to_java(val);
1449 let _ = writeln!(
1450 out,
1451 " assertTrue({field_expr} < {java_val}, \"expected < {java_val}\");"
1452 );
1453 }
1454 }
1455 "greater_than_or_equal" => {
1456 if let Some(val) = &assertion.value {
1457 let java_val = json_to_java(val);
1458 let _ = writeln!(
1459 out,
1460 " assertTrue({field_expr} >= {java_val}, \"expected >= {java_val}\");"
1461 );
1462 }
1463 }
1464 "less_than_or_equal" => {
1465 if let Some(val) = &assertion.value {
1466 let java_val = json_to_java(val);
1467 let _ = writeln!(
1468 out,
1469 " assertTrue({field_expr} <= {java_val}, \"expected <= {java_val}\");"
1470 );
1471 }
1472 }
1473 "starts_with" => {
1474 if let Some(expected) = &assertion.value {
1475 let java_val = json_to_java(expected);
1476 let _ = writeln!(
1477 out,
1478 " assertTrue({string_expr}.startsWith({java_val}), \"expected to start with: \" + {java_val});"
1479 );
1480 }
1481 }
1482 "ends_with" => {
1483 if let Some(expected) = &assertion.value {
1484 let java_val = json_to_java(expected);
1485 let _ = writeln!(
1486 out,
1487 " assertTrue({string_expr}.endsWith({java_val}), \"expected to end with: \" + {java_val});"
1488 );
1489 }
1490 }
1491 "min_length" => {
1492 if let Some(val) = &assertion.value {
1493 if let Some(n) = val.as_u64() {
1494 let len_expr = if result_is_bytes {
1496 format!("{field_expr}.length")
1497 } else {
1498 format!("{field_expr}.length()")
1499 };
1500 let _ = writeln!(
1501 out,
1502 " assertTrue({len_expr} >= {n}, \"expected length >= {n}\");"
1503 );
1504 }
1505 }
1506 }
1507 "max_length" => {
1508 if let Some(val) = &assertion.value {
1509 if let Some(n) = val.as_u64() {
1510 let len_expr = if result_is_bytes {
1511 format!("{field_expr}.length")
1512 } else {
1513 format!("{field_expr}.length()")
1514 };
1515 let _ = writeln!(
1516 out,
1517 " assertTrue({len_expr} <= {n}, \"expected length <= {n}\");"
1518 );
1519 }
1520 }
1521 }
1522 "count_min" => {
1523 if let Some(val) = &assertion.value {
1524 if let Some(n) = val.as_u64() {
1525 let _ = writeln!(
1526 out,
1527 " assertTrue({field_expr}.size() >= {n}, \"expected at least {n} elements\");"
1528 );
1529 }
1530 }
1531 }
1532 "count_equals" => {
1533 if let Some(val) = &assertion.value {
1534 if let Some(n) = val.as_u64() {
1535 let _ = writeln!(
1536 out,
1537 " assertEquals({n}, {field_expr}.size(), \"expected exactly {n} elements\");"
1538 );
1539 }
1540 }
1541 }
1542 "is_true" => {
1543 let _ = writeln!(out, " assertTrue({field_expr}, \"expected true\");");
1544 }
1545 "is_false" => {
1546 let _ = writeln!(out, " assertFalse({field_expr}, \"expected false\");");
1547 }
1548 "method_result" => {
1549 if let Some(method_name) = &assertion.method {
1550 let call_expr = build_java_method_call(result_var, method_name, assertion.args.as_ref(), class_name);
1551 let check = assertion.check.as_deref().unwrap_or("is_true");
1552 let method_returns_collection =
1554 matches!(method_name.as_str(), "find_nodes_by_type" | "findNodesByType");
1555 match check {
1556 "equals" => {
1557 if let Some(val) = &assertion.value {
1558 if val.is_boolean() {
1559 if val.as_bool() == Some(true) {
1560 let _ = writeln!(out, " assertTrue({call_expr});");
1561 } else {
1562 let _ = writeln!(out, " assertFalse({call_expr});");
1563 }
1564 } else if method_returns_collection {
1565 let java_val = json_to_java(val);
1566 let _ = writeln!(out, " assertEquals({java_val}, {call_expr}.size());");
1567 } else {
1568 let java_val = json_to_java(val);
1569 let _ = writeln!(out, " assertEquals({java_val}, {call_expr});");
1570 }
1571 }
1572 }
1573 "is_true" => {
1574 let _ = writeln!(out, " assertTrue({call_expr});");
1575 }
1576 "is_false" => {
1577 let _ = writeln!(out, " assertFalse({call_expr});");
1578 }
1579 "greater_than_or_equal" => {
1580 if let Some(val) = &assertion.value {
1581 let n = val.as_u64().unwrap_or(0);
1582 let _ = writeln!(out, " assertTrue({call_expr} >= {n}, \"expected >= {n}\");");
1583 }
1584 }
1585 "count_min" => {
1586 if let Some(val) = &assertion.value {
1587 let n = val.as_u64().unwrap_or(0);
1588 let _ = writeln!(
1589 out,
1590 " assertTrue({call_expr}.size() >= {n}, \"expected at least {n} elements\");"
1591 );
1592 }
1593 }
1594 "is_error" => {
1595 let _ = writeln!(out, " assertThrows(Exception.class, () -> {{ {call_expr}; }});");
1596 }
1597 "contains" => {
1598 if let Some(val) = &assertion.value {
1599 let java_val = json_to_java(val);
1600 let _ = writeln!(
1601 out,
1602 " assertTrue({call_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
1603 );
1604 }
1605 }
1606 other_check => {
1607 panic!("Java e2e generator: unsupported method_result check type: {other_check}");
1608 }
1609 }
1610 } else {
1611 panic!("Java e2e generator: method_result assertion missing 'method' field");
1612 }
1613 }
1614 "matches_regex" => {
1615 if let Some(expected) = &assertion.value {
1616 let java_val = json_to_java(expected);
1617 let _ = writeln!(
1618 out,
1619 " assertTrue({string_expr}.matches({java_val}), \"expected value to match regex: \" + {java_val});"
1620 );
1621 }
1622 }
1623 "not_error" => {
1624 }
1626 "error" => {
1627 }
1629 other => {
1630 panic!("Java e2e generator: unsupported assertion type: {other}");
1631 }
1632 }
1633}
1634
1635fn build_java_method_call(
1639 result_var: &str,
1640 method_name: &str,
1641 args: Option<&serde_json::Value>,
1642 class_name: &str,
1643) -> String {
1644 match method_name {
1645 "root_child_count" => format!("{result_var}.rootNode().childCount()"),
1646 "root_node_type" => format!("{result_var}.rootNode().kind()"),
1647 "named_children_count" => format!("{result_var}.rootNode().namedChildCount()"),
1648 "has_error_nodes" => format!("{class_name}.treeHasErrorNodes({result_var})"),
1649 "error_count" | "tree_error_count" => format!("{class_name}.treeErrorCount({result_var})"),
1650 "tree_to_sexp" => format!("{class_name}.treeToSexp({result_var})"),
1651 "contains_node_type" => {
1652 let node_type = args
1653 .and_then(|a| a.get("node_type"))
1654 .and_then(|v| v.as_str())
1655 .unwrap_or("");
1656 format!("{class_name}.treeContainsNodeType({result_var}, \"{node_type}\")")
1657 }
1658 "find_nodes_by_type" => {
1659 let node_type = args
1660 .and_then(|a| a.get("node_type"))
1661 .and_then(|v| v.as_str())
1662 .unwrap_or("");
1663 format!("{class_name}.findNodesByType({result_var}, \"{node_type}\")")
1664 }
1665 "run_query" => {
1666 let query_source = args
1667 .and_then(|a| a.get("query_source"))
1668 .and_then(|v| v.as_str())
1669 .unwrap_or("");
1670 let language = args
1671 .and_then(|a| a.get("language"))
1672 .and_then(|v| v.as_str())
1673 .unwrap_or("");
1674 let escaped_query = escape_java(query_source);
1675 format!("{class_name}.runQuery({result_var}, \"{language}\", \"{escaped_query}\", source)")
1676 }
1677 _ => {
1678 format!("{result_var}.{}()", method_name.to_lower_camel_case())
1679 }
1680 }
1681}
1682
1683fn json_to_java(value: &serde_json::Value) -> String {
1685 json_to_java_typed(value, None)
1686}
1687
1688fn emit_java_batch_item_array(arr: &serde_json::Value, elem_type: &str) -> String {
1692 if let Some(items) = arr.as_array() {
1693 let item_strs: Vec<String> = items
1694 .iter()
1695 .filter_map(|item| {
1696 if let Some(obj) = item.as_object() {
1697 match elem_type {
1698 "BatchBytesItem" => {
1699 let content = obj.get("content").and_then(|v| v.as_array());
1700 let mime_type = obj.get("mime_type").and_then(|v| v.as_str()).unwrap_or("text/plain");
1701 let content_code = if let Some(arr) = content {
1702 let bytes: Vec<String> = arr
1703 .iter()
1704 .filter_map(|v| v.as_u64().map(|n| format!("(byte) {}", n)))
1705 .collect();
1706 format!("new byte[] {{{}}}", bytes.join(", "))
1707 } else {
1708 "new byte[] {}".to_string()
1709 };
1710 Some(format!("new {}({}, \"{}\", null)", elem_type, content_code, mime_type))
1711 }
1712 "BatchFileItem" => {
1713 let path = obj.get("path").and_then(|v| v.as_str()).unwrap_or("");
1714 Some(format!(
1715 "new {}(java.nio.file.Paths.get(\"{}\"), null)",
1716 elem_type, path
1717 ))
1718 }
1719 _ => None,
1720 }
1721 } else {
1722 None
1723 }
1724 })
1725 .collect();
1726 format!("java.util.Arrays.asList({})", item_strs.join(", "))
1727 } else {
1728 "java.util.List.of()".to_string()
1729 }
1730}
1731
1732fn json_to_java_typed(value: &serde_json::Value, element_type: Option<&str>) -> String {
1733 match value {
1734 serde_json::Value::String(s) => format!("\"{}\"", escape_java(s)),
1735 serde_json::Value::Bool(b) => b.to_string(),
1736 serde_json::Value::Number(n) => {
1737 if n.is_f64() {
1738 match element_type {
1739 Some("f32" | "float" | "Float") => format!("{}f", n),
1740 _ => format!("{}d", n),
1741 }
1742 } else {
1743 n.to_string()
1744 }
1745 }
1746 serde_json::Value::Null => "null".to_string(),
1747 serde_json::Value::Array(arr) => {
1748 let items: Vec<String> = arr.iter().map(|v| json_to_java_typed(v, element_type)).collect();
1749 format!("java.util.List.of({})", items.join(", "))
1750 }
1751 serde_json::Value::Object(_) => {
1752 let json_str = serde_json::to_string(value).unwrap_or_default();
1753 format!("\"{}\"", escape_java(&json_str))
1754 }
1755 }
1756}
1757
1758fn java_builder_expression(
1769 obj: &serde_json::Map<String, serde_json::Value>,
1770 type_name: &str,
1771 enum_fields: &std::collections::HashMap<String, String>,
1772 nested_types: &std::collections::HashMap<String, String>,
1773 nested_types_optional: bool,
1774 path_fields: &[String],
1775) -> String {
1776 let mut expr = format!("{}.builder()", type_name);
1777 for (key, val) in obj {
1778 let camel_key = key.to_lower_camel_case();
1780 let method_name = format!("with{}", camel_key.to_upper_camel_case());
1781
1782 let java_val = match val {
1783 serde_json::Value::String(s) => {
1784 if let Some(enum_type_name) = enum_fields.get(&camel_key) {
1787 let variant_name = s.to_upper_camel_case();
1789 format!("{}.{}", enum_type_name, variant_name)
1790 } else if camel_key == "preset" && type_name == "PreprocessingOptions" {
1791 let variant_name = s.to_upper_camel_case();
1793 format!("PreprocessingPreset.{}", variant_name)
1794 } else if path_fields.contains(key) {
1795 format!("Optional.of(java.nio.file.Path.of(\"{}\"))", escape_java(s))
1797 } else {
1798 format!("\"{}\"", escape_java(s))
1800 }
1801 }
1802 serde_json::Value::Bool(b) => b.to_string(),
1803 serde_json::Value::Null => "null".to_string(),
1804 serde_json::Value::Number(n) => {
1805 let camel_key = key.to_lower_camel_case();
1813 let is_plain_field = matches!(camel_key.as_str(), "listIndentWidth" | "wrapWidth");
1814 let is_primitive_builder = matches!(type_name, "SecurityLimits" | "SecurityLimitsBuilder");
1817
1818 if is_plain_field || is_primitive_builder {
1819 if n.is_f64() {
1821 format!("{}d", n)
1822 } else {
1823 format!("{}L", n)
1824 }
1825 } else {
1826 if n.is_f64() {
1828 format!("Optional.of({}d)", n)
1829 } else {
1830 format!("Optional.of({}L)", n)
1831 }
1832 }
1833 }
1834 serde_json::Value::Array(arr) => {
1835 let items: Vec<String> = arr.iter().map(|v| json_to_java_typed(v, None)).collect();
1836 format!("java.util.List.of({})", items.join(", "))
1837 }
1838 serde_json::Value::Object(nested) => {
1839 let nested_type = nested_types
1841 .get(key.as_str())
1842 .cloned()
1843 .unwrap_or_else(|| format!("{}Options", key.to_upper_camel_case()));
1844 let inner = java_builder_expression(
1845 nested,
1846 &nested_type,
1847 enum_fields,
1848 nested_types,
1849 nested_types_optional,
1850 &[],
1851 );
1852 let is_primitive_builder = matches!(type_name, "SecurityLimits" | "SecurityLimitsBuilder");
1856 if is_primitive_builder || !nested_types_optional {
1857 inner
1858 } else {
1859 format!("Optional.of({inner})")
1860 }
1861 }
1862 };
1863 expr.push_str(&format!(".{}({})", method_name, java_val));
1864 }
1865 expr.push_str(".build()");
1866 expr
1867}
1868
1869fn default_java_nested_types() -> std::collections::HashMap<String, String> {
1876 [
1877 ("chunking", "ChunkingConfig"),
1878 ("ocr", "OcrConfig"),
1879 ("images", "ImageExtractionConfig"),
1880 ("html_output", "HtmlOutputConfig"),
1881 ("language_detection", "LanguageDetectionConfig"),
1882 ("postprocessor", "PostProcessorConfig"),
1883 ("acceleration", "AccelerationConfig"),
1884 ("email", "EmailConfig"),
1885 ("pages", "PageConfig"),
1886 ("pdf_options", "PdfConfig"),
1887 ("layout", "LayoutDetectionConfig"),
1888 ("tree_sitter", "TreeSitterConfig"),
1889 ("structured_extraction", "StructuredExtractionConfig"),
1890 ("content_filter", "ContentFilterConfig"),
1891 ("token_reduction", "TokenReductionOptions"),
1892 ("security_limits", "SecurityLimits"),
1893 ]
1894 .iter()
1895 .map(|(k, v)| (k.to_string(), v.to_string()))
1896 .collect()
1897}
1898
1899fn collect_enum_and_nested_types(
1906 obj: &serde_json::Map<String, serde_json::Value>,
1907 enum_fields: &std::collections::HashMap<String, String>,
1908 types_out: &mut std::collections::BTreeSet<String>,
1909) {
1910 for (key, val) in obj {
1911 let camel_key = key.to_lower_camel_case();
1913 if let Some(enum_type) = enum_fields.get(&camel_key) {
1914 types_out.insert(enum_type.clone());
1916 } else if camel_key == "preset" {
1917 types_out.insert("PreprocessingPreset".to_string());
1919 }
1920 if let Some(nested) = val.as_object() {
1922 collect_enum_and_nested_types(nested, enum_fields, types_out);
1923 }
1924 }
1925}
1926
1927fn collect_nested_type_names(
1928 obj: &serde_json::Map<String, serde_json::Value>,
1929 nested_types: &std::collections::HashMap<String, String>,
1930 types_out: &mut std::collections::BTreeSet<String>,
1931) {
1932 for (key, val) in obj {
1933 if let Some(type_name) = nested_types.get(key.as_str()) {
1934 types_out.insert(type_name.clone());
1935 }
1936 if let Some(nested) = val.as_object() {
1937 collect_nested_type_names(nested, nested_types, types_out);
1938 }
1939 }
1940}
1941
1942fn build_java_visitor(
1948 setup_lines: &mut Vec<String>,
1949 visitor_spec: &crate::fixture::VisitorSpec,
1950 class_name: &str,
1951) -> String {
1952 setup_lines.push("class _TestVisitor implements Visitor {".to_string());
1953 for (method_name, action) in &visitor_spec.callbacks {
1954 emit_java_visitor_method(setup_lines, method_name, action, class_name);
1955 }
1956 setup_lines.push("}".to_string());
1957 setup_lines.push("var visitor = new _TestVisitor();".to_string());
1958 "visitor".to_string()
1959}
1960
1961fn emit_java_visitor_method(
1963 setup_lines: &mut Vec<String>,
1964 method_name: &str,
1965 action: &CallbackAction,
1966 _class_name: &str,
1967) {
1968 let camel_method = method_to_camel(method_name);
1969 let params = match method_name {
1970 "visit_link" => "NodeContext ctx, String href, String text, String title",
1971 "visit_image" => "NodeContext ctx, String src, String alt, String title",
1972 "visit_heading" => "NodeContext ctx, int level, String text, String id",
1973 "visit_code_block" => "NodeContext ctx, String lang, String code",
1974 "visit_code_inline"
1975 | "visit_strong"
1976 | "visit_emphasis"
1977 | "visit_strikethrough"
1978 | "visit_underline"
1979 | "visit_subscript"
1980 | "visit_superscript"
1981 | "visit_mark"
1982 | "visit_button"
1983 | "visit_summary"
1984 | "visit_figcaption"
1985 | "visit_definition_term"
1986 | "visit_definition_description" => "NodeContext ctx, String text",
1987 "visit_text" => "NodeContext ctx, String text",
1988 "visit_list_item" => "NodeContext ctx, boolean ordered, String marker, String text",
1989 "visit_blockquote" => "NodeContext ctx, String content, long depth",
1990 "visit_table_row" => "NodeContext ctx, java.util.List<String> cells, boolean isHeader",
1991 "visit_custom_element" => "NodeContext ctx, String tagName, String html",
1992 "visit_form" => "NodeContext ctx, String actionUrl, String method",
1993 "visit_input" => "NodeContext ctx, String inputType, String name, String value",
1994 "visit_audio" | "visit_video" | "visit_iframe" => "NodeContext ctx, String src",
1995 "visit_details" => "NodeContext ctx, boolean isOpen",
1996 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1997 "NodeContext ctx, String output"
1998 }
1999 "visit_list_start" => "NodeContext ctx, boolean ordered",
2000 "visit_list_end" => "NodeContext ctx, boolean ordered, String output",
2001 _ => "NodeContext ctx",
2002 };
2003
2004 setup_lines.push(format!(" @Override public VisitResult {camel_method}({params}) {{"));
2005 match action {
2006 CallbackAction::Skip => {
2007 setup_lines.push(" return VisitResult.skip();".to_string());
2008 }
2009 CallbackAction::Continue => {
2010 setup_lines.push(" return VisitResult.continue_();".to_string());
2011 }
2012 CallbackAction::PreserveHtml => {
2013 setup_lines.push(" return VisitResult.preserveHtml();".to_string());
2014 }
2015 CallbackAction::Custom { output } => {
2016 let escaped = escape_java(output);
2017 setup_lines.push(format!(" return VisitResult.custom(\"{escaped}\");"));
2018 }
2019 CallbackAction::CustomTemplate { template } => {
2020 let mut format_str = String::with_capacity(template.len());
2024 let mut format_args: Vec<String> = Vec::new();
2025 let mut chars = template.chars().peekable();
2026 while let Some(ch) = chars.next() {
2027 if ch == '{' {
2028 let mut name = String::new();
2030 let mut closed = false;
2031 for inner in chars.by_ref() {
2032 if inner == '}' {
2033 closed = true;
2034 break;
2035 }
2036 name.push(inner);
2037 }
2038 if closed && !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
2039 let camel_name = name.as_str().to_lower_camel_case();
2040 format_args.push(camel_name);
2041 format_str.push_str("%s");
2042 } else {
2043 format_str.push('{');
2045 format_str.push_str(&name);
2046 if closed {
2047 format_str.push('}');
2048 }
2049 }
2050 } else {
2051 format_str.push(ch);
2052 }
2053 }
2054 let escaped = escape_java(&format_str);
2055 if format_args.is_empty() {
2056 setup_lines.push(format!(" return VisitResult.custom(\"{escaped}\");"));
2057 } else {
2058 let args_str = format_args.join(", ");
2059 setup_lines.push(format!(
2060 " return VisitResult.custom(String.format(\"{escaped}\", {args_str}));"
2061 ));
2062 }
2063 }
2064 }
2065 setup_lines.push(" }".to_string());
2066}
2067
2068fn method_to_camel(snake: &str) -> String {
2070 snake.to_lower_camel_case()
2071}