1use crate::core::backend::GeneratedFile;
7use crate::core::config::ResolvedCrateConfig;
8use crate::e2e::config::E2eConfig;
9use crate::e2e::escape::sanitize_filename;
10use crate::e2e::fixture::{Fixture, FixtureGroup};
11use anyhow::Result;
12use heck::ToUpperCamelCase;
13use std::path::PathBuf;
14
15use super::E2eCodegen;
16use super::java_mvnw::{MAVEN_WRAPPER_PROPERTIES, MVNW_UNIX, MVNW_WINDOWS};
17
18pub struct JavaCodegen;
20
21impl E2eCodegen for JavaCodegen {
22 fn generate(
23 &self,
24 groups: &[FixtureGroup],
25 e2e_config: &E2eConfig,
26 config: &ResolvedCrateConfig,
27 type_defs: &[crate::core::ir::TypeDef],
28 enums: &[crate::core::ir::EnumDef],
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(|| 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(|| config.name.clone());
60
61 let java_group_id = config.java_group_id();
63 let binding_pkg = config.java_package();
64 let pkg_version = config.resolved_version().unwrap_or_else(|| "0.1.0".to_string());
65
66 files.push(GeneratedFile {
68 path: output_base.join("pom.xml"),
69 content: project::render_pom_xml(
70 &pkg_name,
71 &java_group_id,
72 &pkg_version,
73 e2e_config.dep_mode,
74 &e2e_config.test_documents_relative_from(0),
75 &config.ffi_lib_name(),
76 ),
77 generated_header: false,
78 });
79
80 files.push(GeneratedFile {
86 path: output_base.join("mvnw"),
87 content: MVNW_UNIX.to_string(),
88 generated_header: false,
89 });
90 files.push(GeneratedFile {
91 path: output_base.join("mvnw.cmd"),
92 content: MVNW_WINDOWS.to_string(),
93 generated_header: false,
94 });
95 files.push(GeneratedFile {
96 path: output_base
97 .join(".mvn")
98 .join("wrapper")
99 .join("maven-wrapper.properties"),
100 content: MAVEN_WRAPPER_PROPERTIES.to_string(),
101 generated_header: false,
102 });
103
104 let has_http_fixtures = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| f.http.is_some());
106 let uses_harness = has_http_fixtures && !e2e_config.harness.imports.is_empty();
107 let needs_mock_server = groups
110 .iter()
111 .flat_map(|g| g.fixtures.iter())
112 .any(|f| f.needs_mock_server());
113
114 let mut test_base = output_base.join("src").join("test").join("java");
118 for segment in java_group_id.split('.') {
119 test_base = test_base.join(segment);
120 }
121 let test_base = test_base.join("e2e");
122
123 if needs_mock_server {
130 files.push(GeneratedFile {
131 path: test_base.join("MockServerListener.java"),
132 content: project::render_mock_server_listener(&java_group_id),
133 generated_header: true,
134 });
135 files.push(GeneratedFile {
136 path: output_base
137 .join("src")
138 .join("test")
139 .join("resources")
140 .join("META-INF")
141 .join("services")
142 .join("org.junit.platform.launcher.LauncherSessionListener"),
143 content: format!("{java_group_id}.e2e.MockServerListener\n"),
144 generated_header: false,
145 });
146 }
147
148 let fixtures_resource_base = output_base.join("src").join("test").join("resources").join("fixtures");
150 for group in groups {
151 for fixture in &group.fixtures {
152 if fixture.http.is_none() {
153 continue;
154 }
155 let http_data = fixture.http.as_ref().unwrap();
156 let fixture_json = serde_json::json!({
157 "http": {
158 "handler": {
159 "route": &http_data.handler.route,
160 "method": &http_data.handler.method,
161 "body_schema": http_data.handler.body_schema.clone(),
162 },
163 "request": {
164 "path": &http_data.request.path,
165 },
166 "expected_response": {
167 "status_code": http_data.expected_response.status_code,
168 "body": &http_data.expected_response.body,
169 "headers": &http_data.expected_response.headers,
170 }
171 }
172 });
173 let fixture_json_str = serde_json::to_string(&fixture_json).unwrap_or_default();
174 files.push(GeneratedFile {
175 path: fixtures_resource_base.join(format!("{}.json", fixture.id)),
176 content: fixture_json_str,
177 generated_header: false,
178 });
179 }
180 }
181
182 if uses_harness {
184 files.push(GeneratedFile {
185 path: test_base.join("FixtureLoader.java"),
186 content: project::render_fixture_loader(&java_group_id),
187 generated_header: true,
188 });
189 }
190
191 if uses_harness {
193 files.push(GeneratedFile {
194 path: test_base.join("HarnessMain.java"),
195 content: project::render_harness_main(e2e_config, groups, &java_group_id, &binding_pkg),
196 generated_header: true,
197 });
198 }
199
200 let sealed_display_types: std::collections::BTreeSet<String> = std::iter::once(&e2e_config.call)
205 .chain(e2e_config.calls.values())
206 .filter_map(|c| c.overrides.get(lang))
207 .flat_map(|o| o.assert_enum_fields.values().cloned())
208 .collect();
209
210 for type_name in &sealed_display_types {
211 if let Some(enum_def) = enums.iter().find(|e| &e.name == type_name) {
212 files.push(GeneratedFile {
213 path: test_base.join(format!("{type_name}Display.java")),
214 content: project::render_sealed_display(type_name, enum_def, type_defs, &java_group_id),
215 generated_header: true,
216 });
217 }
218 }
219
220 let options_type = overrides.and_then(|o| o.options_type.clone()).or_else(|| {
224 for cand in ["csharp", "c", "go", "php", "python"] {
226 if let Some(o) = e2e_config.call.overrides.get(cand) {
227 if let Some(t) = &o.options_type {
228 return Some(t.clone());
229 }
230 }
231 }
232 None
233 });
234
235 static EMPTY_ENUM_FIELDS: std::sync::LazyLock<std::collections::HashMap<String, String>> =
237 std::sync::LazyLock::new(std::collections::HashMap::new);
238 let _enum_fields = overrides.map(|o| &o.enum_fields).unwrap_or(&EMPTY_ENUM_FIELDS);
239
240 let mut effective_nested_types: std::collections::HashMap<String, String> = std::collections::HashMap::new();
242 if let Some(overrides_map) = overrides.map(|o| &o.nested_types) {
243 effective_nested_types.extend(overrides_map.clone());
244 }
245
246 let nested_types_optional = overrides.map(|o| o.nested_types_optional).unwrap_or(true);
248
249 for group in groups {
250 let active: Vec<&Fixture> = group
251 .fixtures
252 .iter()
253 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
254 .collect();
255
256 if active.is_empty() {
257 continue;
258 }
259
260 let class_file_name = format!("{}Test.java", sanitize_filename(&group.category).to_upper_camel_case());
261 let content = test_file::render_test_file(
262 &group.category,
263 &active,
264 &class_name,
265 &function_name,
266 &java_group_id,
267 &binding_pkg,
268 result_var,
269 &e2e_config.call.args,
270 options_type.as_deref(),
271 result_is_simple,
272 e2e_config,
273 &effective_nested_types,
274 nested_types_optional,
275 &config.adapters,
276 config,
277 type_defs,
278 uses_harness,
279 );
280 files.push(GeneratedFile {
281 path: test_base.join(class_file_name),
282 content,
283 generated_header: true,
284 });
285 }
286
287 Ok(files)
288 }
289
290 fn language_name(&self) -> &'static str {
291 "java"
292 }
293}
294
295mod args;
296mod assertions;
297mod http;
298mod project;
299mod stubs;
300mod test_file;
301mod test_method;
302mod values;
303mod visitor;
304
305pub use stubs::emit_test_backend;
306
307#[cfg(test)]
308mod tests;