skir-kotlin-gen 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,469 @@
1
+ [![npm](https://img.shields.io/npm/v/skir-kotlin-gen)](https://www.npmjs.com/package/skir-kotlin-gen)
2
+ [![build](https://github.com/gepheum/skir-kotlin-gen/workflows/Build/badge.svg)](https://github.com/gepheum/skir-kotlin-gen/actions)
3
+
4
+ # Soia's Kotlin code generator
5
+
6
+ Official plugin for generating Kotlin code from [.skir](https://github.com/gepheum/skir) files.
7
+
8
+ ## Installation
9
+
10
+ From your project's root directory, run `npm i --save-dev skir-kotlin-gen`.
11
+
12
+ In your `skir.yml` file, add the following snippet under `generators`:
13
+ ```yaml
14
+ - mod: skir-kotlin-gen
15
+ config: {}
16
+ ```
17
+
18
+ The `npm run skirc` command will now generate .kt files within the `skirout` directory.
19
+
20
+ The generated Kotlin code has a runtime dependency on `build.skir:skir-client`. Add this line to your `build.gradle.kts` file in the `dependencies` section:
21
+
22
+ ```kotlin
23
+ implementation("build.skir:skir-client:1.1.4") // Pick the latest version
24
+ ```
25
+
26
+ For more information, see this Kotlin project [example](https://github.com/gepheum/skir-kotlin-example).
27
+
28
+ ## Kotlin generated code guide
29
+
30
+ The examples below are for the code generated from [this](https://github.com/gepheum/skir-kotlin-example/blob/main/skir-src/user.skir) .skir file.
31
+
32
+ ### Referring to generated symbols
33
+
34
+ ```kotlin
35
+ // Import the given symbols from the Kotlin module generated from "user.skir"
36
+ import skirout.user.User
37
+ import skirout.user.UserRegistry
38
+ import skirout.user.SubscriptionStatus
39
+ import skirout.user.TARZAN
40
+
41
+ // Now you can use: TARZAN, User, UserRegistry, SubscriptionStatus, etc.
42
+ ```
43
+
44
+ ### Frozen struct classes
45
+
46
+ For every struct S in the .skir file, skir generates a frozen (deeply immutable) class `S` and a mutable class `S.Mutable`.
47
+
48
+ ```kotlin
49
+ // Construct a frozen User.
50
+ val john =
51
+ User(
52
+ userId = 42,
53
+ name = "John Doe",
54
+ quote = "Coffee is just a socially acceptable form of rage.",
55
+ pets =
56
+ listOf(
57
+ User.Pet(
58
+ name = "Dumbo",
59
+ heightInMeters = 1.0f,
60
+ picture = "🐘",
61
+ ),
62
+ ),
63
+ subscriptionStatus = SubscriptionStatus.FREE,
64
+ // foo = "bar",
65
+ // ^ Does not compile: 'foo' is not a field of User
66
+ )
67
+
68
+ assert(john.name == "John Doe")
69
+
70
+ // john.name = "John Smith";
71
+ // ^ Does not compile: all the properties are read-only
72
+ ```
73
+
74
+ #### Partial construction
75
+
76
+ ```kotlin
77
+ // With .partial(), you don't need to specify all the fields of the struct.
78
+ val jane =
79
+ User.partial(
80
+ userId = 43,
81
+ name = "Jane Doe",
82
+ pets =
83
+ listOf(
84
+ User.Pet.partial(
85
+ name = "Fido",
86
+ picture = "🐶",
87
+ ),
88
+ ),
89
+ )
90
+
91
+ // Missing fields are initialized to their default values.
92
+ assert(jane.quote == "")
93
+
94
+ // User.partial() with no arguments returns an instance of User with all
95
+ // fields set to their default values.
96
+ assert(User.partial().pets.isEmpty())
97
+ ```
98
+
99
+ #### Creating modified copies
100
+
101
+ ```kotlin
102
+ // User.copy() creates a shallow copy of the struct with the specified fields
103
+ // modified.
104
+ val evilJohn =
105
+ john.copy(
106
+ name = "Evil John",
107
+ quote = "I solemnly swear I am up to no good.",
108
+ )
109
+ assert(evilJohn.name == "Evil John")
110
+ assert(evilJohn.userId == 42)
111
+ ```
112
+
113
+ ### Mutable struct classes
114
+
115
+ `User.Mutable` is a dataclass similar to User except it is mutable.
116
+
117
+ ```kotlin
118
+ val lyla = User.Mutable()
119
+ lyla.userId = 44
120
+ lyla.name = "Lyla Doe"
121
+
122
+ val userHistory = UserHistory.Mutable()
123
+ userHistory.user = lyla
124
+ // ^ The right-hand side of the assignment can be either frozen or mutable.
125
+ ```
126
+
127
+ #### Mutable accessors
128
+
129
+ ```kotlin
130
+ // The 'mutableUser' getter provides access to a mutable version of 'user'.
131
+ // If 'user' is already mutable, it returns it directly.
132
+ // If 'user' is frozen, it creates a mutable shallow copy, assigns it to
133
+ // 'user', and returns it.
134
+
135
+ // The user is currently 'lyla', which is mutable.
136
+ assert(userHistory.mutableUser === lyla)
137
+ // Now assign a frozen User to 'user'.
138
+ userHistory.user = john
139
+ // Since 'john' is frozen, mutableUser makes a mutable shallow copy of it.
140
+ userHistory.mutableUser.name = "John the Second"
141
+ assert(userHistory.user.name == "John the Second")
142
+ assert(userHistory.user.userId == 42)
143
+
144
+ // Similarly, 'mutablePets' provides access to a mutable version of 'pets'.
145
+ // It returns the existing list if already mutable, or creates and returns a
146
+ // mutable shallow copy.
147
+ lyla.mutablePets.add(
148
+ User.Pet(
149
+ name = "Simba",
150
+ heightInMeters = 0.4f,
151
+ picture = "🦁",
152
+ ),
153
+ )
154
+ lyla.mutablePets.add(User.Pet.Mutable(name = "Cupcake"))
155
+
156
+ // lyla.pets.add(User.Pet.Mutable(name = "Cupcake"));
157
+ // ^ Does not compile: 'User.pets' is read-only
158
+ ```
159
+
160
+ ### Converting between frozen and mutable structs
161
+
162
+ ```kotlin
163
+ // toMutable() does a shallow copy of the frozen struct, so it's cheap. All
164
+ // the properties of the copy hold a frozen value.
165
+ val evilJaneBuilder = jane.toMutable()
166
+ evilJaneBuilder.name = "Evil Jane"
167
+ evilJaneBuilder.mutablePets.add(
168
+ User.Pet(
169
+ name = "Shadow",
170
+ heightInMeters = 0.5f,
171
+ picture = "🐺",
172
+ ),
173
+ )
174
+
175
+ // toFrozen() recursively copies the mutable values held by properties of the
176
+ // object.
177
+ val evilJane = evilJaneBuilder.toFrozen()
178
+
179
+ assert(evilJane.name == "Evil Jane")
180
+ assert(evilJane.userId == 43)
181
+ ```
182
+
183
+ #### Type aliases for frozen or mutable
184
+
185
+ ```kotlin
186
+ // 'User_OrMutable' is a type alias for the sealed class that both 'User' and
187
+ // 'User.Mutable' implement.
188
+ val greet: (User_OrMutable) -> Unit = {
189
+ println("Hello, $it")
190
+ }
191
+
192
+ greet(jane)
193
+ // Hello, Jane Doe
194
+ greet(lyla)
195
+ // Hello, Lyla Doe
196
+ ```
197
+
198
+ ### Enum classes
199
+
200
+ Soia generates a deeply immutable Kotlin class for every enum in the .skir file. This class is *not* a Kotlin enum, although the syntax for referring to constants is similar.
201
+
202
+ ```kotlin
203
+ val someStatuses =
204
+ listOf(
205
+ // The UNKNOWN constant is present in all Soia enums even if it is not
206
+ // declared in the .skir file.
207
+ SubscriptionStatus.UNKNOWN,
208
+ SubscriptionStatus.FREE,
209
+ SubscriptionStatus.PREMIUM,
210
+ // Soia generates one subclass {VariantName}Wrapper for every wrapper
211
+ // variant. The constructor of this subclass expects the value to
212
+ // wrap.
213
+ SubscriptionStatus.TrialWrapper(
214
+ SubscriptionStatus.Trial(
215
+ startTime = Instant.now(),
216
+ ),
217
+ ),
218
+ // Same as above (^), with a more concise syntax.
219
+ // Available when the wrapped value is a struct.
220
+ SubscriptionStatus.createTrial(
221
+ startTime = Instant.now(),
222
+ ),
223
+ )
224
+ ```
225
+
226
+ ### Conditions on enums
227
+
228
+ ```kotlin
229
+ assert(john.subscriptionStatus == SubscriptionStatus.FREE)
230
+
231
+ // UNKNOWN is the default value for enums.
232
+ assert(jane.subscriptionStatus == SubscriptionStatus.UNKNOWN)
233
+
234
+ val now = Instant.now()
235
+ val trialStatus: SubscriptionStatus =
236
+ SubscriptionStatus.TrialWrapper(
237
+ Trial(startTime = now),
238
+ )
239
+
240
+ assert(
241
+ trialStatus is SubscriptionStatus.TrialWrapper &&
242
+ trialStatus.value.startTime == now,
243
+ )
244
+ ```
245
+
246
+ #### Branching on enum variants
247
+
248
+ ```kotlin
249
+ val getInfoText: (SubscriptionStatus) -> String = {
250
+ when (it) {
251
+ SubscriptionStatus.FREE -> "Free user"
252
+ SubscriptionStatus.PREMIUM -> "Premium user"
253
+ is SubscriptionStatus.TrialWrapper -> "On trial since ${it.value.startTime}"
254
+ is SubscriptionStatus.Unknown -> "Unknown subscription status"
255
+ }
256
+ }
257
+
258
+ println(getInfoText(john.subscriptionStatus))
259
+ // "Free user"
260
+ ```
261
+
262
+ ### Serialization
263
+
264
+ Every frozen struct class and enum class has a static `serializer` property which can be used for serializing and deserializing instances of the class.
265
+
266
+ ```kotlin
267
+ val serializer = User.serializer
268
+
269
+ // Serialize 'john' to dense JSON.
270
+ println(serializer.toJsonCode(john))
271
+ // [42,"John Doe","Coffee is just a socially acceptable form of rage.",[["Dumbo",1.0,"🐘"]],[1]]
272
+
273
+ // Serialize 'john' to readable JSON.
274
+ println(serializer.toJsonCode(john, JsonFlavor.READABLE))
275
+ // {
276
+ // "user_id": 42,
277
+ // "name": "John Doe",
278
+ // "quote": "Coffee is just a socially acceptable form of rage.",
279
+ // "pets": [
280
+ // {
281
+ // "name": "Dumbo",
282
+ // "height_in_meters": 1.0,
283
+ // "picture": "🐘"
284
+ // }
285
+ // ],
286
+ // "subscription_status": "FREE"
287
+ // }
288
+
289
+ // The dense JSON flavor is the flavor you should pick if you intend to
290
+ // deserialize the value in the future. Soia allows fields to be renamed,
291
+ // and because field names are not part of the dense JSON, renaming a field
292
+ // does not prevent you from deserializing the value.
293
+ // You should pick the readable flavor mostly for debugging purposes.
294
+
295
+ // Serialize 'john' to binary format.
296
+ val johnBytes = serializer.toBytes(john)
297
+
298
+ // The binary format is not human readable, but it is slightly more compact
299
+ // than JSON, and serialization/deserialization can be a bit faster in
300
+ // languages like C++. Only use it when this small performance gain is
301
+ // likely to matter, which should be rare.
302
+ ```
303
+
304
+ ### Deserialization
305
+
306
+ ```kotlin
307
+ // Use fromJson(), fromJsonCode() and fromBytes() to deserialize.
308
+ val reserializedJohn: User =
309
+ serializer.fromJsonCode(serializer.toJsonCode(john))
310
+ assert(reserializedJohn.equals(john))
311
+
312
+ // fromJson/fromJsonCode can deserialize both dense and readable JSON
313
+ val reserializedEvilJohn: User =
314
+ serializer.fromJsonCode(
315
+ serializer.toJsonCode(john, JsonFlavor.READABLE),
316
+ )
317
+ assert(reserializedEvilJohn.equals(evilJohn))
318
+
319
+ val reserializedJane: User =
320
+ serializer.fromBytes(serializer.toBytes(jane))
321
+ assert(reserializedJane.equals(jane))
322
+ ```
323
+
324
+ ### Constants
325
+
326
+ ```kotlin
327
+ println(TARZAN)
328
+ // User(
329
+ // userId = 123,
330
+ // name = "Tarzan",
331
+ // quote = "AAAAaAaAaAyAAAAaAaAaAyAAAAaAaAaA",
332
+ // pets = listOf(
333
+ // User.Pet(
334
+ // name = "Cheeta",
335
+ // heightInMeters = 1.67F,
336
+ // picture = "🐒",
337
+ // ),
338
+ // ),
339
+ // subscriptionStatus = SubscriptionStatus.TrialWrapper(
340
+ // SubscriptionStatus.Trial(
341
+ // startTime = Instant.ofEpochMillis(
342
+ // // 2025-04-02T11:13:29Z
343
+ // 1743592409000L
344
+ // ),
345
+ // )
346
+ // ),
347
+ // )
348
+ ```
349
+
350
+ ### Keyed lists
351
+
352
+ ```kotlin
353
+ // In the .skir file:
354
+ // struct UserRegistry {
355
+ // users: [User|user_id];
356
+ // }
357
+
358
+ val userRegistry = UserRegistry(users = listOf(john, jane, evilJohn))
359
+
360
+ // find() returns the user with the given key (specified in the .skir file).
361
+ // In this example, the key is the user id.
362
+ // The first lookup runs in O(N) time, and the following lookups run in O(1)
363
+ // time.
364
+ assert(userRegistry.users.findByKey(43) === jane)
365
+
366
+ // If multiple elements have the same key, the last one is returned.
367
+ assert(userRegistry.users.findByKey(42) === evilJohn)
368
+ assert(userRegistry.users.findByKey(100) == null)
369
+ ```
370
+
371
+ ### Frozen lists and copies
372
+
373
+ ```kotlin
374
+ // Since all Soia objects are deeply immutable, all lists contained in a
375
+ // Soia object are also deeply immutable.
376
+ // This section helps understand when lists are copied and when they are
377
+ // not.
378
+ val pets: MutableList<Pet> =
379
+ mutableListOf(
380
+ Pet.partial(name = "Fluffy", picture = "🐶"),
381
+ Pet.partial(name = "Fido", picture = "🐻"),
382
+ )
383
+
384
+ val jade =
385
+ User.partial(
386
+ name = "Jade",
387
+ pets = pets,
388
+ // ^ 'pets' is mutable, so Soia makes an immutable shallow copy of it
389
+ )
390
+
391
+ assert(pets == jade.pets)
392
+ assert(pets !== jade.pets)
393
+
394
+ val jack =
395
+ User.partial(
396
+ name = "Jack",
397
+ pets = jade.pets,
398
+ // ^ 'jade.pets' is already immutable, so Soia does not make a copy
399
+ )
400
+
401
+ assert(jack.pets === jade.pets)
402
+ ```
403
+
404
+ ### Soia services
405
+
406
+ #### Starting a skir service on an HTTP server
407
+
408
+ Full example [here](https://github.com/gepheum/skir-kotlin-example/blob/main/src/main/kotlin/examples/startservice/StartService.kt).
409
+
410
+ #### Sending RPCs to a skir service
411
+
412
+ Full example [here](https://github.com/gepheum/skir-kotlin-example/blob/main/src/main/kotlin/examples/callservice/CallService.kt).
413
+
414
+ ### Reflection
415
+
416
+ Reflection allows you to inspect a skir type at runtime.
417
+
418
+ ```kotlin
419
+ println(
420
+ User.typeDescriptor
421
+ .fields
422
+ .map { field -> field.name }
423
+ .toList(),
424
+ )
425
+ // [user_id, name, quote, pets, subscription_status]
426
+
427
+ // A type descriptor can be serialized to JSON and deserialized later.
428
+ val typeDescriptor =
429
+ TypeDescriptor.parseFromJsonCode(
430
+ User.serializer.typeDescriptor.asJsonCode(),
431
+ )
432
+
433
+ assert(typeDescriptor is StructDescriptor)
434
+ assert((typeDescriptor as StructDescriptor).fields.size == 5)
435
+
436
+ // The 'allStringsToUpperCase' function uses reflection to convert all the
437
+ // strings contained in a given Soia value to upper case.
438
+ // See the implementation at
439
+ // https://github.com/gepheum/skir-kotlin-example/blob/main/src/main/kotlin/AllStringsToUpperCase.kt
440
+ println(allStringsToUpperCase(TARZAN, User.typeDescriptor))
441
+ // User(
442
+ // userId = 123,
443
+ // name = "TARZAN",
444
+ // quote = "AAAAAAAAAAYAAAAAAAAAAYAAAAAAAAAA",
445
+ // pets = listOf(
446
+ // User.Pet(
447
+ // name = "CHEETA",
448
+ // heightInMeters = 1.67F,
449
+ // picture = "🐒",
450
+ // ),
451
+ // ),
452
+ // subscriptionStatus = SubscriptionStatus.TrialWrapper(
453
+ // SubscriptionStatus.Trial(
454
+ // startTime = Instant.ofEpochMillis(
455
+ // // 2025-04-02T11:13:29Z
456
+ // 1743592409000L
457
+ // ),
458
+ // )
459
+ // ),
460
+ // )
461
+ ```
462
+
463
+ ## Java codegen versus Kotlin codegen
464
+
465
+ While Java and Kotlin code can interoperate seamlessly, Soia provides separate code generators for each language to leverage their unique strengths and idioms. For instance, the Kotlin generator utilizes named parameters for struct construction, whereas the Java generator employs the builder pattern.
466
+
467
+ Although it's technically feasible to use Kotlin-generated code in a Java project (or vice versa), doing so results in an API that feels unnatural and cumbersome in the calling language. For the best developer experience, use the code generator that matches your project's primary language.
468
+
469
+ Note that both the Java and Kotlin generated code share the same runtime dependency: `build.skir:skir-client`.
@@ -0,0 +1,17 @@
1
+ import { type CodeGenerator } from "skir-internal";
2
+ import { z } from "zod";
3
+ declare const Config: z.ZodObject<{
4
+ packagePrefix: z.ZodOptional<z.ZodString>;
5
+ }, z.core.$strip>;
6
+ type Config = z.infer<typeof Config>;
7
+ declare class KotlinCodeGenerator implements CodeGenerator<Config> {
8
+ readonly id = "kotlin";
9
+ readonly configType: z.ZodObject<{
10
+ packagePrefix: z.ZodOptional<z.ZodString>;
11
+ }, z.core.$strip>;
12
+ readonly version = "1.0.0";
13
+ generateCode(input: CodeGenerator.Input<Config>): CodeGenerator.Output;
14
+ }
15
+ export declare const GENERATOR: KotlinCodeGenerator;
16
+ export {};
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAWnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,QAAA,MAAM,MAAM;;iBAKV,CAAC;AAEH,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAErC,cAAM,mBAAoB,YAAW,aAAa,CAAC,MAAM,CAAC;IACxD,QAAQ,CAAC,EAAE,YAAY;IACvB,QAAQ,CAAC,UAAU;;sBAAU;IAC7B,QAAQ,CAAC,OAAO,WAAW;IAE3B,YAAY,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,aAAa,CAAC,MAAM;CAevE;AAi8BD,eAAO,MAAM,SAAS,qBAA4B,CAAC"}