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=${{project.basedir}}/../../target/release</argLine>
237 <workingDirectory>${{project.basedir}}/../../test_documents</workingDirectory>
238 </configuration>
239 </plugin>
240 </plugins>
241 </build>
242</project>
243"#,
244 junit = tv::maven::JUNIT,
245 jackson = tv::maven::JACKSON_E2E,
246 build_helper = tv::maven::BUILD_HELPER_MAVEN_PLUGIN,
247 maven_surefire = tv::maven::MAVEN_SUREFIRE_PLUGIN_E2E,
248 )
249}
250
251#[allow(clippy::too_many_arguments)]
252fn render_test_file(
253 category: &str,
254 fixtures: &[&Fixture],
255 class_name: &str,
256 function_name: &str,
257 java_group_id: &str,
258 result_var: &str,
259 args: &[crate::config::ArgMapping],
260 options_type: Option<&str>,
261 field_resolver: &FieldResolver,
262 result_is_simple: bool,
263 enum_fields: &HashSet<String>,
264 e2e_config: &E2eConfig,
265) -> String {
266 let mut out = String::new();
267 out.push_str(&hash::header(CommentStyle::DoubleSlash));
268 let test_class_name = format!("{}Test", sanitize_filename(category).to_upper_camel_case());
269
270 let (import_path, simple_class) = if class_name.contains('.') {
273 let simple = class_name.rsplit('.').next().unwrap_or(class_name);
274 (class_name, simple)
275 } else {
276 ("", class_name)
277 };
278
279 let _ = writeln!(out, "package {java_group_id}.e2e;");
280 let _ = writeln!(out);
281
282 let lang_for_om = "java";
286 let needs_object_mapper_for_options = fixtures.iter().any(|f| {
287 let call_cfg = e2e_config.resolve_call(f.call.as_deref());
288 let eff_opts = call_cfg
289 .overrides
290 .get(lang_for_om)
291 .and_then(|o| o.options_type.as_deref())
292 .or(options_type);
293 if eff_opts.is_none() {
294 return false;
295 }
296 call_cfg.args.iter().any(|arg| {
297 if arg.arg_type != "json_object" {
298 return false;
299 }
300 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
301 let val = f.input.get(field);
302 match val {
305 None | Some(serde_json::Value::Null) => arg.optional, Some(v) => !v.is_array(), }
308 })
309 });
310 let needs_object_mapper_for_handle = fixtures.iter().any(|f| {
312 args.iter().filter(|a| a.arg_type == "handle").any(|a| {
313 let v = f.input.get(&a.field).unwrap_or(&serde_json::Value::Null);
314 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
315 })
316 });
317 let has_http_fixtures = fixtures.iter().any(|f| f.http.is_some());
319 let needs_object_mapper = needs_object_mapper_for_options || needs_object_mapper_for_handle || has_http_fixtures;
320
321 let mut all_options_types: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
323 if let Some(t) = options_type {
324 all_options_types.insert(t.to_string());
325 }
326 for f in fixtures.iter() {
327 let call_cfg = e2e_config.resolve_call(f.call.as_deref());
328 if let Some(ov) = call_cfg.overrides.get(lang_for_om) {
329 if let Some(t) = &ov.options_type {
330 all_options_types.insert(t.clone());
331 }
332 }
333 }
334
335 let _ = writeln!(out, "import org.junit.jupiter.api.Test;");
336 let _ = writeln!(out, "import static org.junit.jupiter.api.Assertions.*;");
337 if !import_path.is_empty() {
338 let _ = writeln!(out, "import {import_path};");
339 }
340 if needs_object_mapper {
341 let _ = writeln!(out, "import com.fasterxml.jackson.databind.ObjectMapper;");
342 let _ = writeln!(out, "import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;");
343 }
344 if needs_object_mapper && !all_options_types.is_empty() {
346 let opts_pkg = if !import_path.is_empty() {
347 import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("")
348 } else {
349 ""
350 };
351 for opts_type in &all_options_types {
352 let qualified = if opts_pkg.is_empty() {
353 opts_type.clone()
354 } else {
355 format!("{opts_pkg}.{opts_type}")
356 };
357 let _ = writeln!(out, "import {qualified};");
358 }
359 }
360 if needs_object_mapper_for_handle && !import_path.is_empty() {
362 let pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
363 let _ = writeln!(out, "import {pkg}.CrawlConfig;");
364 }
365 let _ = writeln!(out);
366
367 let _ = writeln!(out, "/** E2e tests for category: {category}. */");
368 let _ = writeln!(out, "class {test_class_name} {{");
369
370 if needs_object_mapper {
371 let _ = writeln!(out);
372 let _ = writeln!(
373 out,
374 " private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());"
375 );
376 }
377
378 for fixture in fixtures {
379 render_test_method(
380 &mut out,
381 fixture,
382 simple_class,
383 function_name,
384 result_var,
385 args,
386 options_type,
387 field_resolver,
388 result_is_simple,
389 enum_fields,
390 e2e_config,
391 );
392 let _ = writeln!(out);
393 }
394
395 let _ = writeln!(out, "}}");
396 out
397}
398
399fn render_http_test_method(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
404 let method_name = fixture.id.to_upper_camel_case();
405 let description = &fixture.description;
406 let request = &http.request;
407 let expected = &http.expected_response;
408 let method = request.method.to_uppercase();
409 let fixture_id = &fixture.id;
410 let expected_status = expected.status_code;
411
412 if expected_status == 101 {
415 let _ = writeln!(out, " @Test");
416 let _ = writeln!(out, " void test{method_name}() {{");
417 let _ = writeln!(out, " // {description}");
418 let _ = writeln!(
419 out,
420 " org.junit.jupiter.api.Assumptions.assumeTrue(false, \"Skipped: Java HttpClient cannot handle 101 Switching Protocols responses\");"
421 );
422 let _ = writeln!(out, " }}");
423 return;
424 }
425
426 let _ = writeln!(out, " @Test");
427 let _ = writeln!(out, " void test{method_name}() throws Exception {{");
428 let _ = writeln!(out, " // {description}");
429 let _ = writeln!(out, " String baseUrl = System.getenv(\"MOCK_SERVER_URL\");",);
430 let _ = writeln!(out, " if (baseUrl == null) baseUrl = \"http://localhost:8080\";");
431
432 let _ = writeln!(
435 out,
436 " java.net.URI uri = java.net.URI.create(baseUrl + \"/fixtures/{fixture_id}\");"
437 );
438
439 let body_publisher = if let Some(body) = &request.body {
441 let json = serde_json::to_string(body).unwrap_or_default();
442 let escaped = escape_java(&json);
443 format!("java.net.http.HttpRequest.BodyPublishers.ofString(\"{escaped}\")")
444 } else {
445 "java.net.http.HttpRequest.BodyPublishers.noBody()".to_string()
446 };
447
448 let _ = writeln!(out, " var builder = java.net.http.HttpRequest.newBuilder(uri)");
449 let _ = writeln!(out, " .method(\"{method}\", {body_publisher});");
450
451 const JAVA_RESTRICTED_HEADERS: &[&str] = &["connection", "content-length", "expect", "host", "upgrade"];
453
454 let content_type = request.content_type.as_deref().unwrap_or("application/json");
456 if request.body.is_some() {
457 let _ = writeln!(
458 out,
459 " builder = builder.header(\"Content-Type\", \"{content_type}\");"
460 );
461 }
462 for (name, value) in &request.headers {
463 if JAVA_RESTRICTED_HEADERS.contains(&name.to_lowercase().as_str()) {
465 continue;
466 }
467 let escaped_name = escape_java(name);
468 let escaped_value = escape_java(value);
469 let _ = writeln!(
470 out,
471 " builder = builder.header(\"{escaped_name}\", \"{escaped_value}\");"
472 );
473 }
474
475 if !request.cookies.is_empty() {
477 let cookie_str: Vec<String> = request.cookies.iter().map(|(k, v)| format!("{k}={v}")).collect();
478 let cookie_header = escape_java(&cookie_str.join("; "));
479 let _ = writeln!(
480 out,
481 " builder = builder.header(\"Cookie\", \"{cookie_header}\");"
482 );
483 }
484
485 let _ = writeln!(out, " var response = java.net.http.HttpClient.newHttpClient()");
486 let _ = writeln!(
487 out,
488 " .send(builder.build(), java.net.http.HttpResponse.BodyHandlers.ofString());"
489 );
490
491 let _ = writeln!(
493 out,
494 " assertEquals({expected_status}, response.statusCode(), \"status code mismatch\");"
495 );
496
497 if let Some(expected_body) = &expected.body {
499 match expected_body {
500 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
501 let json_str = serde_json::to_string(expected_body).unwrap_or_default();
502 let escaped = escape_java(&json_str);
503 let _ = writeln!(out, " var bodyJson = MAPPER.readTree(response.body());");
504 let _ = writeln!(out, " var expectedJson = MAPPER.readTree(\"{escaped}\");");
505 let _ = writeln!(out, " assertEquals(expectedJson, bodyJson, \"body mismatch\");");
506 }
507 serde_json::Value::String(s) => {
508 let escaped = escape_java(s);
509 let _ = writeln!(
510 out,
511 " assertEquals(\"{escaped}\", response.body().trim(), \"body mismatch\");"
512 );
513 }
514 other => {
515 let escaped = escape_java(&other.to_string());
516 let _ = writeln!(
517 out,
518 " assertEquals(\"{escaped}\", response.body().trim(), \"body mismatch\");"
519 );
520 }
521 }
522 }
523
524 for (name, value) in &expected.headers {
526 if value == "<<absent>>" || value == "<<present>>" || value == "<<uuid>>" {
527 continue;
529 }
530 if name.to_lowercase() == "content-encoding" {
533 continue;
534 }
535 let escaped_name = escape_java(name);
536 let escaped_value = escape_java(value);
537 let _ = writeln!(
538 out,
539 " assertTrue(response.headers().firstValue(\"{escaped_name}\").orElse(\"\").contains(\"{escaped_value}\"), \"header {escaped_name} mismatch\");"
540 );
541 }
542
543 let _ = writeln!(out, " }}");
544}
545
546#[allow(clippy::too_many_arguments)]
547fn render_test_method(
548 out: &mut String,
549 fixture: &Fixture,
550 class_name: &str,
551 _function_name: &str,
552 _result_var: &str,
553 _args: &[crate::config::ArgMapping],
554 options_type: Option<&str>,
555 field_resolver: &FieldResolver,
556 result_is_simple: bool,
557 enum_fields: &HashSet<String>,
558 e2e_config: &E2eConfig,
559) {
560 if let Some(http) = &fixture.http {
562 render_http_test_method(out, fixture, http);
563 return;
564 }
565
566 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
568 let lang = "java";
569 let call_overrides = call_config.overrides.get(lang);
570 let effective_function_name = call_overrides
571 .and_then(|o| o.function.as_ref())
572 .cloned()
573 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
574 let effective_result_var = &call_config.result_var;
575 let effective_args = &call_config.args;
576 let function_name = effective_function_name.as_str();
577 let result_var = effective_result_var.as_str();
578 let args: &[crate::config::ArgMapping] = effective_args.as_slice();
579
580 let method_name = fixture.id.to_upper_camel_case();
581 let description = &fixture.description;
582 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
583
584 if call_overrides.is_none() {
586 let _ = writeln!(out, " @Test");
587 let _ = writeln!(out, " void test{method_name}() {{");
588 let _ = writeln!(out, " // {description}");
589 let _ = writeln!(
590 out,
591 " org.junit.jupiter.api.Assumptions.assumeTrue(false, \"TODO: implement Java e2e test for fixture '{}'\");",
592 fixture.id
593 );
594 let _ = writeln!(out, " }}");
595 return;
596 }
597
598 let effective_options_type: Option<String> = call_overrides
600 .and_then(|o| o.options_type.clone())
601 .or_else(|| options_type.map(|s| s.to_string()));
602 let effective_options_type = effective_options_type.as_deref();
603
604 let effective_result_is_simple = call_overrides.is_some_and(|o| o.result_is_simple) || result_is_simple;
606 let effective_result_is_bytes = call_overrides.is_some_and(|o| o.result_is_bytes);
607
608 let needs_deser = effective_options_type.is_some()
611 && args.iter().any(|arg| {
612 if arg.arg_type != "json_object" {
613 return false;
614 }
615 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
616 fixture.input.get(field).is_some_and(|v| !v.is_null() && !v.is_array())
617 });
618
619 let throws_clause = " throws Exception";
621
622 let _ = writeln!(out, " @Test");
623 let _ = writeln!(out, " void test{method_name}(){throws_clause} {{");
624 let _ = writeln!(out, " // {description}");
625
626 if let (true, Some(opts_type)) = (needs_deser, effective_options_type) {
628 for arg in args {
629 if arg.arg_type == "json_object" {
630 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
631 if let Some(val) = fixture.input.get(field) {
632 if !val.is_null() && !val.is_array() {
633 let normalized = super::normalize_json_keys_to_snake_case(val);
637 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
638 let var_name = &arg.name;
639 let _ = writeln!(
640 out,
641 " var {var_name} = MAPPER.readValue(\"{}\", {opts_type}.class);",
642 escape_java(&json_str)
643 );
644 }
645 }
646 }
647 }
648 }
649
650 let (mut setup_lines, args_str) =
651 build_args_and_setup(&fixture.input, args, class_name, effective_options_type, &fixture.id);
652
653 let mut visitor_arg = String::new();
655 if let Some(visitor_spec) = &fixture.visitor {
656 visitor_arg = build_java_visitor(&mut setup_lines, visitor_spec, class_name);
657 }
658
659 for line in &setup_lines {
660 let _ = writeln!(out, " {line}");
661 }
662
663 let final_args = if visitor_arg.is_empty() {
664 args_str
665 } else {
666 format!("{args_str}, {visitor_arg}")
667 };
668
669 if expects_error {
670 let _ = writeln!(
671 out,
672 " assertThrows(Exception.class, () -> {class_name}.{function_name}({final_args}));"
673 );
674 let _ = writeln!(out, " }}");
675 return;
676 }
677
678 let _ = writeln!(
679 out,
680 " var {result_var} = {class_name}.{function_name}({final_args});"
681 );
682
683 let needs_source_var = fixture
685 .assertions
686 .iter()
687 .any(|a| a.assertion_type == "method_result" && a.method.as_deref() == Some("run_query"));
688 if needs_source_var {
689 if let Some(source_arg) = args.iter().find(|a| a.field == "source_code") {
691 let field = source_arg.field.strip_prefix("input.").unwrap_or(&source_arg.field);
692 if let Some(val) = fixture.input.get(field) {
693 let java_val = json_to_java(val);
694 let _ = writeln!(out, " var source = {java_val}.getBytes();");
695 }
696 }
697 }
698
699 for assertion in &fixture.assertions {
700 render_assertion(
701 out,
702 assertion,
703 result_var,
704 class_name,
705 field_resolver,
706 effective_result_is_simple,
707 effective_result_is_bytes,
708 enum_fields,
709 );
710 }
711
712 let _ = writeln!(out, " }}");
713}
714
715fn build_args_and_setup(
719 input: &serde_json::Value,
720 args: &[crate::config::ArgMapping],
721 class_name: &str,
722 options_type: Option<&str>,
723 fixture_id: &str,
724) -> (Vec<String>, String) {
725 if args.is_empty() {
726 return (Vec::new(), String::new());
727 }
728
729 let mut setup_lines: Vec<String> = Vec::new();
730 let mut parts: Vec<String> = Vec::new();
731
732 for arg in args {
733 if arg.arg_type == "mock_url" {
734 setup_lines.push(format!(
735 "String {} = System.getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";",
736 arg.name,
737 ));
738 parts.push(arg.name.clone());
739 continue;
740 }
741
742 if arg.arg_type == "handle" {
743 let constructor_name = format!("create{}", arg.name.to_upper_camel_case());
745 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
746 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
747 if config_value.is_null()
748 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
749 {
750 setup_lines.push(format!("var {} = {class_name}.{constructor_name}(null);", arg.name,));
751 } else {
752 let json_str = serde_json::to_string(config_value).unwrap_or_default();
753 let name = &arg.name;
754 setup_lines.push(format!(
755 "var {name}Config = MAPPER.readValue(\"{}\", CrawlConfig.class);",
756 escape_java(&json_str),
757 ));
758 setup_lines.push(format!(
759 "var {} = {class_name}.{constructor_name}({name}Config);",
760 arg.name,
761 name = name,
762 ));
763 }
764 parts.push(arg.name.clone());
765 continue;
766 }
767
768 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
769 let val = input.get(field);
770 match val {
771 None | Some(serde_json::Value::Null) if arg.optional => {
772 if arg.arg_type == "json_object" {
776 if let Some(opts_type) = options_type {
777 parts.push(format!("MAPPER.readValue(\"{{}}\", {opts_type}.class)"));
778 } else {
779 parts.push("null".to_string());
780 }
781 } else {
782 parts.push("null".to_string());
783 }
784 }
785 None | Some(serde_json::Value::Null) => {
786 let default_val = match arg.arg_type.as_str() {
788 "string" | "file_path" => "\"\"".to_string(),
789 "int" | "integer" => "0".to_string(),
790 "float" | "number" => "0.0d".to_string(),
791 "bool" | "boolean" => "false".to_string(),
792 _ => "null".to_string(),
793 };
794 parts.push(default_val);
795 }
796 Some(v) => {
797 if arg.arg_type == "json_object" {
798 if v.is_array() {
801 let elem_type = arg.element_type.as_deref();
802 parts.push(json_to_java_typed(v, elem_type));
803 continue;
804 }
805 if options_type.is_some() {
807 parts.push(arg.name.clone());
808 continue;
809 }
810 parts.push(json_to_java(v));
811 continue;
812 }
813 if arg.arg_type == "bytes" {
815 let val = json_to_java(v);
816 parts.push(format!("{val}.getBytes()"));
817 continue;
818 }
819 if arg.arg_type == "file_path" {
821 let val = json_to_java(v);
822 parts.push(format!("java.nio.file.Path.of({val})"));
823 continue;
824 }
825 parts.push(json_to_java(v));
826 }
827 }
828 }
829
830 (setup_lines, parts.join(", "))
831}
832
833#[allow(clippy::too_many_arguments)]
834fn render_assertion(
835 out: &mut String,
836 assertion: &Assertion,
837 result_var: &str,
838 class_name: &str,
839 field_resolver: &FieldResolver,
840 result_is_simple: bool,
841 result_is_bytes: bool,
842 enum_fields: &HashSet<String>,
843) {
844 if let Some(f) = &assertion.field {
846 match f.as_str() {
847 "chunks_have_content" => {
849 let pred = format!(
850 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.content() != null && !c.content().isBlank())"
851 );
852 match assertion.assertion_type.as_str() {
853 "is_true" => {
854 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
855 }
856 "is_false" => {
857 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
858 }
859 _ => {
860 let _ = writeln!(
861 out,
862 " // skipped: unsupported assertion on synthetic field '{f}'"
863 );
864 }
865 }
866 return;
867 }
868 "chunks_have_heading_context" => {
869 let pred = format!(
870 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.metadata().headingContext().isPresent())"
871 );
872 match assertion.assertion_type.as_str() {
873 "is_true" => {
874 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
875 }
876 "is_false" => {
877 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
878 }
879 _ => {
880 let _ = writeln!(
881 out,
882 " // skipped: unsupported assertion on synthetic field '{f}'"
883 );
884 }
885 }
886 return;
887 }
888 "chunks_have_embeddings" => {
889 let pred = format!(
890 "{result_var}.chunks().orElse(java.util.List.of()).stream().allMatch(c -> c.embedding() != null && !c.embedding().isEmpty())"
891 );
892 match assertion.assertion_type.as_str() {
893 "is_true" => {
894 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
895 }
896 "is_false" => {
897 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
898 }
899 _ => {
900 let _ = writeln!(
901 out,
902 " // skipped: unsupported assertion on synthetic field '{f}'"
903 );
904 }
905 }
906 return;
907 }
908 "first_chunk_starts_with_heading" => {
909 let pred = format!(
910 "{result_var}.chunks().orElse(java.util.List.of()).stream().findFirst().map(c -> c.metadata().headingContext().isPresent()).orElse(false)"
911 );
912 match assertion.assertion_type.as_str() {
913 "is_true" => {
914 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
915 }
916 "is_false" => {
917 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
918 }
919 _ => {
920 let _ = writeln!(
921 out,
922 " // skipped: unsupported assertion on synthetic field '{f}'"
923 );
924 }
925 }
926 return;
927 }
928 "embedding_dimensions" => {
932 let embed_list = if result_is_simple {
934 result_var.to_string()
935 } else {
936 format!("{result_var}.embeddings()")
937 };
938 let expr = format!("({embed_list}.isEmpty() ? 0 : {embed_list}.get(0).size())");
939 match assertion.assertion_type.as_str() {
940 "equals" => {
941 if let Some(val) = &assertion.value {
942 let java_val = json_to_java(val);
943 let _ = writeln!(out, " assertEquals({java_val}, {expr});");
944 }
945 }
946 "greater_than" => {
947 if let Some(val) = &assertion.value {
948 let java_val = json_to_java(val);
949 let _ = writeln!(
950 out,
951 " assertTrue({expr} > {java_val}, \"expected > {java_val}\");"
952 );
953 }
954 }
955 _ => {
956 let _ = writeln!(out, " // skipped: unsupported assertion on '{f}'");
957 }
958 }
959 return;
960 }
961 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
962 let embed_list = if result_is_simple {
964 result_var.to_string()
965 } else {
966 format!("{result_var}.embeddings()")
967 };
968 let pred = match f.as_str() {
969 "embeddings_valid" => {
970 format!("{embed_list}.stream().allMatch(e -> e != null && !e.isEmpty())")
971 }
972 "embeddings_finite" => {
973 format!("{embed_list}.stream().flatMap(java.util.Collection::stream).allMatch(Float::isFinite)")
974 }
975 "embeddings_non_zero" => {
976 format!("{embed_list}.stream().allMatch(e -> e.stream().anyMatch(v -> v != 0.0f))")
977 }
978 "embeddings_normalized" => format!(
979 "{embed_list}.stream().allMatch(e -> {{ double n = e.stream().mapToDouble(v -> v * v).sum(); return Math.abs(n - 1.0) < 1e-3; }})"
980 ),
981 _ => unreachable!(),
982 };
983 match assertion.assertion_type.as_str() {
984 "is_true" => {
985 let _ = writeln!(out, " assertTrue({pred}, \"expected true\");");
986 }
987 "is_false" => {
988 let _ = writeln!(out, " assertFalse({pred}, \"expected false\");");
989 }
990 _ => {
991 let _ = writeln!(out, " // skipped: unsupported assertion on '{f}'");
992 }
993 }
994 return;
995 }
996 "keywords" | "keywords_count" => {
998 let _ = writeln!(
999 out,
1000 " // skipped: field '{f}' not available on Java ExtractionResult"
1001 );
1002 return;
1003 }
1004 "metadata" => {
1007 match assertion.assertion_type.as_str() {
1008 "not_empty" => {
1009 let _ = writeln!(
1010 out,
1011 " assertTrue({result_var}.metadata().title().isPresent() || {result_var}.metadata().subject().isPresent() || !{result_var}.metadata().additional().isEmpty(), \"expected non-empty value\");"
1012 );
1013 return;
1014 }
1015 "is_empty" => {
1016 let _ = writeln!(
1017 out,
1018 " assertFalse({result_var}.metadata().title().isPresent() || {result_var}.metadata().subject().isPresent() || !{result_var}.metadata().additional().isEmpty(), \"expected empty value\");"
1019 );
1020 return;
1021 }
1022 _ => {} }
1024 }
1025 _ => {}
1026 }
1027 }
1028
1029 if let Some(f) = &assertion.field {
1031 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1032 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1033 return;
1034 }
1035 }
1036
1037 let field_is_enum = assertion
1042 .field
1043 .as_deref()
1044 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1045
1046 let field_expr = if result_is_simple {
1047 result_var.to_string()
1048 } else {
1049 match &assertion.field {
1050 Some(f) if !f.is_empty() => {
1051 let accessor = field_resolver.accessor(f, "java", result_var);
1052 let resolved = field_resolver.resolve(f);
1053 if field_resolver.is_optional(resolved) && !field_resolver.has_map_access(f) {
1056 match assertion.assertion_type.as_str() {
1058 "not_empty" | "is_empty" => accessor,
1061 "count_min" | "count_equals" => {
1063 format!("{accessor}.orElse(java.util.List.of())")
1064 }
1065 "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" => {
1067 if field_resolver.is_array(resolved) {
1068 format!("{accessor}.orElse(java.util.List.of())")
1069 } else {
1070 format!("{accessor}.orElse(0L)")
1071 }
1072 }
1073 _ if field_resolver.is_array(resolved) => {
1074 format!("{accessor}.orElse(java.util.List.of())")
1075 }
1076 _ => format!("{accessor}.orElse(\"\")"),
1077 }
1078 } else {
1079 accessor
1080 }
1081 }
1082 _ => result_var.to_string(),
1083 }
1084 };
1085
1086 let string_expr = if field_is_enum {
1090 format!("{field_expr}.getValue()")
1091 } else {
1092 field_expr.clone()
1093 };
1094
1095 match assertion.assertion_type.as_str() {
1096 "equals" => {
1097 if let Some(expected) = &assertion.value {
1098 let java_val = json_to_java(expected);
1099 if expected.is_string() {
1100 let _ = writeln!(out, " assertEquals({java_val}, {string_expr}.trim());");
1101 } else {
1102 let _ = writeln!(out, " assertEquals({java_val}, {field_expr});");
1103 }
1104 }
1105 }
1106 "contains" => {
1107 if let Some(expected) = &assertion.value {
1108 let java_val = json_to_java(expected);
1109 let _ = writeln!(
1110 out,
1111 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
1112 );
1113 }
1114 }
1115 "contains_all" => {
1116 if let Some(values) = &assertion.values {
1117 for val in values {
1118 let java_val = json_to_java(val);
1119 let _ = writeln!(
1120 out,
1121 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
1122 );
1123 }
1124 }
1125 }
1126 "not_contains" => {
1127 if let Some(expected) = &assertion.value {
1128 let java_val = json_to_java(expected);
1129 let _ = writeln!(
1130 out,
1131 " assertFalse({string_expr}.contains({java_val}), \"expected NOT to contain: \" + {java_val});"
1132 );
1133 }
1134 }
1135 "not_empty" => {
1136 let _ = writeln!(
1137 out,
1138 " assertFalse({field_expr}.isEmpty(), \"expected non-empty value\");"
1139 );
1140 }
1141 "is_empty" => {
1142 let _ = writeln!(
1143 out,
1144 " assertTrue({field_expr}.isEmpty(), \"expected empty value\");"
1145 );
1146 }
1147 "contains_any" => {
1148 if let Some(values) = &assertion.values {
1149 let checks: Vec<String> = values
1150 .iter()
1151 .map(|v| {
1152 let java_val = json_to_java(v);
1153 format!("{string_expr}.contains({java_val})")
1154 })
1155 .collect();
1156 let joined = checks.join(" || ");
1157 let _ = writeln!(
1158 out,
1159 " assertTrue({joined}, \"expected to contain at least one of the specified values\");"
1160 );
1161 }
1162 }
1163 "greater_than" => {
1164 if let Some(val) = &assertion.value {
1165 let java_val = json_to_java(val);
1166 let _ = writeln!(
1167 out,
1168 " assertTrue({field_expr} > {java_val}, \"expected > {java_val}\");"
1169 );
1170 }
1171 }
1172 "less_than" => {
1173 if let Some(val) = &assertion.value {
1174 let java_val = json_to_java(val);
1175 let _ = writeln!(
1176 out,
1177 " assertTrue({field_expr} < {java_val}, \"expected < {java_val}\");"
1178 );
1179 }
1180 }
1181 "greater_than_or_equal" => {
1182 if let Some(val) = &assertion.value {
1183 let java_val = json_to_java(val);
1184 let _ = writeln!(
1185 out,
1186 " assertTrue({field_expr} >= {java_val}, \"expected >= {java_val}\");"
1187 );
1188 }
1189 }
1190 "less_than_or_equal" => {
1191 if let Some(val) = &assertion.value {
1192 let java_val = json_to_java(val);
1193 let _ = writeln!(
1194 out,
1195 " assertTrue({field_expr} <= {java_val}, \"expected <= {java_val}\");"
1196 );
1197 }
1198 }
1199 "starts_with" => {
1200 if let Some(expected) = &assertion.value {
1201 let java_val = json_to_java(expected);
1202 let _ = writeln!(
1203 out,
1204 " assertTrue({string_expr}.startsWith({java_val}), \"expected to start with: \" + {java_val});"
1205 );
1206 }
1207 }
1208 "ends_with" => {
1209 if let Some(expected) = &assertion.value {
1210 let java_val = json_to_java(expected);
1211 let _ = writeln!(
1212 out,
1213 " assertTrue({string_expr}.endsWith({java_val}), \"expected to end with: \" + {java_val});"
1214 );
1215 }
1216 }
1217 "min_length" => {
1218 if let Some(val) = &assertion.value {
1219 if let Some(n) = val.as_u64() {
1220 let len_expr = if result_is_bytes {
1222 format!("{field_expr}.length")
1223 } else {
1224 format!("{field_expr}.length()")
1225 };
1226 let _ = writeln!(
1227 out,
1228 " assertTrue({len_expr} >= {n}, \"expected length >= {n}\");"
1229 );
1230 }
1231 }
1232 }
1233 "max_length" => {
1234 if let Some(val) = &assertion.value {
1235 if let Some(n) = val.as_u64() {
1236 let len_expr = if result_is_bytes {
1237 format!("{field_expr}.length")
1238 } else {
1239 format!("{field_expr}.length()")
1240 };
1241 let _ = writeln!(
1242 out,
1243 " assertTrue({len_expr} <= {n}, \"expected length <= {n}\");"
1244 );
1245 }
1246 }
1247 }
1248 "count_min" => {
1249 if let Some(val) = &assertion.value {
1250 if let Some(n) = val.as_u64() {
1251 let _ = writeln!(
1252 out,
1253 " assertTrue({field_expr}.size() >= {n}, \"expected at least {n} elements\");"
1254 );
1255 }
1256 }
1257 }
1258 "count_equals" => {
1259 if let Some(val) = &assertion.value {
1260 if let Some(n) = val.as_u64() {
1261 let _ = writeln!(
1262 out,
1263 " assertEquals({n}, {field_expr}.size(), \"expected exactly {n} elements\");"
1264 );
1265 }
1266 }
1267 }
1268 "is_true" => {
1269 let _ = writeln!(out, " assertTrue({field_expr}, \"expected true\");");
1270 }
1271 "is_false" => {
1272 let _ = writeln!(out, " assertFalse({field_expr}, \"expected false\");");
1273 }
1274 "method_result" => {
1275 if let Some(method_name) = &assertion.method {
1276 let call_expr = build_java_method_call(result_var, method_name, assertion.args.as_ref(), class_name);
1277 let check = assertion.check.as_deref().unwrap_or("is_true");
1278 let method_returns_collection =
1280 matches!(method_name.as_str(), "find_nodes_by_type" | "findNodesByType");
1281 match check {
1282 "equals" => {
1283 if let Some(val) = &assertion.value {
1284 if val.is_boolean() {
1285 if val.as_bool() == Some(true) {
1286 let _ = writeln!(out, " assertTrue({call_expr});");
1287 } else {
1288 let _ = writeln!(out, " assertFalse({call_expr});");
1289 }
1290 } else if method_returns_collection {
1291 let java_val = json_to_java(val);
1292 let _ = writeln!(out, " assertEquals({java_val}, {call_expr}.size());");
1293 } else {
1294 let java_val = json_to_java(val);
1295 let _ = writeln!(out, " assertEquals({java_val}, {call_expr});");
1296 }
1297 }
1298 }
1299 "is_true" => {
1300 let _ = writeln!(out, " assertTrue({call_expr});");
1301 }
1302 "is_false" => {
1303 let _ = writeln!(out, " assertFalse({call_expr});");
1304 }
1305 "greater_than_or_equal" => {
1306 if let Some(val) = &assertion.value {
1307 let n = val.as_u64().unwrap_or(0);
1308 let _ = writeln!(out, " assertTrue({call_expr} >= {n}, \"expected >= {n}\");");
1309 }
1310 }
1311 "count_min" => {
1312 if let Some(val) = &assertion.value {
1313 let n = val.as_u64().unwrap_or(0);
1314 let _ = writeln!(
1315 out,
1316 " assertTrue({call_expr}.size() >= {n}, \"expected at least {n} elements\");"
1317 );
1318 }
1319 }
1320 "is_error" => {
1321 let _ = writeln!(out, " assertThrows(Exception.class, () -> {{ {call_expr}; }});");
1322 }
1323 "contains" => {
1324 if let Some(val) = &assertion.value {
1325 let java_val = json_to_java(val);
1326 let _ = writeln!(
1327 out,
1328 " assertTrue({call_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
1329 );
1330 }
1331 }
1332 other_check => {
1333 panic!("Java e2e generator: unsupported method_result check type: {other_check}");
1334 }
1335 }
1336 } else {
1337 panic!("Java e2e generator: method_result assertion missing 'method' field");
1338 }
1339 }
1340 "matches_regex" => {
1341 if let Some(expected) = &assertion.value {
1342 let java_val = json_to_java(expected);
1343 let _ = writeln!(
1344 out,
1345 " assertTrue({string_expr}.matches({java_val}), \"expected value to match regex: \" + {java_val});"
1346 );
1347 }
1348 }
1349 "not_error" => {
1350 }
1352 "error" => {
1353 }
1355 other => {
1356 panic!("Java e2e generator: unsupported assertion type: {other}");
1357 }
1358 }
1359}
1360
1361fn build_java_method_call(
1365 result_var: &str,
1366 method_name: &str,
1367 args: Option<&serde_json::Value>,
1368 class_name: &str,
1369) -> String {
1370 match method_name {
1371 "root_child_count" => format!("{result_var}.rootNode().childCount()"),
1372 "root_node_type" => format!("{result_var}.rootNode().kind()"),
1373 "named_children_count" => format!("{result_var}.rootNode().namedChildCount()"),
1374 "has_error_nodes" => format!("{class_name}.treeHasErrorNodes({result_var})"),
1375 "error_count" | "tree_error_count" => format!("{class_name}.treeErrorCount({result_var})"),
1376 "tree_to_sexp" => format!("{class_name}.treeToSexp({result_var})"),
1377 "contains_node_type" => {
1378 let node_type = args
1379 .and_then(|a| a.get("node_type"))
1380 .and_then(|v| v.as_str())
1381 .unwrap_or("");
1382 format!("{class_name}.treeContainsNodeType({result_var}, \"{node_type}\")")
1383 }
1384 "find_nodes_by_type" => {
1385 let node_type = args
1386 .and_then(|a| a.get("node_type"))
1387 .and_then(|v| v.as_str())
1388 .unwrap_or("");
1389 format!("{class_name}.findNodesByType({result_var}, \"{node_type}\")")
1390 }
1391 "run_query" => {
1392 let query_source = args
1393 .and_then(|a| a.get("query_source"))
1394 .and_then(|v| v.as_str())
1395 .unwrap_or("");
1396 let language = args
1397 .and_then(|a| a.get("language"))
1398 .and_then(|v| v.as_str())
1399 .unwrap_or("");
1400 let escaped_query = escape_java(query_source);
1401 format!("{class_name}.runQuery({result_var}, \"{language}\", \"{escaped_query}\", source)")
1402 }
1403 _ => {
1404 format!("{result_var}.{}()", method_name.to_lower_camel_case())
1405 }
1406 }
1407}
1408
1409fn json_to_java(value: &serde_json::Value) -> String {
1411 json_to_java_typed(value, None)
1412}
1413
1414fn json_to_java_typed(value: &serde_json::Value, element_type: Option<&str>) -> String {
1417 match value {
1418 serde_json::Value::String(s) => format!("\"{}\"", escape_java(s)),
1419 serde_json::Value::Bool(b) => b.to_string(),
1420 serde_json::Value::Number(n) => {
1421 if n.is_f64() {
1422 match element_type {
1423 Some("f32" | "float" | "Float") => format!("{}f", n),
1424 _ => format!("{}d", n),
1425 }
1426 } else {
1427 n.to_string()
1428 }
1429 }
1430 serde_json::Value::Null => "null".to_string(),
1431 serde_json::Value::Array(arr) => {
1432 let items: Vec<String> = arr.iter().map(|v| json_to_java_typed(v, element_type)).collect();
1433 format!("java.util.List.of({})", items.join(", "))
1434 }
1435 serde_json::Value::Object(_) => {
1436 let json_str = serde_json::to_string(value).unwrap_or_default();
1437 format!("\"{}\"", escape_java(&json_str))
1438 }
1439 }
1440}
1441
1442fn build_java_visitor(
1448 setup_lines: &mut Vec<String>,
1449 visitor_spec: &crate::fixture::VisitorSpec,
1450 class_name: &str,
1451) -> String {
1452 setup_lines.push("class _TestVisitor implements TestVisitor {".to_string());
1453 for (method_name, action) in &visitor_spec.callbacks {
1454 emit_java_visitor_method(setup_lines, method_name, action, class_name);
1455 }
1456 setup_lines.push("}".to_string());
1457 setup_lines.push("var visitor = new _TestVisitor();".to_string());
1458 "visitor".to_string()
1459}
1460
1461fn emit_java_visitor_method(
1463 setup_lines: &mut Vec<String>,
1464 method_name: &str,
1465 action: &CallbackAction,
1466 _class_name: &str,
1467) {
1468 let camel_method = method_to_camel(method_name);
1469 let params = match method_name {
1470 "visit_link" => "VisitContext ctx, String href, String text, String title",
1471 "visit_image" => "VisitContext ctx, String src, String alt, String title",
1472 "visit_heading" => "VisitContext ctx, int level, String text, String id",
1473 "visit_code_block" => "VisitContext ctx, String lang, String code",
1474 "visit_code_inline"
1475 | "visit_strong"
1476 | "visit_emphasis"
1477 | "visit_strikethrough"
1478 | "visit_underline"
1479 | "visit_subscript"
1480 | "visit_superscript"
1481 | "visit_mark"
1482 | "visit_button"
1483 | "visit_summary"
1484 | "visit_figcaption"
1485 | "visit_definition_term"
1486 | "visit_definition_description" => "VisitContext ctx, String text",
1487 "visit_text" => "VisitContext ctx, String text",
1488 "visit_list_item" => "VisitContext ctx, boolean ordered, String marker, String text",
1489 "visit_blockquote" => "VisitContext ctx, String content, int depth",
1490 "visit_table_row" => "VisitContext ctx, java.util.List<String> cells, boolean isHeader",
1491 "visit_custom_element" => "VisitContext ctx, String tagName, String html",
1492 "visit_form" => "VisitContext ctx, String actionUrl, String method",
1493 "visit_input" => "VisitContext ctx, String inputType, String name, String value",
1494 "visit_audio" | "visit_video" | "visit_iframe" => "VisitContext ctx, String src",
1495 "visit_details" => "VisitContext ctx, boolean isOpen",
1496 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1497 "VisitContext ctx, String output"
1498 }
1499 "visit_list_start" => "VisitContext ctx, boolean ordered",
1500 "visit_list_end" => "VisitContext ctx, boolean ordered, String output",
1501 _ => "VisitContext ctx",
1502 };
1503
1504 setup_lines.push(format!(" @Override public VisitResult {camel_method}({params}) {{"));
1505 match action {
1506 CallbackAction::Skip => {
1507 setup_lines.push(" return VisitResult.skip();".to_string());
1508 }
1509 CallbackAction::Continue => {
1510 setup_lines.push(" return VisitResult.continue_();".to_string());
1511 }
1512 CallbackAction::PreserveHtml => {
1513 setup_lines.push(" return VisitResult.preserveHtml();".to_string());
1514 }
1515 CallbackAction::Custom { output } => {
1516 let escaped = escape_java(output);
1517 setup_lines.push(format!(" return VisitResult.custom(\"{escaped}\");"));
1518 }
1519 CallbackAction::CustomTemplate { template } => {
1520 let escaped = escape_java(template);
1521 setup_lines.push(format!(
1522 " return VisitResult.custom(String.format(\"{escaped}\"));"
1523 ));
1524 }
1525 }
1526 setup_lines.push(" }".to_string());
1527}
1528
1529fn method_to_camel(snake: &str) -> String {
1531 snake.to_lower_camel_case()
1532}