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 = kotlin_android_pkg
91 .as_ref()
92 .and_then(|p| p.module.clone())
93 .or_else(|| config.kotlin_android.as_ref().and_then(|c| c.package.clone()))
94 .unwrap_or_else(|| config.kotlin_package());
95
96 let needs_mock_server = groups
102 .iter()
103 .flat_map(|g| g.fixtures.iter())
104 .any(|f| f.needs_mock_server());
105
106 files.push(GeneratedFile {
108 path: output_base.join("build.gradle.kts"),
109 content: render_build_gradle_kotlin_android(
110 &pkg_name,
111 &kotlin_pkg_id,
112 &kotlin_android_version,
113 e2e_config.dep_mode,
114 needs_mock_server,
115 ),
116 generated_header: false,
117 });
118
119 let mut test_base = output_base.join("src").join("test").join("kotlin");
123 for segment in kotlin_pkg_id.split('.') {
124 test_base = test_base.join(segment);
125 }
126 let test_base = test_base.join("e2e");
127
128 if needs_mock_server {
129 files.push(GeneratedFile {
130 path: test_base.join("MockServerListener.kt"),
131 content: kotlin::render_mock_server_listener_kt(&kotlin_pkg_id),
132 generated_header: true,
133 });
134 files.push(GeneratedFile {
135 path: output_base
136 .join("src")
137 .join("test")
138 .join("resources")
139 .join("META-INF")
140 .join("services")
141 .join("org.junit.platform.launcher.LauncherSessionListener"),
142 content: format!("{kotlin_pkg_id}.e2e.MockServerListener\n"),
143 generated_header: false,
144 });
145 }
146
147 let options_type = overrides.and_then(|o| o.options_type.clone());
149 let field_resolver = FieldResolver::new(
150 &e2e_config.fields,
151 &e2e_config.fields_optional,
152 &e2e_config.result_fields,
153 &e2e_config.fields_array,
154 &HashSet::new(),
155 );
156
157 let struct_names: HashSet<&str> = type_defs.iter().map(|td| td.name.as_str()).collect();
163 let type_enum_fields: std::collections::HashMap<String, HashSet<String>> = type_defs
164 .iter()
165 .filter_map(|td| {
166 let enum_field_names: HashSet<String> = td
167 .fields
168 .iter()
169 .filter(|field| is_enum_typed(&field.ty, &struct_names))
170 .map(|field| field.name.clone())
171 .collect();
172 if enum_field_names.is_empty() {
173 None
174 } else {
175 Some((td.name.clone(), enum_field_names))
176 }
177 })
178 .collect();
179
180 for group in groups {
181 let active: Vec<&Fixture> = group
182 .fixtures
183 .iter()
184 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
185 .collect();
186
187 if active.is_empty() {
188 continue;
189 }
190
191 let class_file_name = format!("{}Test.kt", sanitize_filename(&group.category).to_upper_camel_case());
192 let content = kotlin::render_test_file_android(
193 &group.category,
194 &active,
195 &class_name,
196 &function_name,
197 &kotlin_pkg_id,
198 result_var,
199 &e2e_config.call.args,
200 options_type.as_deref(),
201 &field_resolver,
202 result_is_simple,
203 &e2e_config.fields_enum,
204 e2e_config,
205 &type_enum_fields,
206 );
207 files.push(GeneratedFile {
208 path: test_base.join(class_file_name),
209 content,
210 generated_header: true,
211 });
212 }
213
214 Ok(files)
215 }
216
217 fn language_name(&self) -> &'static str {
218 "kotlin_android"
219 }
220}
221
222fn is_enum_typed(ty: &alef_core::ir::TypeRef, struct_names: &HashSet<&str>) -> bool {
230 use alef_core::ir::TypeRef;
231 match ty {
232 TypeRef::Named(name) => !struct_names.contains(name.as_str()),
233 TypeRef::Optional(inner) => {
234 matches!(inner.as_ref(), TypeRef::Named(name) if !struct_names.contains(name.as_str()))
235 }
236 _ => false,
237 }
238}
239
240fn render_build_gradle_kotlin_android(
251 _pkg_name: &str,
252 kotlin_pkg_id: &str,
253 _pkg_version: &str,
254 _dep_mode: crate::config::DependencyMode,
255 needs_mock_server: bool,
256) -> String {
257 let kotlin_plugin = maven::KOTLIN_JVM_PLUGIN;
262 let junit = maven::JUNIT;
263 let jackson = maven::JACKSON_E2E;
264 let jvm_target = toolchain::JVM_TARGET;
265 let jna = maven::JNA;
266 let jspecify = maven::JSPECIFY;
267 let coroutines = maven::KOTLINX_COROUTINES_CORE;
268 let launcher_dep = if needs_mock_server {
269 format!(r#" testImplementation("org.junit.platform:junit-platform-launcher:{junit}")"#)
270 } else {
271 String::new()
272 };
273
274 format!(
275 r#"import org.jetbrains.kotlin.gradle.dsl.JvmTarget
276
277plugins {{
278 kotlin("jvm") version "{kotlin_plugin}"
279 java
280}}
281
282group = "{kotlin_pkg_id}"
283version = "0.1.0"
284
285java {{
286 sourceCompatibility = JavaVersion.VERSION_{jvm_target}
287 targetCompatibility = JavaVersion.VERSION_{jvm_target}
288}}
289
290kotlin {{
291 compilerOptions {{
292 jvmTarget.set(JvmTarget.JVM_{jvm_target})
293 }}
294}}
295
296repositories {{
297 mavenCentral()
298}}
299
300sourceSets {{
301 test {{
302 // Include the AAR-bundled Java facade as test sources
303 java.srcDir("../../packages/kotlin-android/src/main/java")
304 // Include the AAR-bundled Kotlin wrapper as test sources
305 kotlin.srcDir("../../packages/kotlin-android/src/main/kotlin")
306 }}
307}}
308
309dependencies {{
310 // JNA for loading libkreuzberg_ffi from java.library.path
311 testImplementation("net.java.dev.jna:jna:{jna}")
312
313 // Jackson for JSON assertion helpers
314 testImplementation("com.fasterxml.jackson.core:jackson-annotations:{jackson}")
315 testImplementation("com.fasterxml.jackson.core:jackson-databind:{jackson}")
316 testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:{jackson}")
317
318 // jspecify for null-safety annotations on wrapped types
319 testImplementation("org.jspecify:jspecify:{jspecify}")
320
321 // Kotlin coroutines for async test helpers
322 testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:{coroutines}")
323
324 // JUnit 5 API and engine
325 testImplementation("org.junit.jupiter:junit-jupiter-api:{junit}")
326 testImplementation("org.junit.jupiter:junit-jupiter-engine:{junit}")
327{launcher_dep}
328
329 // Kotlin stdlib test helpers
330 testImplementation(kotlin("test"))
331}}
332
333tasks.test {{
334 useJUnitPlatform()
335
336 // Resolve libkreuzberg_ffi location (e.g., ../../target/release)
337 val libPath = System.getProperty("kb.lib.path") ?: "${{rootDir}}/../../target/release"
338 systemProperty("java.library.path", libPath)
339 systemProperty("jna.library.path", libPath)
340
341 // Resolve fixture paths (e.g. "docx/fake.docx") against test_documents/
342 workingDir = file("${{rootDir}}/../../test_documents")
343}}
344"#
345 )
346}