1use crate::config::E2eConfig;
7use crate::escape::{escape_java, sanitize_filename};
8use crate::field_access::FieldResolver;
9use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup};
10use alef_core::backend::GeneratedFile;
11use alef_core::config::AlefConfig;
12use anyhow::Result;
13use heck::ToUpperCamelCase;
14use std::collections::HashSet;
15use std::fmt::Write as FmtWrite;
16use std::path::PathBuf;
17
18use super::E2eCodegen;
19
20pub struct JavaCodegen;
22
23impl E2eCodegen for JavaCodegen {
24 fn generate(
25 &self,
26 groups: &[FixtureGroup],
27 e2e_config: &E2eConfig,
28 alef_config: &AlefConfig,
29 ) -> Result<Vec<GeneratedFile>> {
30 let lang = self.language_name();
31 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
32
33 let mut files = Vec::new();
34
35 let call = &e2e_config.call;
37 let overrides = call.overrides.get(lang);
38 let _module_path = overrides
39 .and_then(|o| o.module.as_ref())
40 .cloned()
41 .unwrap_or_else(|| call.module.clone());
42 let function_name = overrides
43 .and_then(|o| o.function.as_ref())
44 .cloned()
45 .unwrap_or_else(|| call.function.clone());
46 let class_name = overrides
47 .and_then(|o| o.class.as_ref())
48 .cloned()
49 .unwrap_or_else(|| alef_config.crate_config.name.to_upper_camel_case());
50 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
51 let result_var = &call.result_var;
52
53 let java_pkg = e2e_config.resolve_package("java");
55 let pkg_name = java_pkg
56 .as_ref()
57 .and_then(|p| p.name.as_ref())
58 .cloned()
59 .unwrap_or_else(|| alef_config.crate_config.name.clone());
60
61 let java_group_id = alef_config.java_group_id();
63 let pkg_version = alef_config.resolved_version().unwrap_or_else(|| "0.1.0".to_string());
64
65 files.push(GeneratedFile {
67 path: output_base.join("pom.xml"),
68 content: render_pom_xml(&pkg_name, &java_group_id, &pkg_version, e2e_config.dep_mode),
69 generated_header: false,
70 });
71
72 let test_base = output_base
74 .join("src")
75 .join("test")
76 .join("java")
77 .join("dev")
78 .join("kreuzberg")
79 .join("e2e");
80
81 let options_type = overrides.and_then(|o| o.options_type.clone());
83 let field_resolver = FieldResolver::new(
84 &e2e_config.fields,
85 &e2e_config.fields_optional,
86 &e2e_config.result_fields,
87 &e2e_config.fields_array,
88 );
89
90 for group in groups {
91 let active: Vec<&Fixture> = group
92 .fixtures
93 .iter()
94 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
95 .collect();
96
97 if active.is_empty() {
98 continue;
99 }
100
101 let class_file_name = format!("{}Test.java", sanitize_filename(&group.category).to_upper_camel_case());
102 let content = render_test_file(
103 &group.category,
104 &active,
105 &class_name,
106 &function_name,
107 result_var,
108 &e2e_config.call.args,
109 options_type.as_deref(),
110 &field_resolver,
111 result_is_simple,
112 &e2e_config.fields_enum,
113 );
114 files.push(GeneratedFile {
115 path: test_base.join(class_file_name),
116 content,
117 generated_header: true,
118 });
119 }
120
121 Ok(files)
122 }
123
124 fn language_name(&self) -> &'static str {
125 "java"
126 }
127}
128
129fn render_pom_xml(
134 pkg_name: &str,
135 java_group_id: &str,
136 pkg_version: &str,
137 dep_mode: crate::config::DependencyMode,
138) -> String {
139 let (dep_group_id, dep_artifact_id) = if let Some((g, a)) = pkg_name.split_once(':') {
141 (g, a)
142 } else {
143 (java_group_id, pkg_name)
144 };
145 let artifact_id = format!("{dep_artifact_id}-e2e-java");
146 let dep_block = match dep_mode {
147 crate::config::DependencyMode::Registry => {
148 format!(
149 r#" <dependency>
150 <groupId>{dep_group_id}</groupId>
151 <artifactId>{dep_artifact_id}</artifactId>
152 <version>{pkg_version}</version>
153 </dependency>"#
154 )
155 }
156 crate::config::DependencyMode::Local => {
157 format!(
158 r#" <dependency>
159 <groupId>{dep_group_id}</groupId>
160 <artifactId>{dep_artifact_id}</artifactId>
161 <version>{pkg_version}</version>
162 <scope>system</scope>
163 <systemPath>${{project.basedir}}/../../packages/java/target/{dep_artifact_id}-{pkg_version}.jar</systemPath>
164 </dependency>"#
165 )
166 }
167 };
168 format!(
169 r#"<?xml version="1.0" encoding="UTF-8"?>
170<project xmlns="http://maven.apache.org/POM/4.0.0"
171 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
172 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
173 <modelVersion>4.0.0</modelVersion>
174
175 <groupId>dev.kreuzberg</groupId>
176 <artifactId>{artifact_id}</artifactId>
177 <version>0.1.0</version>
178
179 <properties>
180 <maven.compiler.source>25</maven.compiler.source>
181 <maven.compiler.target>25</maven.compiler.target>
182 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
183 <junit.version>5.11.4</junit.version>
184 </properties>
185
186 <dependencies>
187{dep_block}
188 <dependency>
189 <groupId>com.fasterxml.jackson.core</groupId>
190 <artifactId>jackson-databind</artifactId>
191 <version>2.18.2</version>
192 </dependency>
193 <dependency>
194 <groupId>com.fasterxml.jackson.datatype</groupId>
195 <artifactId>jackson-datatype-jdk8</artifactId>
196 <version>2.18.2</version>
197 </dependency>
198 <dependency>
199 <groupId>org.junit.jupiter</groupId>
200 <artifactId>junit-jupiter</artifactId>
201 <version>${{junit.version}}</version>
202 <scope>test</scope>
203 </dependency>
204 </dependencies>
205
206 <build>
207 <plugins>
208 <plugin>
209 <groupId>org.codehaus.mojo</groupId>
210 <artifactId>build-helper-maven-plugin</artifactId>
211 <version>3.6.0</version>
212 <executions>
213 <execution>
214 <id>add-test-source</id>
215 <phase>generate-test-sources</phase>
216 <goals>
217 <goal>add-test-source</goal>
218 </goals>
219 <configuration>
220 <sources>
221 <source>src/test/java</source>
222 </sources>
223 </configuration>
224 </execution>
225 </executions>
226 </plugin>
227 <plugin>
228 <groupId>org.apache.maven.plugins</groupId>
229 <artifactId>maven-surefire-plugin</artifactId>
230 <version>3.5.2</version>
231 <configuration>
232 <argLine>--enable-preview --enable-native-access=ALL-UNNAMED -Djava.library.path=../../target/release</argLine>
233 </configuration>
234 </plugin>
235 </plugins>
236 </build>
237</project>
238"#
239 )
240}
241
242#[allow(clippy::too_many_arguments)]
243fn render_test_file(
244 category: &str,
245 fixtures: &[&Fixture],
246 class_name: &str,
247 function_name: &str,
248 result_var: &str,
249 args: &[crate::config::ArgMapping],
250 options_type: Option<&str>,
251 field_resolver: &FieldResolver,
252 result_is_simple: bool,
253 enum_fields: &HashSet<String>,
254) -> String {
255 let mut out = String::new();
256 let _ = writeln!(out, "// This file is auto-generated by alef. DO NOT EDIT.");
257 let test_class_name = format!("{}Test", sanitize_filename(category).to_upper_camel_case());
258
259 let (import_path, simple_class) = if class_name.contains('.') {
262 let simple = class_name.rsplit('.').next().unwrap_or(class_name);
263 (class_name, simple)
264 } else {
265 ("", class_name)
266 };
267
268 let _ = writeln!(out, "package dev.kreuzberg.e2e;");
269 let _ = writeln!(out);
270
271 let needs_object_mapper_for_options = options_type.is_some()
273 && fixtures.iter().any(|f| {
274 args.iter().any(|arg| {
275 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
276 arg.arg_type == "json_object" && f.input.get(field).is_some_and(|v| !v.is_null())
277 })
278 });
279 let needs_object_mapper_for_handle = fixtures.iter().any(|f| {
281 args.iter().filter(|a| a.arg_type == "handle").any(|a| {
282 let v = f.input.get(&a.field).unwrap_or(&serde_json::Value::Null);
283 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
284 })
285 });
286 let needs_object_mapper = needs_object_mapper_for_options || needs_object_mapper_for_handle;
287
288 let _ = writeln!(out, "import org.junit.jupiter.api.Test;");
289 let _ = writeln!(out, "import static org.junit.jupiter.api.Assertions.*;");
290 if !import_path.is_empty() {
291 let _ = writeln!(out, "import {import_path};");
292 }
293 if needs_object_mapper {
294 let _ = writeln!(out, "import com.fasterxml.jackson.databind.ObjectMapper;");
295 let _ = writeln!(out, "import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;");
296 }
297 if let Some(opts_type) = options_type {
299 if needs_object_mapper {
300 let opts_package = if !import_path.is_empty() {
302 let pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
303 format!("{pkg}.{opts_type}")
304 } else {
305 opts_type.to_string()
306 };
307 let _ = writeln!(out, "import {opts_package};");
308 }
309 }
310 if needs_object_mapper_for_handle && !import_path.is_empty() {
312 let pkg = import_path.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
313 let _ = writeln!(out, "import {pkg}.CrawlConfig;");
314 }
315 let _ = writeln!(out);
316
317 let _ = writeln!(out, "/** E2e tests for category: {category}. */");
318 let _ = writeln!(out, "class {test_class_name} {{");
319
320 if needs_object_mapper {
321 let _ = writeln!(out);
322 let _ = writeln!(
323 out,
324 " private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());"
325 );
326 }
327
328 for fixture in fixtures {
329 render_test_method(
330 &mut out,
331 fixture,
332 simple_class,
333 function_name,
334 result_var,
335 args,
336 options_type,
337 field_resolver,
338 result_is_simple,
339 enum_fields,
340 );
341 let _ = writeln!(out);
342 }
343
344 let _ = writeln!(out, "}}");
345 out
346}
347
348#[allow(clippy::too_many_arguments)]
349fn render_test_method(
350 out: &mut String,
351 fixture: &Fixture,
352 class_name: &str,
353 function_name: &str,
354 result_var: &str,
355 args: &[crate::config::ArgMapping],
356 options_type: Option<&str>,
357 field_resolver: &FieldResolver,
358 result_is_simple: bool,
359 enum_fields: &HashSet<String>,
360) {
361 let method_name = fixture.id.to_upper_camel_case();
362 let description = &fixture.description;
363 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
364
365 let needs_deser = options_type.is_some()
367 && args
368 .iter()
369 .any(|arg| arg.arg_type == "json_object" && fixture.input.get(&arg.field).is_some_and(|v| !v.is_null()));
370
371 let throws_clause = " throws Exception";
373
374 let _ = writeln!(out, " @Test");
375 let _ = writeln!(out, " void test{method_name}(){throws_clause} {{");
376 let _ = writeln!(out, " // {description}");
377
378 if let (true, Some(opts_type)) = (needs_deser, options_type) {
380 for arg in args {
381 if arg.arg_type == "json_object" {
382 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
383 if let Some(val) = fixture.input.get(field) {
384 if !val.is_null() {
385 let normalized = super::normalize_json_keys_to_snake_case(val);
389 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
390 let var_name = &arg.name;
391 let _ = writeln!(
392 out,
393 " var {var_name} = MAPPER.readValue(\"{}\", {opts_type}.class);",
394 escape_java(&json_str)
395 );
396 }
397 }
398 }
399 }
400 }
401
402 let (mut setup_lines, args_str) = build_args_and_setup(&fixture.input, args, class_name, options_type, &fixture.id);
403
404 let mut visitor_arg = String::new();
406 if let Some(visitor_spec) = &fixture.visitor {
407 visitor_arg = build_java_visitor(&mut setup_lines, visitor_spec, class_name);
408 }
409
410 for line in &setup_lines {
411 let _ = writeln!(out, " {line}");
412 }
413
414 let final_args = if visitor_arg.is_empty() {
415 args_str
416 } else {
417 format!("{args_str}, {visitor_arg}")
418 };
419
420 if expects_error {
421 let _ = writeln!(
422 out,
423 " assertThrows(Exception.class, () -> {class_name}.{function_name}({final_args}));"
424 );
425 let _ = writeln!(out, " }}");
426 return;
427 }
428
429 let _ = writeln!(
430 out,
431 " var {result_var} = {class_name}.{function_name}({final_args});"
432 );
433
434 for assertion in &fixture.assertions {
435 render_assertion(
436 out,
437 assertion,
438 result_var,
439 field_resolver,
440 result_is_simple,
441 enum_fields,
442 );
443 }
444
445 let _ = writeln!(out, " }}");
446}
447
448fn build_args_and_setup(
452 input: &serde_json::Value,
453 args: &[crate::config::ArgMapping],
454 class_name: &str,
455 options_type: Option<&str>,
456 fixture_id: &str,
457) -> (Vec<String>, String) {
458 if args.is_empty() {
459 return (Vec::new(), json_to_java(input));
460 }
461
462 let mut setup_lines: Vec<String> = Vec::new();
463 let mut parts: Vec<String> = Vec::new();
464
465 for arg in args {
466 if arg.arg_type == "mock_url" {
467 setup_lines.push(format!(
468 "String {} = System.getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";",
469 arg.name,
470 ));
471 parts.push(arg.name.clone());
472 continue;
473 }
474
475 if arg.arg_type == "handle" {
476 let constructor_name = format!("create{}", arg.name.to_upper_camel_case());
478 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
479 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
480 if config_value.is_null()
481 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
482 {
483 setup_lines.push(format!("var {} = {class_name}.{constructor_name}(null);", arg.name,));
484 } else {
485 let json_str = serde_json::to_string(config_value).unwrap_or_default();
486 let name = &arg.name;
487 setup_lines.push(format!(
488 "var {name}Config = MAPPER.readValue(\"{}\", CrawlConfig.class);",
489 escape_java(&json_str),
490 ));
491 setup_lines.push(format!(
492 "var {} = {class_name}.{constructor_name}({name}Config);",
493 arg.name,
494 name = name,
495 ));
496 }
497 parts.push(arg.name.clone());
498 continue;
499 }
500
501 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
502 let val = input.get(field);
503 match val {
504 None | Some(serde_json::Value::Null) if arg.optional => {
505 continue;
507 }
508 None | Some(serde_json::Value::Null) => {
509 let default_val = match arg.arg_type.as_str() {
511 "string" => "\"\"".to_string(),
512 "int" | "integer" => "0".to_string(),
513 "float" | "number" => "0.0d".to_string(),
514 "bool" | "boolean" => "false".to_string(),
515 _ => "null".to_string(),
516 };
517 parts.push(default_val);
518 }
519 Some(v) => {
520 if arg.arg_type == "json_object" && options_type.is_some() {
522 parts.push(arg.name.clone());
523 continue;
524 }
525 parts.push(json_to_java(v));
526 }
527 }
528 }
529
530 (setup_lines, parts.join(", "))
531}
532
533fn render_assertion(
534 out: &mut String,
535 assertion: &Assertion,
536 result_var: &str,
537 field_resolver: &FieldResolver,
538 result_is_simple: bool,
539 enum_fields: &HashSet<String>,
540) {
541 if let Some(f) = &assertion.field {
543 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
544 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
545 return;
546 }
547 }
548
549 let field_is_enum = assertion
554 .field
555 .as_deref()
556 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
557
558 let field_expr = if result_is_simple {
559 result_var.to_string()
560 } else {
561 match &assertion.field {
562 Some(f) if !f.is_empty() => {
563 let accessor = field_resolver.accessor(f, "java", result_var);
564 let resolved = field_resolver.resolve(f);
565 if field_resolver.is_optional(resolved) && !field_resolver.has_map_access(f) {
568 format!("{accessor}.orElse(\"\")")
569 } else {
570 accessor
571 }
572 }
573 _ => result_var.to_string(),
574 }
575 };
576
577 let string_expr = if field_is_enum {
581 format!("{field_expr}.getValue()")
582 } else {
583 field_expr.clone()
584 };
585
586 match assertion.assertion_type.as_str() {
587 "equals" => {
588 if let Some(expected) = &assertion.value {
589 let java_val = json_to_java(expected);
590 if expected.is_string() {
591 let _ = writeln!(out, " assertEquals({java_val}, {string_expr}.trim());");
592 } else {
593 let _ = writeln!(out, " assertEquals({java_val}, {field_expr});");
594 }
595 }
596 }
597 "contains" => {
598 if let Some(expected) = &assertion.value {
599 let java_val = json_to_java(expected);
600 let _ = writeln!(
601 out,
602 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
603 );
604 }
605 }
606 "contains_all" => {
607 if let Some(values) = &assertion.values {
608 for val in values {
609 let java_val = json_to_java(val);
610 let _ = writeln!(
611 out,
612 " assertTrue({string_expr}.contains({java_val}), \"expected to contain: \" + {java_val});"
613 );
614 }
615 }
616 }
617 "not_contains" => {
618 if let Some(expected) = &assertion.value {
619 let java_val = json_to_java(expected);
620 let _ = writeln!(
621 out,
622 " assertFalse({string_expr}.contains({java_val}), \"expected NOT to contain: \" + {java_val});"
623 );
624 }
625 }
626 "not_empty" => {
627 let _ = writeln!(
628 out,
629 " assertFalse({field_expr}.isEmpty(), \"expected non-empty value\");"
630 );
631 }
632 "is_empty" => {
633 let _ = writeln!(
634 out,
635 " assertTrue({field_expr}.isEmpty(), \"expected empty value\");"
636 );
637 }
638 "contains_any" => {
639 if let Some(values) = &assertion.values {
640 let checks: Vec<String> = values
641 .iter()
642 .map(|v| {
643 let java_val = json_to_java(v);
644 format!("{string_expr}.contains({java_val})")
645 })
646 .collect();
647 let joined = checks.join(" || ");
648 let _ = writeln!(
649 out,
650 " assertTrue({joined}, \"expected to contain at least one of the specified values\");"
651 );
652 }
653 }
654 "greater_than" => {
655 if let Some(val) = &assertion.value {
656 let java_val = json_to_java(val);
657 let _ = writeln!(
658 out,
659 " assertTrue({field_expr} > {java_val}, \"expected > {java_val}\");"
660 );
661 }
662 }
663 "less_than" => {
664 if let Some(val) = &assertion.value {
665 let java_val = json_to_java(val);
666 let _ = writeln!(
667 out,
668 " assertTrue({field_expr} < {java_val}, \"expected < {java_val}\");"
669 );
670 }
671 }
672 "greater_than_or_equal" => {
673 if let Some(val) = &assertion.value {
674 let java_val = json_to_java(val);
675 let _ = writeln!(
676 out,
677 " assertTrue({field_expr} >= {java_val}, \"expected >= {java_val}\");"
678 );
679 }
680 }
681 "less_than_or_equal" => {
682 if let Some(val) = &assertion.value {
683 let java_val = json_to_java(val);
684 let _ = writeln!(
685 out,
686 " assertTrue({field_expr} <= {java_val}, \"expected <= {java_val}\");"
687 );
688 }
689 }
690 "starts_with" => {
691 if let Some(expected) = &assertion.value {
692 let java_val = json_to_java(expected);
693 let _ = writeln!(
694 out,
695 " assertTrue({string_expr}.startsWith({java_val}), \"expected to start with: \" + {java_val});"
696 );
697 }
698 }
699 "ends_with" => {
700 if let Some(expected) = &assertion.value {
701 let java_val = json_to_java(expected);
702 let _ = writeln!(
703 out,
704 " assertTrue({string_expr}.endsWith({java_val}), \"expected to end with: \" + {java_val});"
705 );
706 }
707 }
708 "min_length" => {
709 if let Some(val) = &assertion.value {
710 if let Some(n) = val.as_u64() {
711 let _ = writeln!(
712 out,
713 " assertTrue({field_expr}.length() >= {n}, \"expected length >= {n}\");"
714 );
715 }
716 }
717 }
718 "max_length" => {
719 if let Some(val) = &assertion.value {
720 if let Some(n) = val.as_u64() {
721 let _ = writeln!(
722 out,
723 " assertTrue({field_expr}.length() <= {n}, \"expected length <= {n}\");"
724 );
725 }
726 }
727 }
728 "count_min" => {
729 if let Some(val) = &assertion.value {
730 if let Some(n) = val.as_u64() {
731 let _ = writeln!(
732 out,
733 " assertTrue({field_expr}.size() >= {n}, \"expected at least {n} elements\");"
734 );
735 }
736 }
737 }
738 "count_equals" => {
739 if let Some(val) = &assertion.value {
740 if let Some(n) = val.as_u64() {
741 let _ = writeln!(
742 out,
743 " assertEquals({n}, {field_expr}.size(), \"expected exactly {n} elements\");"
744 );
745 }
746 }
747 }
748 "is_true" => {
749 let _ = writeln!(out, " assertTrue({field_expr}, \"expected true\");");
750 }
751 "not_error" => {
752 }
754 "error" => {
755 }
757 other => {
758 let _ = writeln!(out, " // TODO: unsupported assertion type: {other}");
759 }
760 }
761}
762
763fn json_to_java(value: &serde_json::Value) -> String {
765 match value {
766 serde_json::Value::String(s) => format!("\"{}\"", escape_java(s)),
767 serde_json::Value::Bool(b) => b.to_string(),
768 serde_json::Value::Number(n) => {
769 if n.is_f64() {
770 format!("{}d", n)
771 } else {
772 n.to_string()
773 }
774 }
775 serde_json::Value::Null => "null".to_string(),
776 serde_json::Value::Array(arr) => {
777 let items: Vec<String> = arr.iter().map(json_to_java).collect();
778 format!("java.util.List.of({})", items.join(", "))
779 }
780 serde_json::Value::Object(_) => {
781 let json_str = serde_json::to_string(value).unwrap_or_default();
782 format!("\"{}\"", escape_java(&json_str))
783 }
784 }
785}
786
787fn build_java_visitor(
793 setup_lines: &mut Vec<String>,
794 visitor_spec: &crate::fixture::VisitorSpec,
795 class_name: &str,
796) -> String {
797 setup_lines.push("class _TestVisitor implements TestVisitor {".to_string());
798 for (method_name, action) in &visitor_spec.callbacks {
799 emit_java_visitor_method(setup_lines, method_name, action, class_name);
800 }
801 setup_lines.push("}".to_string());
802 setup_lines.push("var visitor = new _TestVisitor();".to_string());
803 "visitor".to_string()
804}
805
806fn emit_java_visitor_method(
808 setup_lines: &mut Vec<String>,
809 method_name: &str,
810 action: &CallbackAction,
811 _class_name: &str,
812) {
813 let camel_method = method_to_camel(method_name);
814 let params = match method_name {
815 "visit_link" => "VisitContext ctx, String href, String text, String title",
816 "visit_image" => "VisitContext ctx, String src, String alt, String title",
817 "visit_heading" => "VisitContext ctx, int level, String text, String id",
818 "visit_code_block" => "VisitContext ctx, String lang, String code",
819 "visit_code_inline"
820 | "visit_strong"
821 | "visit_emphasis"
822 | "visit_strikethrough"
823 | "visit_underline"
824 | "visit_subscript"
825 | "visit_superscript"
826 | "visit_mark"
827 | "visit_button"
828 | "visit_summary"
829 | "visit_figcaption"
830 | "visit_definition_term"
831 | "visit_definition_description" => "VisitContext ctx, String text",
832 "visit_text" => "VisitContext ctx, String text",
833 "visit_list_item" => "VisitContext ctx, boolean ordered, String marker, String text",
834 "visit_blockquote" => "VisitContext ctx, String content, int depth",
835 "visit_table_row" => "VisitContext ctx, java.util.List<String> cells, boolean isHeader",
836 "visit_custom_element" => "VisitContext ctx, String tagName, String html",
837 "visit_form" => "VisitContext ctx, String actionUrl, String method",
838 "visit_input" => "VisitContext ctx, String inputType, String name, String value",
839 "visit_audio" | "visit_video" | "visit_iframe" => "VisitContext ctx, String src",
840 "visit_details" => "VisitContext ctx, boolean isOpen",
841 _ => "VisitContext ctx",
842 };
843
844 setup_lines.push(format!(" @Override public VisitResult {camel_method}({params}) {{"));
845 match action {
846 CallbackAction::Skip => {
847 setup_lines.push(" return VisitResult.skip();".to_string());
848 }
849 CallbackAction::Continue => {
850 setup_lines.push(" return VisitResult.continue_();".to_string());
851 }
852 CallbackAction::PreserveHtml => {
853 setup_lines.push(" return VisitResult.preserveHtml();".to_string());
854 }
855 CallbackAction::Custom { output } => {
856 let escaped = escape_java(output);
857 setup_lines.push(format!(" return VisitResult.custom(\"{escaped}\");"));
858 }
859 CallbackAction::CustomTemplate { template } => {
860 setup_lines.push(format!(
861 " return VisitResult.custom(String.format(\"{template}\"));"
862 ));
863 }
864 }
865 setup_lines.push(" }".to_string());
866}
867
868fn method_to_camel(snake: &str) -> String {
870 use heck::ToLowerCamelCase;
871 snake.to_lower_camel_case()
872}