alef 0.107.2

Opinionated polyglot binding generator for Rust libraries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
use crate::core::hash::{self, CommentStyle};
use crate::core::template_versions::{maven, toolchain};
use std::fmt::Write as FmtWrite;

/// Render build.gradle.kts for the kotlin e2e project.
pub(crate) fn render_build_gradle(
    pkg_name: &str,
    kotlin_pkg_id: &str,
    pkg_version: &str,
    dep_mode: crate::e2e::config::DependencyMode,
    has_http_fixtures: bool,
    test_documents_path: &str,
) -> String {
    let dep_block = match dep_mode {
        crate::e2e::config::DependencyMode::Registry => {
            // Registry mode: maven central with group:artifact:version.
            //
            // `pkg_name` is the published artifactId only (e.g. `sample_project-kotlin`);
            // the group is `kotlin_pkg_id` (the `[kotlin] package`, e.g.
            // `dev.sample_project`). Guard against a `pkg_name` that already embeds the
            // group (e.g. `dev.sample_project:sample_project-kotlin`) so the group is never
            // prepended twice — otherwise gradle resolves a non-existent
            // `dev.sample_project:dev.sample_project:sample_project-kotlin` coordinate.
            let coordinate = if pkg_name.starts_with(&format!("{kotlin_pkg_id}:")) {
                format!("{pkg_name}:{pkg_version}")
            } else {
                format!("{kotlin_pkg_id}:{pkg_name}:{pkg_version}")
            };
            format!(r#"    testImplementation("{coordinate}")"#)
        }
        crate::e2e::config::DependencyMode::Local => {
            // Local mode: reference the kotlin binding's built jar. The Kotlin
            // module is produced by `gradle build` under
            // `packages/kotlin/build/libs/<jar_name>-<version>.jar`, not by cargo.
            // We must also pull in the binding's runtime dependencies (JNA,
            // Jackson, jspecify, kotlinx-coroutines) since `files()` does not
            // resolve transitive metadata.
            let jar_name = pkg_name.rsplit(':').next().unwrap_or(pkg_name).replace('-', "_");
            let jna = maven::JNA;
            let jackson = maven::JACKSON_E2E;
            // jackson-annotations dropped patch-version releases after 2.19.x (2.20, 2.21,
            // 2.22, ... -- no third component), unlike jackson-databind/datatype-jdk8/
            // module-kotlin, which still publish `major.minor.patch`. `JACKSON_E2E` tracks
            // the latter scheme; using it for jackson-annotations resolves a coordinate
            // (e.g. `2.22.2`) that was never published, and every Local-mode Gradle
            // dependency resolution fails before Kotlin compilation starts. `scaffold::
            // languages::kotlin` (the real package build.gradle.kts generator) already gets
            // this right via the dedicated `JACKSON_ANNOTATIONS` constant; this e2e path had
            // its own copy of the dependency list and never picked it up. ~keep
            let jackson_annotations = maven::JACKSON_ANNOTATIONS;
            let jspecify = maven::JSPECIFY;
            let coroutines = maven::KOTLINX_COROUTINES_CORE;
            format!(
                r#"    testImplementation(files("../../packages/kotlin/build/libs/{jar_name}-{pkg_version}.jar"))
    testImplementation("net.java.dev.jna:jna:{jna}")
    testImplementation("com.fasterxml.jackson.core:jackson-annotations:{jackson_annotations}")
    testImplementation("com.fasterxml.jackson.core:jackson-databind:{jackson}")
    testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:{jackson}")
    testImplementation("org.jspecify:jspecify:{jspecify}")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:{coroutines}")"#
            )
        }
    };

    let kotlin_plugin = maven::KOTLIN_JVM_PLUGIN;
    let junit = maven::JUNIT;
    let jackson = maven::JACKSON_E2E;
    let jvm_target = toolchain::KOTLIN_JVM_TARGET;
    // The JUnit Platform launcher must be on the test classpath at both compile
    // and runtime when HTTP fixtures are present: the Gradle Test Executor loads
    // it at runtime to discover and launch JUnit Platform tests, and the
    // generated `MockServerListener` implements the `LauncherSessionListener`
    // SPI, referencing `org.junit.platform.launcher.{LauncherSession,
    // LauncherSessionListener}` as compile-time symbols. `testRuntimeOnly` keeps
    // it off the compile classpath and fails Kotlin compilation with
    // "Unresolved reference 'launcher'", so use `testImplementation` (mirrors
    // the kotlin_android backend).
    let launcher_dep = if has_http_fixtures {
        format!("\n    testImplementation(\"org.junit.platform:junit-platform-launcher:{junit}\")")
    } else {
        String::new()
    };
    let marker = crate::core::hash::SELF_MARKING_HEADER_LINE;
    format!(
        r#"{marker}

import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {{
    kotlin("jvm") version "{kotlin_plugin}"
    java
}}

group = "{kotlin_pkg_id}"
version = "0.1.0"

java {{
    sourceCompatibility = JavaVersion.VERSION_{jvm_target}
    targetCompatibility = JavaVersion.VERSION_{jvm_target}
}}

kotlin {{
    compilerOptions {{
        jvmTarget.set(JvmTarget.JVM_{jvm_target})
    }}
}}

repositories {{
    mavenCentral()
}}

dependencies {{
{dep_block}
    testImplementation("org.junit.jupiter:junit-jupiter-api:{junit}")
    testImplementation("org.junit.jupiter:junit-jupiter-engine:{junit}")
    testImplementation("com.fasterxml.jackson.core:jackson-databind:{jackson}")
    testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:{jackson}"){launcher_dep}
    testImplementation(kotlin("test"))
}}

tasks.test {{
    useJUnitPlatform()
    val libPath = System.getProperty("native.lib.path") ?: "${{rootDir}}/../../target/release"
    systemProperty("java.library.path", libPath)
    systemProperty("jna.library.path", libPath)
    // Panama FFI bindings are compiled with --enable-preview against the
    // java.lang.foreign API, so the forked test worker must enable preview +
    // native access — otherwise the worker JVM aborts before JUnit starts and
    // Gradle reports a misleading "Gradle Test Executor N ... not in started or
    // detached state". Mirrors the Maven surefire argLine.
    jvmArgs("--enable-preview", "--enable-native-access=ALL-UNNAMED")
    // Resolve fixture paths (e.g. "docx/fake.docx") against the configured
    // test_documents dir. Guard on existence: Gradle test workers fail to fork if
    // workingDir points at a directory that does not exist.
    val testDocuments = file("${{rootDir}}/{test_documents_path}")
    if (testDocuments.isDirectory) {{
        workingDir = testDocuments
    }}
}}
"#
    )
}

/// Render the JUnit Platform `LauncherSessionListener` that spawns the
/// mock-server binary once per launcher session and tears it down on close.
///
/// Mirrors the Java `MockServerListener.java` — same logic, idiomatic Kotlin.
/// The URL is exposed via `System.setProperty("mockServerUrl", url)`;
/// generated test bodies read `System.getenv("MOCK_SERVER_URL")` (which the
/// listener also honours to skip spawning when the caller already has the
/// server running).
pub(crate) fn render_mock_server_listener_kt(kotlin_pkg_id: &str, alt_host: &str) -> String {
    let header = hash::e2e_header(CommentStyle::DoubleSlash);
    format!(
        r#"{header}package {kotlin_pkg_id}.e2e

import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import java.nio.file.Paths
import java.util.regex.Pattern
import org.junit.platform.launcher.LauncherSession
import org.junit.platform.launcher.LauncherSessionListener

/**
 * Spawns the mock-server binary once per JUnit launcher session and
 * exposes its URL as the `mockServerUrl` system property. Generated
 * test bodies read the property (with `MOCK_SERVER_URL` env-var
 * fallback) so tests can run via plain `./gradlew test` without any
 * external mock-server orchestration. Mirrors the Ruby spec_helper /
 * Python conftest spawn pattern. Honors a pre-set MOCK_SERVER_URL by
 * skipping the spawn entirely.
 */
class MockServerListener : LauncherSessionListener {{
    private var mockServer: Process? = null

    override fun launcherSessionOpened(session: LauncherSession) {{
        val preset = System.getenv("MOCK_SERVER_URL")
        if (!preset.isNullOrEmpty()) {{
            System.setProperty("mockServerUrl", preset)
            // Even when MOCK_SERVER_URL is preset (alef test-apps runner mode),
            // the runner also exports MOCK_SERVERS as a JSON env var of
            // fixture_id -> url. Translate it into mockServer.<fixture_id> system
            // properties so tests that target a dedicated per-fixture server
            // (e.g. asset-download tests) resolve it instead of falling back to
            // the shared server. Mirrors the spawn path below and the Java listener.
            val presetServers = System.getenv("MOCK_SERVERS")
            if (!presetServers.isNullOrEmpty()) {{
                System.setProperty("mockServers", presetServers)
                val p = Pattern.compile(""""([^"]+)":"([^"]+)"""")
                val matcher = p.matcher(presetServers)
                while (matcher.find()) {{
                    System.setProperty("mockServer.${{matcher.group(1)}}", matcher.group(2))
                }}
            }}
            return
        }}
        val repoRoot = locateRepoRoot()
            ?: error("MockServerListener: could not locate repo root (looked for fixtures/ in ancestors of ${{System.getProperty("user.dir")}})")
        val binName = if (System.getProperty("os.name", "").orEmpty().lowercase().contains("win")) "mock-server.exe" else "mock-server"
        val bin = repoRoot.resolve("e2e").resolve("rust").resolve("target").resolve("release").resolve(binName).toFile()
        val fixturesDir = repoRoot.resolve("fixtures").toFile()
        check(bin.exists()) {{
            "MockServerListener: mock-server binary not found at $bin — run: cargo build --manifest-path e2e/rust/Cargo.toml --bin mock-server --release"
        }}
        val pb = ProcessBuilder(bin.absolutePath, fixturesDir.absolutePath)
            .redirectErrorStream(false)
        // Baked from `[crates.e2e] alt_host` at generation time. `putIfAbsent` keeps an
        // already-exported override (e.g. a parent `alef test-apps run`) authoritative;
        // `ProcessBuilder.environment()` otherwise already inherits the full parent env. ~keep
        pb.environment().putIfAbsent("ALEF_MOCK_ALT_HOST", "{alt_host}")
        val server = try {{
            pb.start()
        }} catch (e: IOException) {{
            throw IllegalStateException("MockServerListener: failed to start mock-server", e)
        }}
        mockServer = server
        // Read until we see MOCK_SERVER_URL= and optionally MOCK_SERVERS=.
        // Cap the loop so a misbehaving mock-server cannot block indefinitely.
        val stdout = BufferedReader(InputStreamReader(server.inputStream, StandardCharsets.UTF_8))
        var url: String? = null
        try {{
            for (i in 0 until 16) {{
                val line = stdout.readLine() ?: break
                when {{
                    line.startsWith("MOCK_SERVER_URL=") -> {{
                        url = line.removePrefix("MOCK_SERVER_URL=").trim()
                    }}
                    line.startsWith("MOCK_SERVERS=") -> {{
                        val jsonVal = line.removePrefix("MOCK_SERVERS=").trim()
                        System.setProperty("mockServers", jsonVal)
                        // Parse JSON map of fixture_id -> url and expose as system properties.
                        val p = Pattern.compile(""""([^"]+)":"([^"]+)"""")
                        val matcher = p.matcher(jsonVal)
                        while (matcher.find()) {{
                            System.setProperty("mockServer.${{matcher.group(1)}}", matcher.group(2))
                        }}
                        break
                    }}
                    url != null -> break
                }}
            }}
        }} catch (e: IOException) {{
            server.destroyForcibly()
            throw IllegalStateException("MockServerListener: failed to read mock-server stdout", e)
        }}
        if (url.isNullOrEmpty()) {{
            server.destroyForcibly()
            error("MockServerListener: mock-server did not emit MOCK_SERVER_URL")
        }}
        // TCP-readiness probe: ensure axum::serve is accepting before tests start.
        // The mock-server binds the TcpListener synchronously then prints the URL
        // before tokio::spawn(axum::serve(...)) is polled, so under Gradle parallel
        // mode tests can race startup. Poll-connect (max 5s, 50ms backoff) until success.
        val healthUri = java.net.URI.create(url)
        val host = healthUri.host
        val port = healthUri.port
        val deadline = System.nanoTime() + 5_000_000_000L
        while (System.nanoTime() < deadline) {{
            try {{
                java.net.Socket().use {{ s ->
                    s.connect(java.net.InetSocketAddress(host, port), 100)
                    break
                }}
            }} catch (_: java.io.IOException) {{
                try {{ Thread.sleep(50) }} catch (ie: InterruptedException) {{ Thread.currentThread().interrupt(); break }}
            }}
        }}
        System.setProperty("mockServerUrl", url)
        // Drain remaining stdout/stderr in daemon threads so a full pipe
        // does not block the child.
        Thread {{ drain(stdout) }}.also {{ it.isDaemon = true }}.start()
        Thread {{ drain(BufferedReader(InputStreamReader(server.errorStream, StandardCharsets.UTF_8))) }}.also {{ it.isDaemon = true }}.start()
    }}

    override fun launcherSessionClosed(session: LauncherSession) {{
        val server = mockServer ?: return
        try {{ server.outputStream.close() }} catch (_: IOException) {{}}
        try {{
            if (!server.waitFor(2, java.util.concurrent.TimeUnit.SECONDS)) {{
                server.destroyForcibly()
            }}
        }} catch (ie: InterruptedException) {{
            Thread.currentThread().interrupt()
            server.destroyForcibly()
        }}
    }}

    companion object {{
        private fun locateRepoRoot(): Path? {{
            var dir: Path? = Paths.get("").toAbsolutePath()
            while (dir != null) {{
                if (dir.resolve("fixtures").toFile().isDirectory
                    && dir.resolve("e2e").toFile().isDirectory) {{
                    return dir
                }}
                dir = dir.parent
            }}
            return null
        }}

        private fun drain(reader: BufferedReader) {{
            try {{
                val buf = CharArray(1024)
                while (reader.read(buf) >= 0) {{ /* drain */ }}
            }} catch (_: IOException) {{}}
        }}
    }}
}}
"#
    )
}

/// Render SutServerSetup.kt with JUnit 5 @BeforeAll fixture to set SUT_URL.
pub(super) fn render_sut_server_setup_kt(kotlin_pkg_id: &str) -> String {
    let header = hash::e2e_header(CommentStyle::DoubleSlash);

    let mut out = String::new();
    let _ = writeln!(out, "{}", header);
    let _ = writeln!(out, "package {}.e2e", kotlin_pkg_id);
    let _ = writeln!(out);
    let _ = writeln!(out, "import org.junit.jupiter.api.BeforeAll");
    let _ = writeln!(out);
    let _ = writeln!(out, "/**");
    let _ = writeln!(out, " * JUnit 5 setup that ensures SUT_URL is set before tests run.");
    let _ = writeln!(out, " * Defaults to http://127.0.0.1:8007 if not already set.");
    let _ = writeln!(out, " */");
    let _ = writeln!(out, "class SutServerSetup {{");
    let _ = writeln!(out, "    companion object {{");
    let _ = writeln!(out, "        @BeforeAll");
    let _ = writeln!(out, "        @JvmStatic");
    let _ = writeln!(out, "        fun setupSutServer() {{");
    let _ = writeln!(
        out,
        "            val existing = System.getenv(\"SUT_URL\") ?: System.getProperty(\"SUT_URL\")"
    );
    let _ = writeln!(out, "            val url = if (!existing.isNullOrEmpty()) {{");
    let _ = writeln!(out, "                existing");
    let _ = writeln!(out, "            }} else {{");
    let _ = writeln!(out, "                \"http://127.0.0.1:8007\"");
    let _ = writeln!(out, "            }}");
    let _ = writeln!(out, "            System.setProperty(\"SUT_URL\", url)");
    let _ = writeln!(out, "            println(\"Tests will use SUT at: $url\")");
    let _ = writeln!(out, "        }}");
    let _ = writeln!(out, "    }}");
    let _ = writeln!(out, "}}");

    out
}

#[cfg(test)]
mod tests {
    use super::render_build_gradle;

    /// Regression: local-mode build.gradle.kts must pin jackson-annotations to
    /// `JACKSON_ANNOTATIONS`, not the `JACKSON_E2E` version used for
    /// jackson-databind/jackson-datatype-jdk8. jackson-annotations stopped publishing
    /// patch-version releases after 2.19.x (2.20, 2.21, 2.22, ... with no third
    /// component); reusing `JACKSON_E2E`'s `major.minor.patch` value resolves a
    /// coordinate that was never published on Maven Central, so Gradle fails to
    /// resolve the test classpath before Kotlin compilation even starts.
    #[test]
    fn local_dep_pins_jackson_annotations_to_its_own_version_scheme() {
        let out = render_build_gradle(
            "sample_router",
            "dev.sample_router",
            "0.15.6-rc.3",
            crate::e2e::config::DependencyMode::Local,
            false,
            "../../test_documents",
        );
        let annotations_line = out
            .lines()
            .find(|line| line.contains("jackson-annotations"))
            .expect("jackson-annotations dependency line must be emitted");
        assert!(
            annotations_line.contains(&format!(
                "jackson-annotations:{}\"",
                crate::core::template_versions::maven::JACKSON_ANNOTATIONS
            )),
            "jackson-annotations must use JACKSON_ANNOTATIONS's version scheme, got:\n{annotations_line}"
        );
        assert!(
            !annotations_line.contains(crate::core::template_versions::maven::JACKSON_E2E),
            "jackson-annotations must not reuse JACKSON_E2E's patch-versioned scheme, got:\n{annotations_line}"
        );
    }

    /// #442: a non-default `[crates.e2e] alt_host` must be baked into the standalone
    /// mock-server spawn's environment, not silently dropped. A distinctive value
    /// proves the generator threads the configured value through rather than
    /// hard-coding it.
    #[test]
    fn mock_server_listener_bakes_configured_alt_host_into_the_spawn() {
        let out = super::render_mock_server_listener_kt("example", "alt-host-from-config.test");
        assert!(
            out.contains("pb.environment().putIfAbsent(\"ALEF_MOCK_ALT_HOST\", \"alt-host-from-config.test\")"),
            "MockServerListener.kt must set ALEF_MOCK_ALT_HOST to the configured alt_host, got:\n{out}"
        );
    }
}