Skip to main content

showcase/
showcase.rs

1//! Every construct the model can emit, in one runnable generator.
2//!
3//! This is what a real code generator does: build `KtFile` fragments, merge
4//! them so each package collapses to one file, validate, and render. Run it
5//! with:
6//!
7//! ```text
8//! cargo run --example showcase
9//! ```
10//!
11//! The output is checked against `tests/golden/showcase.txt` by
12//! `tests/examples.rs`, so this doubles as a readable regression test for the
13//! renderer.
14
15use kotlin_codegen::{
16    escape_kotlin_ident, is_kotlin_hard_keyword, is_valid_kotlin_ident, is_valid_kotlin_package,
17    mangle_kotlin_ident, mangle_kotlin_package, merge_files, merged_file_path, KtClass,
18    KtClassModifier, KtCode, KtCompanion, KtCtorParam, KtDecl, KtEnumEntry, KtFile, KtFun,
19    KtFunInterface, KtFunSig, KtParam, KtProperty, KtType, KtVis,
20};
21
22fn main() {
23    let fragments = vec![
24        types_fragment(),
25        session_fragment(),
26        natives_fragment(),
27        root_fragment(),
28    ];
29
30    let files = merge_files(fragments).expect("the showcase model is valid");
31
32    println!("=== generated files ===");
33    for file in &files {
34        let path = merged_file_path(std::path::Path::new("kotlin"), file, "Generated");
35        println!("\n--- {} ---", path.display());
36        print!("{}", file.render());
37    }
38
39    println!("\n=== identifier helpers ===");
40    for raw in ["myValue", "my-field", "2fast", "object", "data", ""] {
41        let quoted = format!("{raw:?}");
42        let mangled = format!("{:?}", mangle_kotlin_ident(raw));
43        let escaped = format!("{:?}", escape_kotlin_ident(raw));
44        let valid = is_valid_kotlin_ident(raw);
45        let keyword = is_kotlin_hard_keyword(raw);
46        println!(
47            "{quoted:>10} -> valid={valid:<5} keyword={keyword:<5} \
48             mangled={mangled:<10} escaped={escaped}"
49        );
50    }
51    for raw in ["io.example.api", "fun.my-pkg", "io..jni."] {
52        let quoted = format!("{raw:?}");
53        let mangled = format!("{:?}", mangle_kotlin_package(raw));
54        let valid = is_valid_kotlin_package(raw);
55        println!("{quoted:>16} -> valid={valid:<5} mangled={mangled}");
56    }
57}
58
59/// Data-carrying types: enum, data class, value class, and a sealed
60/// interface whose alternatives nest inside it.
61fn types_fragment() -> KtFile {
62    // `enum class` with a primary constructor its entries call, plus a named
63    // companion object holding a factory.
64    let priority = KtClass::enum_("Priority")
65        .vis(KtVis::Public)
66        .kdoc("Delivery priority.\n\nMirrors the native `z_priority_t`.")
67        .ctor_param(
68            KtCtorParam::new("code", KtType::int())
69                .val()
70                .vis(KtVis::Public),
71        )
72        .entry(KtEnumEntry::with_args("REAL_TIME", "1"))
73        .entry(KtEnumEntry::with_args("INTERACTIVE", "4"))
74        .entry(KtEnumEntry::with_args("DATA", "5"))
75        .companion(
76            KtCompanion::named("Codes")
77                .vis(KtVis::Public)
78                .kdoc("Lookup helpers keyed by the native code.")
79                .member(
80                    KtFun::new("fromInt")
81                        .vis(KtVis::Public)
82                        .annotation("JvmStatic")
83                        .param(KtParam::new("value", KtType::int()))
84                        .returns(KtType::cls("Priority"))
85                        .body(
86                            KtCode::new()
87                                .blk("return when (value) {", |c| {
88                                    c.line("1 -> REAL_TIME")
89                                        .line("4 -> INTERACTIVE")
90                                        .line("5 -> DATA")
91                                        .line("else -> throw IllegalArgumentException(\"bad priority: $value\")")
92                                }),
93                        ),
94                ),
95        );
96
97    // `@JvmInline value class` — exactly one read-only property.
98    let zid = KtClass::value(
99        "ZenohId",
100        KtCtorParam::new("bytes", KtType::byte_array())
101            .val()
102            .vis(KtVis::Public),
103    )
104    .vis(KtVis::Public)
105    .kdoc("A 16-byte peer identifier, carried by value.");
106
107    // `data class` — every constructor parameter is a property.
108    let sample = KtClass::data(
109        "Sample",
110        KtCtorParam::new("keyExpr", KtType::string())
111            .val()
112            .vis(KtVis::Public),
113    )
114    .vis(KtVis::Public)
115    .ctor_param(
116        KtCtorParam::new("payload", KtType::byte_array())
117            .val()
118            .vis(KtVis::Public),
119    )
120    .ctor_param(
121        KtCtorParam::new("priority", KtType::cls("Priority"))
122            .val()
123            .vis(KtVis::Public)
124            .default("Priority.DATA"),
125    )
126    .ctor_param(
127        KtCtorParam::new("attachment", KtType::byte_array().nullable())
128            .var()
129            .vis(KtVis::Public)
130            .annotation("JvmField")
131            .default("null"),
132    );
133
134    // A `sealed interface` whose alternatives nest inside it: a `data class`
135    // with a payload and a `data object` without one.
136    let reply = KtClass::sealed_interface("Reply")
137        .vis(KtVis::Public)
138        .kdoc("Either a sample or the end of the stream.")
139        .member(
140            KtClass::data(
141                "Value",
142                KtCtorParam::new("sample", KtType::cls("Sample"))
143                    .val()
144                    .vis(KtVis::Public),
145            )
146            .vis(KtVis::Public)
147            .implements(KtType::cls("Reply")),
148        )
149        .member(
150            KtClass::data_object("Done")
151                .vis(KtVis::Public)
152                .implements(KtType::cls("Reply")),
153        );
154
155    KtFile::new("io.example.api")
156        .decl(priority)
157        .decl(zid)
158        .decl(sample)
159        .decl(reply)
160}
161
162/// The handle surface: an abstract base, a concrete subclass, an interface,
163/// a `fun interface`, and a type alias.
164fn session_fragment() -> KtFile {
165    // `abstract class` with an `abstract` member (bodiless, and it says so),
166    // a `@Volatile` property, and an interface it implements.
167    let base = KtClass::class_with(KtClassModifier::Abstract, "NativeHandle")
168        .vis(KtVis::Public)
169        .kdoc("Owns a raw pointer into Rust and frees it exactly once.")
170        .ctor_param(KtCtorParam::new("initialPtr", KtType::long()))
171        .implements(KtType::cls("AutoCloseable"))
172        .member(
173            KtProperty::var("ptr")
174                .ty(KtType::long())
175                .vis(KtVis::Internal)
176                .annotation("Volatile")
177                .kdoc("Zero once closed.")
178                .initializer("initialPtr"),
179        )
180        .member(
181            KtProperty::val("isClosed")
182                .ty(KtType::boolean())
183                .vis(KtVis::Public)
184                .accessors(KtCode::new().line("get() = ptr == 0L")),
185        )
186        .member(
187            KtFun::new("freePtr")
188                .vis(KtVis::Public)
189                .modifier("abstract")
190                .kdoc("Release the native allocation. Called once, under the lock.")
191                .param(KtParam::new("ptr", KtType::long())),
192        );
193
194    // `open class` extending the abstract base and constructing it.
195    let session = KtClass::class_with(KtClassModifier::Open, "Session")
196        .vis(KtVis::Public)
197        .annotation("Suppress(\"unused\")")
198        .ctor_param(KtCtorParam::new("initialPtr", KtType::long()))
199        .extends(KtType::cls("NativeHandle"), Some("initialPtr"))
200        .implements(KtType::cls("io.example.api.Describable"))
201        .member(
202            KtFun::new("freePtr")
203                .vis(KtVis::Public)
204                .modifier("override")
205                .param(KtParam::new("ptr", KtType::long()))
206                .body(KtCode::new().line("JNINative.sessionFree(ptr)")),
207        )
208        .member(
209            KtFun::new("describe")
210                .vis(KtVis::Public)
211                .modifier("override")
212                .returns(KtType::string())
213                .expr_body(KtCode::new().line("\"Session(0x${ptr.toString(16)})\"")),
214        )
215        // A generic method whose body uses every `KtCode` block form.
216        .member(
217            KtFun::new("get")
218                .vis(KtVis::Public)
219                .generic("R")
220                .kdoc("Run a query, folding each reply into an accumulator.")
221                .param(KtParam::new("selector", KtType::string()))
222                .param(KtParam::new(
223                    "onReply",
224                    KtType::lambda(
225                        [("reply".to_string(), KtType::cls("io.example.api.Reply"))],
226                        KtType::var_r(),
227                    ),
228                ))
229                .param(KtParam::new("timeoutMs", KtType::long()).default("10_000L"))
230                .returns(KtType::generic("List", [KtType::var_r()]))
231                .body(
232                    KtCode::new()
233                        .line("val acc = ArrayList<R>()")
234                        .line("val guard = Guard.acquire(this)")
235                        .import("io.example.internal.Guard")
236                        .try_finally(
237                            "",
238                            KtCode::new().blk("JNINative.sessionGet(ptr, selector, timeoutMs) { raw ->", |c| {
239                                c.line("acc.add(onReply(raw))")
240                            }),
241                            KtCode::new().line("guard.release()"),
242                        )
243                        .line("")
244                        // A long call that the renderer breaks by width at its
245                        // real nesting level.
246                        .wline("reportQueryOutcome(selector, timeoutMs, acc.size, acc.isNotEmpty(), \"query finished\", System.nanoTime())")
247                        .line("return acc"),
248                ),
249        )
250        // The interface's member extension, implemented.
251        .member(
252            KtFun::new("label")
253                .vis(KtVis::Public)
254                .modifier("override")
255                .receiver(KtType::cls("Sample"))
256                .returns(KtType::string())
257                .expr_body(KtCode::new().line("\"${keyExpr}@${describe()}\"")),
258        )
259        // A member extension of the class itself.
260        .member(
261            KtFun::new("toSample")
262                .vis(KtVis::Public)
263                .receiver(KtType::byte_array())
264                .returns(KtType::cls("Sample"))
265                .expr_body(KtCode::new().line("Sample(describe(), this)")),
266        )
267        // A nested class — its own scope, so its members may reuse names.
268        .member(
269            KtClass::class_("Config")
270                .vis(KtVis::Public)
271                .member(KtProperty::val("describe").initializer("\"config\"")),
272        )
273        .companion(
274            KtCompanion::new()
275                .vis(KtVis::Public)
276                .member(
277                    KtFun::new("open")
278                        .vis(KtVis::Public)
279                        .annotation("JvmStatic")
280                        .param(KtParam::new("config", KtType::string()).default("\"{}\""))
281                        .returns(KtType::cls("Session"))
282                        .expr_body(KtCode::new().line("Session(JNINative.sessionOpen(config))")),
283                ),
284        );
285
286    // A plain `interface`: bodiless members are abstract by position, and a
287    // `KtFunSig` is exactly that.
288    let describable = KtClass::interface_("Describable")
289        .vis(KtVis::Public)
290        .member(KtFunSig::new("describe").returns(KtType::string()))
291        // A member extension: abstract here, supplied by the implementor.
292        // `KtFunSig` carries a receiver too, so a signature does not quietly
293        // become a plain member.
294        .member(
295            KtFunSig::new("label")
296                .receiver(KtType::cls("Sample"))
297                .returns(KtType::string()),
298        );
299
300    // A `fun interface` (SAM) — its single method cannot carry a body.
301    let handler = KtFunInterface::new(
302        "ReplyHandler",
303        KtFunSig::new("onReply")
304            .param(KtParam::new("reply", KtType::cls("Reply")))
305            .returns(KtType::var_r()),
306    )
307    .vis(KtVis::Public)
308    .type_param("out R")
309    .kdoc("Invoked from the native thread for each reply.");
310
311    // Top-level extension functions. The receiver is a type, not part of the
312    // name, so it resolves through the import set and the name stays a plain
313    // identifier the validator can check as one.
314    let summary = KtFun::new("summary")
315        .vis(KtVis::Public)
316        .receiver(KtType::cls("Sample"))
317        .returns(KtType::string())
318        .expr_body(KtCode::new().line("\"$keyExpr (${payload.size} bytes)\""));
319
320    // Generics render before the receiver, which renders before the name.
321    let map_replies = KtFun::new("mapValues")
322        .vis(KtVis::Public)
323        .generic("R")
324        .receiver(KtType::generic("List", [KtType::cls("Reply")]))
325        .param(KtParam::new(
326            "transform",
327            KtType::lambda(
328                [("sample".to_string(), KtType::cls("Sample"))],
329                KtType::var_r(),
330            ),
331        ))
332        .returns(KtType::generic("List", [KtType::var_r()]))
333        .expr_body(
334            KtCode::new().line("filterIsInstance<Reply.Value>().map { transform(it.sample) }"),
335        );
336
337    // An extension on a *function type*. The receiver needs parentheses here
338    // or the `.` would bind to the return type instead.
339    let as_raw = KtFun::new("asRaw")
340        .vis(KtVis::Internal)
341        .receiver(KtType::lambda(
342            [("sample".to_string(), KtType::cls("Sample"))],
343            KtType::unit(),
344        ))
345        .returns(KtType::cls("io.example.api.internal.RawSink"))
346        // The proxy adapts a typed callback to the raw one the natives call,
347        // so it has to narrow `Reply` to the `Sample` the receiver takes.
348        .expr_body(
349            KtCode::new().line("RawSink { raw -> if (raw is Reply.Value) this(raw.sample) }"),
350        );
351
352    KtFile::new("io.example.api")
353        // FQNs named only from raw body text, which the model cannot see.
354        .imports(["io.example.api.internal.JNINative".to_string()])
355        .decl(base)
356        .decl(describable)
357        .decl(handler)
358        .decl(session)
359        .decl(summary)
360        .decl(map_replies)
361        .decl(as_raw)
362        .decl(KtDecl::TypeAlias {
363            vis: KtVis::Public,
364            name: "SampleList".to_string(),
365            target: KtType::generic("List", [KtType::cls("Sample")]),
366        })
367}
368
369/// The `external` surface — an object of natives, in its own subpackage.
370fn natives_fragment() -> KtFile {
371    let natives = KtClass::object_("JNINative")
372        .vis(KtVis::Internal)
373        .kdoc("One-to-one with the exported Rust symbols.")
374        .member(
375            KtFun::new("sessionOpen")
376                .vis(KtVis::Internal)
377                .param(KtParam::new("config", KtType::string()))
378                .returns(KtType::long())
379                .external(),
380        )
381        .member(
382            KtFun::new("sessionFree")
383                .vis(KtVis::Internal)
384                .param(KtParam::new("ptr", KtType::long()))
385                .external(),
386        )
387        .member(
388            KtFun::new("sessionGet")
389                .vis(KtVis::Internal)
390                .param(KtParam::new("ptr", KtType::long()))
391                .param(KtParam::new("selector", KtType::string()))
392                .param(KtParam::new("timeoutMs", KtType::long()))
393                .param(KtParam::new(
394                    "sink",
395                    KtType::lambda(
396                        [("reply".to_string(), KtType::cls("io.example.api.Reply"))],
397                        KtType::unit(),
398                    ),
399                ))
400                .external(),
401        );
402
403    // A pre-rendered block, for text the model does not describe. Its name is
404    // a merge identity, never emitted.
405    let loader = KtDecl::Raw {
406        name: "__loadNative".to_string(),
407        code: KtCode::raw_reindent(
408            "internal val __loaded: Boolean = run {\n\
409             System.loadLibrary(\"example_jni\")\n\
410             true\n\
411             }",
412        ),
413    };
414
415    let raw_sink = KtFunInterface::new(
416        "RawSink",
417        KtFunSig::new("accept").param(KtParam::new("reply", KtType::cls("io.example.api.Reply"))),
418    )
419    .vis(KtVis::Internal);
420
421    KtFile::new("io.example.api.internal")
422        .decl(raw_sink)
423        .decl(natives)
424        .decl(loader)
425        // An FQN referenced only from raw text the model cannot see.
426        .import("io.example.api.Reply")
427}
428
429/// A file in the default (root) package, exercising a banner override and
430/// top-level declarations of every value kind.
431fn root_fragment() -> KtFile {
432    KtFile::new("")
433        .banner("// Hand-tuned banner for the root package.")
434        .decl(
435            KtProperty::val("LIBRARY_VERSION")
436                .ty(KtType::string())
437                .vis(KtVis::Public)
438                .kdoc("Version this binding was generated against.")
439                .initializer("\"1.9.0\""),
440        )
441        // A keyword modifier rendered between visibility and `val`.
442        .decl(
443            KtProperty::val("PROTOCOL")
444                .ty(KtType::string())
445                .vis(KtVis::Public)
446                .modifier("const")
447                .initializer("\"tcp\""),
448        )
449        // A delegated property: `by <expr>` rather than `= <expr>`.
450        .decl(
451            KtProperty::val("defaultTimeout")
452                .vis(KtVis::Internal)
453                .ty(KtType::cls("java.time.Duration"))
454                .delegate("lazy { Duration.ofSeconds(10) }"),
455        )
456        .decl(
457            KtFun::new("describeAll")
458                .vis(KtVis::Public)
459                // Generic parameter lists are free-form text, so a bound is
460                // written as given — it is not shortened against the imports.
461                .generic("T : io.example.api.Describable")
462                .param(KtParam::new(
463                    "items",
464                    KtType::generic("List", [KtType::var_("T")]),
465                ))
466                .returns(KtType::string())
467                .expr_body(KtCode::new().line("items.joinToString { it.describe() }")),
468        )
469}