alef_e2e/codegen/
kotlin_android.rs1use crate::config::E2eConfig;
9use crate::escape::sanitize_filename;
10use crate::field_access::FieldResolver;
11use crate::fixture::{Fixture, FixtureGroup};
12use alef_core::backend::GeneratedFile;
13use alef_core::config::ResolvedCrateConfig;
14use alef_core::template_versions::{maven, toolchain};
15use anyhow::Result;
16use heck::ToUpperCamelCase;
17use std::collections::HashSet;
18use std::path::PathBuf;
19
20use super::E2eCodegen;
21use super::kotlin;
22
23pub struct KotlinAndroidE2eCodegen;
27
28impl E2eCodegen for KotlinAndroidE2eCodegen {
29 fn generate(
30 &self,
31 groups: &[FixtureGroup],
32 e2e_config: &E2eConfig,
33 config: &ResolvedCrateConfig,
34 type_defs: &[alef_core::ir::TypeDef],
35 ) -> Result<Vec<GeneratedFile>> {
36 let lang = self.language_name();
37 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
38
39 let mut files = Vec::new();
40
41 let call = &e2e_config.call;
43 let overrides = call.overrides.get(lang);
44 let _module_path = overrides
45 .and_then(|o| o.module.as_ref())
46 .cloned()
47 .unwrap_or_else(|| call.module.clone());
48 let function_name = overrides
49 .and_then(|o| o.function.as_ref())
50 .cloned()
51 .unwrap_or_else(|| call.function.clone());
52 let class_name = overrides
53 .and_then(|o| o.class.as_ref())
54 .cloned()
55 .unwrap_or_else(|| config.name.to_upper_camel_case());
56 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
57 let result_var = &call.result_var;
58
59 let kotlin_android_pkg = e2e_config.resolve_package("kotlin_android");
61 let pkg_name = kotlin_android_pkg
62 .as_ref()
63 .and_then(|p| p.name.as_ref())
64 .cloned()
65 .unwrap_or_else(|| config.name.clone());
66
67 let _kotlin_android_pkg_path = kotlin_android_pkg
69 .as_ref()
70 .and_then(|p| p.path.as_deref())
71 .unwrap_or("../../packages/kotlin-android");
72 let kotlin_android_version = kotlin_android_pkg
73 .as_ref()
74 .and_then(|p| p.version.as_ref())
75 .cloned()
76 .or_else(|| config.resolved_version())
77 .unwrap_or_else(|| "0.1.0".to_string());
78 let kotlin_pkg_id = config.kotlin_package();
79
80 let needs_mock_server = groups
86 .iter()
87 .flat_map(|g| g.fixtures.iter())
88 .any(|f| f.needs_mock_server());
89
90 files.push(GeneratedFile {
92 path: output_base.join("build.gradle.kts"),
93 content: render_build_gradle_kotlin_android(
94 &pkg_name,
95 &kotlin_pkg_id,
96 &kotlin_android_version,
97 e2e_config.dep_mode,
98 needs_mock_server,
99 ),
100 generated_header: false,
101 });
102
103 let mut test_base = output_base.join("src").join("test").join("kotlin");
107 for segment in kotlin_pkg_id.split('.') {
108 test_base = test_base.join(segment);
109 }
110 let test_base = test_base.join("e2e");
111
112 if needs_mock_server {
113 files.push(GeneratedFile {
114 path: test_base.join("MockServerListener.kt"),
115 content: kotlin::render_mock_server_listener_kt(&kotlin_pkg_id),
116 generated_header: true,
117 });
118 files.push(GeneratedFile {
119 path: output_base
120 .join("src")
121 .join("test")
122 .join("resources")
123 .join("META-INF")
124 .join("services")
125 .join("org.junit.platform.launcher.LauncherSessionListener"),
126 content: format!("{kotlin_pkg_id}.e2e.MockServerListener\n"),
127 generated_header: false,
128 });
129 }
130
131 let options_type = overrides.and_then(|o| o.options_type.clone());
133 let field_resolver = FieldResolver::new(
134 &e2e_config.fields,
135 &e2e_config.fields_optional,
136 &e2e_config.result_fields,
137 &e2e_config.fields_array,
138 &HashSet::new(),
139 );
140
141 let struct_names: HashSet<&str> = type_defs.iter().map(|td| td.name.as_str()).collect();
147 let type_enum_fields: std::collections::HashMap<String, HashSet<String>> = type_defs
148 .iter()
149 .filter_map(|td| {
150 let enum_field_names: HashSet<String> = td
151 .fields
152 .iter()
153 .filter(|field| is_enum_typed(&field.ty, &struct_names))
154 .map(|field| field.name.clone())
155 .collect();
156 if enum_field_names.is_empty() {
157 None
158 } else {
159 Some((td.name.clone(), enum_field_names))
160 }
161 })
162 .collect();
163
164 for group in groups {
165 let active: Vec<&Fixture> = group
166 .fixtures
167 .iter()
168 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
169 .collect();
170
171 if active.is_empty() {
172 continue;
173 }
174
175 let class_file_name = format!("{}Test.kt", sanitize_filename(&group.category).to_upper_camel_case());
176 let content = kotlin::render_test_file(
177 &group.category,
178 &active,
179 &class_name,
180 &function_name,
181 &kotlin_pkg_id,
182 result_var,
183 &e2e_config.call.args,
184 options_type.as_deref(),
185 &field_resolver,
186 result_is_simple,
187 &e2e_config.fields_enum,
188 e2e_config,
189 &type_enum_fields,
190 );
191 files.push(GeneratedFile {
192 path: test_base.join(class_file_name),
193 content,
194 generated_header: true,
195 });
196 }
197
198 Ok(files)
199 }
200
201 fn language_name(&self) -> &'static str {
202 "kotlin_android"
203 }
204}
205
206fn is_enum_typed(ty: &alef_core::ir::TypeRef, struct_names: &HashSet<&str>) -> bool {
214 use alef_core::ir::TypeRef;
215 match ty {
216 TypeRef::Named(name) => !struct_names.contains(name.as_str()),
217 TypeRef::Optional(inner) => {
218 matches!(inner.as_ref(), TypeRef::Named(name) if !struct_names.contains(name.as_str()))
219 }
220 _ => false,
221 }
222}
223
224fn render_build_gradle_kotlin_android(
235 _pkg_name: &str,
236 kotlin_pkg_id: &str,
237 _pkg_version: &str,
238 _dep_mode: crate::config::DependencyMode,
239 needs_mock_server: bool,
240) -> String {
241 let kotlin_plugin = maven::KOTLIN_JVM_PLUGIN;
246 let junit = maven::JUNIT;
247 let jackson = maven::JACKSON_E2E;
248 let jvm_target = toolchain::JVM_TARGET;
249 let jna = maven::JNA;
250 let jspecify = maven::JSPECIFY;
251 let coroutines = maven::KOTLINX_COROUTINES_CORE;
252 let launcher_dep = if needs_mock_server {
253 format!(r#" testImplementation("org.junit.platform:junit-platform-launcher:{junit}")"#)
254 } else {
255 String::new()
256 };
257
258 format!(
259 r#"import org.jetbrains.kotlin.gradle.dsl.JvmTarget
260
261plugins {{
262 kotlin("jvm") version "{kotlin_plugin}"
263 java
264}}
265
266group = "{kotlin_pkg_id}"
267version = "0.1.0"
268
269java {{
270 sourceCompatibility = JavaVersion.VERSION_{jvm_target}
271 targetCompatibility = JavaVersion.VERSION_{jvm_target}
272}}
273
274kotlin {{
275 compilerOptions {{
276 jvmTarget.set(JvmTarget.JVM_{jvm_target})
277 }}
278}}
279
280repositories {{
281 mavenCentral()
282}}
283
284sourceSets {{
285 test {{
286 // Include the AAR-bundled Java facade as test sources
287 java.srcDir("../../packages/kotlin-android/src/main/java")
288 // Include the AAR-bundled Kotlin wrapper as test sources
289 kotlin.srcDir("../../packages/kotlin-android/src/main/kotlin")
290 }}
291}}
292
293dependencies {{
294 // JNA for loading libkreuzberg_ffi from java.library.path
295 testImplementation("net.java.dev.jna:jna:{jna}")
296
297 // Jackson for JSON assertion helpers
298 testImplementation("com.fasterxml.jackson.core:jackson-annotations:{jackson}")
299 testImplementation("com.fasterxml.jackson.core:jackson-databind:{jackson}")
300 testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:{jackson}")
301
302 // jspecify for null-safety annotations on wrapped types
303 testImplementation("org.jspecify:jspecify:{jspecify}")
304
305 // Kotlin coroutines for async test helpers
306 testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:{coroutines}")
307
308 // JUnit 5 API and engine
309 testImplementation("org.junit.jupiter:junit-jupiter-api:{junit}")
310 testImplementation("org.junit.jupiter:junit-jupiter-engine:{junit}")
311{launcher_dep}
312
313 // Kotlin stdlib test helpers
314 testImplementation(kotlin("test"))
315}}
316
317tasks.test {{
318 useJUnitPlatform()
319
320 // Resolve libkreuzberg_ffi location (e.g., ../../target/release)
321 val libPath = System.getProperty("kb.lib.path") ?: "${{rootDir}}/../../target/release"
322 systemProperty("java.library.path", libPath)
323 systemProperty("jna.library.path", libPath)
324
325 // Resolve fixture paths (e.g. "docx/fake.docx") against test_documents/
326 workingDir = file("${{rootDir}}/../../test_documents")
327}}
328"#
329 )
330}