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