skir-java-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/src/index.ts ADDED
@@ -0,0 +1,1124 @@
1
+ // TODO: add comments to generated code
2
+ // TODO: add comments to records/fields/methods
3
+ // TODO: s/field/variant for enums
4
+
5
+ import {
6
+ type CodeGenerator,
7
+ type Constant,
8
+ convertCase,
9
+ Doc,
10
+ type Method,
11
+ type Record,
12
+ type RecordKey,
13
+ type RecordLocation,
14
+ type ResolvedType,
15
+ } from "skir-internal";
16
+ import { z } from "zod";
17
+ import { Namer, toEnumConstantName } from "./naming.js";
18
+ import { TypeSpeller } from "./type_speller.js";
19
+
20
+ const Config = z.object({
21
+ packagePrefix: z
22
+ .string()
23
+ .regex(/^([a-z_$][a-z0-9_$]*\.)*$/)
24
+ .optional(),
25
+ });
26
+
27
+ type Config = z.infer<typeof Config>;
28
+
29
+ class JavaCodeGenerator implements CodeGenerator<Config> {
30
+ readonly id = "java";
31
+ readonly configType = Config;
32
+ readonly version = "1.0.0";
33
+
34
+ generateCode(input: CodeGenerator.Input<Config>): CodeGenerator.Output {
35
+ const { recordMap, config } = input;
36
+ const javaSourceFiles: JavaSourceFileGenerator[] = [];
37
+ for (const module of input.modules) {
38
+ for (const record of module.records) {
39
+ if (record.recordAncestors.length !== 1) {
40
+ // Only consider top-level records
41
+ continue;
42
+ }
43
+ javaSourceFiles.push(
44
+ new JavaSourceFileGenerator(
45
+ record.record,
46
+ module.path,
47
+ recordMap,
48
+ config,
49
+ ),
50
+ );
51
+ }
52
+ if (module.methods.length > 0) {
53
+ javaSourceFiles.push(
54
+ new JavaSourceFileGenerator(
55
+ {
56
+ kind: "methods",
57
+ methods: module.methods,
58
+ },
59
+ module.path,
60
+ recordMap,
61
+ config,
62
+ ),
63
+ );
64
+ }
65
+ if (module.constants.length > 0) {
66
+ javaSourceFiles.push(
67
+ new JavaSourceFileGenerator(
68
+ {
69
+ kind: "constants",
70
+ constants: module.constants,
71
+ },
72
+ module.path,
73
+ recordMap,
74
+ config,
75
+ ),
76
+ );
77
+ }
78
+ }
79
+ const outputFiles = javaSourceFiles.map((sourceFile) => ({
80
+ path: sourceFile.path,
81
+ code: sourceFile.generate(),
82
+ }));
83
+ return { files: outputFiles };
84
+ }
85
+ }
86
+
87
+ type JavaSourceFileTarget =
88
+ | Record
89
+ | {
90
+ kind: "methods";
91
+ methods: readonly Method[];
92
+ }
93
+ | {
94
+ kind: "constants";
95
+ constants: readonly Constant[];
96
+ };
97
+
98
+ // Generates the code for one Java file.
99
+ class JavaSourceFileGenerator {
100
+ constructor(
101
+ private readonly target: JavaSourceFileTarget,
102
+ private readonly modulePath: string,
103
+ private readonly recordMap: ReadonlyMap<RecordKey, RecordLocation>,
104
+ config: Config,
105
+ ) {
106
+ this.packagePrefix = config.packagePrefix ?? "";
107
+ this.namer = new Namer(this.packagePrefix);
108
+ this.typeSpeller = new TypeSpeller(recordMap, this.namer);
109
+ }
110
+
111
+ generate(): string {
112
+ // http://patorjk.com/software/taag/#f=Doom&t=Do%20not%20edit
113
+ this.push(
114
+ `// ______ _ _ _ _
115
+ // | _ \\ | | | |(_)| |
116
+ // | | | | ___ _ __ ___ | |_ ___ __| | _ | |_
117
+ // | | | | / _ \\ | '_ \\ / _ \\ | __| / _ \\ / _\` || || __|
118
+ // | |/ / | (_) | | | | || (_) || |_ | __/| (_| || || |_
119
+ // |___/ \\___/ |_| |_| \\___/ \\__| \\___| \\__,_||_| \\__|
120
+ //
121
+
122
+ // To install the skir client library, add:
123
+ // implementation("build.skir:skir-kotlin-client:latest.release")
124
+ // to your build.gradle file
125
+
126
+ `,
127
+ `package ${this.packagePrefix}skirout.`,
128
+ this.modulePath.replace(/\.skir$/, "").replace("/", "."),
129
+ ";\n\n",
130
+ );
131
+
132
+ const { target } = this;
133
+ switch (target.kind) {
134
+ case "record": {
135
+ this.writeClassForRecord(target, "top-level");
136
+ break;
137
+ }
138
+ case "methods": {
139
+ this.writeClassForMethods(target.methods);
140
+ break;
141
+ }
142
+ case "constants": {
143
+ this.writeClassForConstants(target.constants);
144
+ break;
145
+ }
146
+ default: {
147
+ const _: never = target;
148
+ }
149
+ }
150
+
151
+ return this.joinLinesAndFixFormatting();
152
+ }
153
+
154
+ private writeClassForRecord(
155
+ record: Record,
156
+ nested: "nested" | "top-level",
157
+ ): void {
158
+ if (record.recordType === "struct") {
159
+ this.writeClassForStruct(record, nested);
160
+ } else {
161
+ this.writeClassForEnum(record, nested);
162
+ }
163
+ }
164
+
165
+ private writeClassForStruct(
166
+ record: Record,
167
+ nested: "nested" | "top-level",
168
+ ): void {
169
+ const { namer, recordMap, typeSpeller } = this;
170
+ const recordLocation = recordMap.get(record.key)!;
171
+ const className = this.namer.getClassName(recordLocation).name;
172
+ const fields = [...record.fields];
173
+ fields.sort((a, b) => a.name.text.localeCompare(b.name.text));
174
+ this.push(
175
+ "public ",
176
+ nested === "nested" ? "static " : "",
177
+ `final class ${className} {\n`,
178
+ );
179
+
180
+ // Declare fields
181
+ for (const field of fields) {
182
+ const fieldName = namer.structFieldToJavaName(field);
183
+ const type = typeSpeller.getJavaType(field.type!, "frozen");
184
+ this.push(`private final ${type} ${fieldName};\n`);
185
+ }
186
+ const unrecognizedFieldsType = `build.skir.internal.UnrecognizedFields<${className}>`;
187
+ this.push(`private final ${unrecognizedFieldsType} _u;\n\n`);
188
+
189
+ // Constructor
190
+ this.push(`private ${className}(\n`);
191
+ for (const field of fields) {
192
+ const fieldName = namer.structFieldToJavaName(field);
193
+ const type = typeSpeller.getJavaType(field.type!, "frozen");
194
+ this.push(`${type} ${fieldName},\n`);
195
+ }
196
+ this.push(`${unrecognizedFieldsType} _u\n`, ") {\n");
197
+ for (const field of fields) {
198
+ const fieldName = namer.structFieldToJavaName(field);
199
+ this.push(`this.${fieldName} = ${fieldName};\n`);
200
+ }
201
+ this.push("this._u = _u;\n", "}\n\n");
202
+
203
+ // DEFAULT instance
204
+ this.push(`private ${className}() {\n`);
205
+ for (const field of fields) {
206
+ const fieldName = namer.structFieldToJavaName(field);
207
+ if (field.isRecursive === "hard") {
208
+ this.push(`this.${fieldName} = null;\n`);
209
+ } else {
210
+ const defaultExpr = this.getDefaultExpression(field.type!);
211
+ this.push(`this.${fieldName} = ${defaultExpr};\n`);
212
+ }
213
+ }
214
+ this.push(
215
+ "this._u = null;\n",
216
+ "}\n\n",
217
+ `public static final ${className} DEFAULT = new ${className}();\n\n`,
218
+ );
219
+
220
+ // Getters
221
+ for (const field of fields) {
222
+ const fieldName = namer.structFieldToJavaName(field);
223
+ const type = typeSpeller.getJavaType(field.type!, "frozen");
224
+ this.push(`public ${type} ${fieldName}() {\n`);
225
+ if (field.isRecursive === "hard") {
226
+ const defaultExpr = this.getDefaultExpression(field.type!);
227
+ this.push(
228
+ `if (this.${fieldName} != null) {\n`,
229
+ `return this.${fieldName};\n`,
230
+ `} else {\n`,
231
+ `return ${defaultExpr};\n`,
232
+ "}\n",
233
+ );
234
+ } else {
235
+ this.push(`return this.${fieldName};\n`);
236
+ }
237
+ this.push("}\n\n");
238
+ }
239
+
240
+ // toBuilder()
241
+ this.push(`public Builder toBuilder() {\n`);
242
+ this.push(`return new Builder(\n`);
243
+ for (const field of fields) {
244
+ const fieldName = namer.structFieldToJavaName(field);
245
+ this.push(`this.${fieldName},\n`);
246
+ }
247
+ this.push("this._u);\n", "}\n\n");
248
+
249
+ // equals()
250
+ this.push(
251
+ "@java.lang.Override\n",
252
+ "public boolean equals(Object other) {\n",
253
+ "if (this == other) return true;\n",
254
+ `if (!(other instanceof ${className})) return false;\n`,
255
+ `return java.util.Arrays.equals(_equalsProxy(), ((${className}) other)._equalsProxy());\n`,
256
+ "}\n\n",
257
+ );
258
+
259
+ // hashCode()
260
+ this.push(
261
+ "@java.lang.Override\n",
262
+ "public int hashCode() {\n",
263
+ "return java.util.Arrays.hashCode(_equalsProxy());\n",
264
+ "}\n\n",
265
+ );
266
+
267
+ // toString()
268
+ this.push(
269
+ "@java.lang.Override\n",
270
+ "public java.lang.String toString() {\n",
271
+ `return SERIALIZER.toJsonCode(this, build.skir.JsonFlavor.READABLE);\n`,
272
+ "}\n\n",
273
+ );
274
+
275
+ // _equalsProxy()
276
+ this.push(
277
+ "private Object[] _equalsProxy() {\n",
278
+ "return new Object[] {\n",
279
+ fields
280
+ .map((field) => "this." + namer.structFieldToJavaName(field))
281
+ .join(",\n"),
282
+ "\n};\n",
283
+ "}\n\n",
284
+ );
285
+
286
+ // builder()
287
+ {
288
+ const firstField = fields[0];
289
+ const retType = firstField
290
+ ? "Builder_At" + convertCase(firstField.name.text, "UpperCamel")
291
+ : "Builder_Done";
292
+ this.push(
293
+ `public static ${retType} builder() {\n`,
294
+ "return new Builder();\n",
295
+ "}\n\n",
296
+ );
297
+ }
298
+
299
+ // partialBuilder()
300
+ this.push(
301
+ "public static Builder partialBuilder() {\n",
302
+ "return new Builder();\n",
303
+ "}\n\n",
304
+ );
305
+
306
+ // Builder_At? interfaces
307
+ for (const [index, field] of fields.entries()) {
308
+ const fieldName = namer.structFieldToJavaName(field);
309
+ const nextField = index < fields.length - 1 ? fields[index + 1] : null;
310
+ const upperCamelName = convertCase(field.name.text, "UpperCamel");
311
+ const retType = nextField
312
+ ? "Builder_At" + convertCase(nextField.name.text, "UpperCamel")
313
+ : "Builder_Done";
314
+ const paramType = typeSpeller.getJavaType(field.type!, "initializer");
315
+ this.push(
316
+ `public interface Builder_At${upperCamelName} {\n`,
317
+ `${retType} set${upperCamelName}(${paramType} ${fieldName});\n`,
318
+ "}\n\n",
319
+ );
320
+ }
321
+ this.push(
322
+ `public interface Builder_Done {\n`,
323
+ `${className} build();\n`,
324
+ "}\n\n",
325
+ );
326
+
327
+ // Builder class
328
+ this.push("public static final class Builder implements ");
329
+ for (const field of fields) {
330
+ const upperCamelName = convertCase(field.name.text, "UpperCamel");
331
+ this.push(`Builder_At${upperCamelName}, `);
332
+ }
333
+ this.push("Builder_Done {\n");
334
+
335
+ // Builder fields
336
+ for (const field of fields) {
337
+ const fieldName = namer.structFieldToJavaName(field);
338
+ const type = typeSpeller.getJavaType(field.type!, "frozen");
339
+ this.push(`private ${type} ${fieldName};\n`);
340
+ }
341
+ this.push(`private ${unrecognizedFieldsType} _u;\n\n`);
342
+
343
+ // Builder constructors
344
+ this.push("private Builder(\n");
345
+ for (const field of fields) {
346
+ const fieldName = namer.structFieldToJavaName(field);
347
+ const type = typeSpeller.getJavaType(field.type!, "frozen");
348
+ this.push(`${type} ${fieldName},\n`);
349
+ }
350
+ this.push(`${unrecognizedFieldsType} _u\n`, ") {\n");
351
+ for (const field of fields) {
352
+ const fieldName = namer.structFieldToJavaName(field);
353
+ this.push(`this.${fieldName} = ${fieldName};\n`);
354
+ }
355
+ this.push("this._u = _u;\n", "}\n\n");
356
+
357
+ this.push("private Builder() {\n");
358
+ for (const field of fields) {
359
+ const fieldName = namer.structFieldToJavaName(field);
360
+ const defaultExpr = this.getDefaultExpression(field.type!);
361
+ this.push(`this.${fieldName} = ${defaultExpr};\n`);
362
+ }
363
+ this.push("this._u = null;\n", "}\n\n");
364
+
365
+ // Setters
366
+ for (const field of fields) {
367
+ const fieldName = namer.structFieldToJavaName(field);
368
+ const upperCamelName = convertCase(field.name.text, "UpperCamel");
369
+ const type = field.type!;
370
+ const javaType = typeSpeller.getJavaType(type, "initializer");
371
+ this.push(
372
+ "@java.lang.Override\n",
373
+ `public Builder set${upperCamelName}(${javaType} ${fieldName}) {\n`,
374
+ );
375
+ const toFrozenExpr = this.toFrozenExpression(
376
+ fieldName,
377
+ type,
378
+ "can-be-null",
379
+ "_e",
380
+ );
381
+ this.push(
382
+ `this.${fieldName} = ${toFrozenExpr};\n`,
383
+ "return this;\n",
384
+ "}\n\n",
385
+ );
386
+ const isStruct =
387
+ type.kind === "record" &&
388
+ recordMap.get(type.key)!.record.recordType === "struct";
389
+ if (isStruct) {
390
+ const updaterType = `java.util.function.Function<? super ${javaType}, ? extends ${javaType}>`;
391
+ this.push(
392
+ `public Builder update${upperCamelName}(${updaterType} updater) {\n`,
393
+ `return set${upperCamelName}(updater.apply(this.${fieldName}));\n`,
394
+ "}\n\n",
395
+ );
396
+ }
397
+ }
398
+ this.push(
399
+ "@java.lang.Override\n",
400
+ `public ${className} build() {\n`,
401
+ `return new ${className}(\n`,
402
+ );
403
+ for (const field of fields) {
404
+ const fieldName = namer.structFieldToJavaName(field);
405
+ this.push(`this.${fieldName},\n`);
406
+ }
407
+ this.push("this._u);\n", "}\n\n");
408
+ this.push("}\n\n");
409
+
410
+ // _serializerImpl
411
+ {
412
+ const serializerType = `build.skir.internal.StructSerializer<${className}, ${className}.Builder>`;
413
+ this.push(
414
+ `private static final ${serializerType} _serializerImpl = (\n`,
415
+ "new build.skir.internal.StructSerializer<>(\n",
416
+ `"${getRecordId(recordLocation)}",\n`,
417
+ `${toJavaStringLiteral(docToCommentText(record.doc))},\n`,
418
+ "DEFAULT,\n",
419
+ `(${className} it) -> it != null ? it.toBuilder() : partialBuilder(),\n`,
420
+ `(${className}.Builder it) -> it.build(),\n`,
421
+ `(${className} it) -> it._u,\n`,
422
+ `(${className}.Builder builder, ${unrecognizedFieldsType} u) -> {\n`,
423
+ `builder._u = u;\n`,
424
+ "return null;\n",
425
+ "}\n",
426
+ ")\n",
427
+ ");\n\n",
428
+ );
429
+ }
430
+
431
+ // SERIALIZER
432
+ this.push(
433
+ `public static final build.skir.Serializer<${className}> SERIALIZER = (\n`,
434
+ "build.skir.internal.SerializersKt.makeSerializer(_serializerImpl)\n",
435
+ ");\n\n",
436
+ );
437
+
438
+ // TYPE_DESCRIPTOR
439
+ {
440
+ const typeDescriptorType = `build.skir.reflection.StructDescriptor.Reflective<${className}, ${className}.Builder>`;
441
+ this.push(
442
+ `public static final ${typeDescriptorType} TYPE_DESCRIPTOR = (\n`,
443
+ "_serializerImpl\n",
444
+ ");\n\n",
445
+ );
446
+ }
447
+
448
+ // Finalize serializer
449
+ this.push("static {\n");
450
+ for (const field of fields) {
451
+ const skirName = field.name.text;
452
+ const javadName = namer.structFieldToJavaName(field);
453
+ this.push(
454
+ "_serializerImpl.addField(\n",
455
+ `"${skirName}",\n`,
456
+ '"",\n',
457
+ `${field.number},\n`,
458
+ `${typeSpeller.getSerializerExpression(field.type!)},\n`,
459
+ `${toJavaStringLiteral(docToCommentText(field.doc))},\n`,
460
+ `(it) -> it.${javadName}(),\n`,
461
+ "(builder, v) -> {\n",
462
+ `builder.${javadName} = v;\n`,
463
+ "return null;\n",
464
+ "}\n",
465
+ ");\n",
466
+ );
467
+ }
468
+ for (const removedNumber of record.removedNumbers) {
469
+ this.push(`_serializerImpl.addRemovedNumber(${removedNumber});\n`);
470
+ }
471
+ this.push("_serializerImpl.finalizeStruct();\n", "}\n\n");
472
+
473
+ // Nested classes
474
+ this.writeClassesForNestedRecords(record);
475
+ this.push("}\n\n");
476
+ }
477
+
478
+ private writeClassForEnum(
479
+ record: Record,
480
+ nested: "nested" | "top-level",
481
+ ): void {
482
+ const { recordMap, typeSpeller } = this;
483
+ const recordLocation = recordMap.get(record.key)!;
484
+ const className = this.namer.getClassName(recordLocation).name;
485
+ const { fields } = record;
486
+ const constantFields = fields.filter((f) => !f.type);
487
+ const wrapperFields = fields.filter((f) => f.type);
488
+ this.push(
489
+ "public ",
490
+ nested === "nested" ? "static " : "",
491
+ `final class ${className} {\n`,
492
+ );
493
+ // Kind enum
494
+ this.push("public enum Kind {\n", "UNKNOWN,\n");
495
+ for (const field of constantFields) {
496
+ this.push(field.name.text, "_CONST,\n");
497
+ }
498
+ for (const field of wrapperFields) {
499
+ this.push(
500
+ convertCase(field.name.text, "UPPER_UNDERSCORE"),
501
+ "_WRAPPER,\n",
502
+ );
503
+ }
504
+ this.push("}\n\n");
505
+
506
+ // Constants
507
+ this.push(
508
+ `public static final ${className} UNKNOWN = new ${className}(Kind.UNKNOWN, null);\n`,
509
+ );
510
+ for (const field of constantFields) {
511
+ const skirName = field.name.text;
512
+ const name = toEnumConstantName(field);
513
+ this.push(
514
+ `public static final ${className} ${name} = new ${className}(Kind.${skirName}_CONST, null);\n`,
515
+ );
516
+ }
517
+ this.pushEol();
518
+
519
+ // WrapX methods
520
+ for (const field of wrapperFields) {
521
+ const upperCamelName = convertCase(field.name.text, "UpperCamel");
522
+ const upperUnderscoreName = convertCase(
523
+ field.name.text,
524
+ "UPPER_UNDERSCORE",
525
+ );
526
+ const type = field.type!;
527
+ const initializerType = typeSpeller.getJavaType(type, "initializer");
528
+ const frozenType = typeSpeller.getJavaType(type, "frozen");
529
+ const toFrozenExpr = this.toFrozenExpression(
530
+ "value",
531
+ type,
532
+ "can-be-null",
533
+ "_e",
534
+ );
535
+ this.push(
536
+ `public static ${className} wrap${upperCamelName}(${initializerType} value) {\n`,
537
+ `final ${frozenType} v = ${toFrozenExpr};\n`,
538
+ `return new ${className}(Kind.${upperUnderscoreName}_WRAPPER, v);\n`,
539
+ "}\n\n",
540
+ );
541
+ }
542
+
543
+ // Declare fields
544
+ this.push(
545
+ "private final Kind kind;\n",
546
+ "private final java.lang.Object value;\n\n",
547
+ );
548
+
549
+ // Constructor
550
+ this.push(
551
+ `private ${className}(Kind kind, java.lang.Object value) {\n`,
552
+ "this.kind = kind;\n",
553
+ "this.value = value;\n",
554
+ "}\n\n",
555
+ );
556
+
557
+ // kind()
558
+ this.push("public Kind kind() {\n", "return kind;\n", "}\n\n");
559
+
560
+ // asX() methods
561
+ for (const field of wrapperFields) {
562
+ const type = typeSpeller.getJavaType(field.type!, "frozen");
563
+ const upperCamelName = convertCase(field.name.text, "UpperCamel");
564
+ const upperUnderscoreName = convertCase(
565
+ field.name.text,
566
+ "UPPER_UNDERSCORE",
567
+ );
568
+ this.push(
569
+ `public ${type} as${upperCamelName}() {\n`,
570
+ `if (kind != Kind.${upperUnderscoreName}_WRAPPER) {\n`,
571
+ `throw new java.lang.IllegalStateException("kind=" + kind.name());\n`,
572
+ "}\n",
573
+ `return (${type}) value;\n`,
574
+ "}\n\n",
575
+ );
576
+ }
577
+
578
+ // Visitor
579
+ this.push("public interface Visitor<R> {\n", "R onUnknown();\n");
580
+ for (const field of constantFields) {
581
+ const upperCamelName = convertCase(field.name.text, "UpperCamel");
582
+ this.push(`R on${upperCamelName}();\n`);
583
+ }
584
+ for (const field of wrapperFields) {
585
+ const upperCamelName = convertCase(field.name.text, "UpperCamel");
586
+ const type = typeSpeller.getJavaType(field.type!, "frozen");
587
+ this.push(`R on${upperCamelName}(${type} value);\n`);
588
+ }
589
+ this.push("}\n\n");
590
+
591
+ // accept()
592
+ this.push(
593
+ "public <R> R accept(Visitor<R> visitor) {\n",
594
+ "return switch (kind) {\n",
595
+ );
596
+ for (const field of constantFields) {
597
+ const upperUnderscoreName = field.name.text;
598
+ const upperCamelName = convertCase(field.name.text, "UpperCamel");
599
+ this.push(
600
+ `case ${upperUnderscoreName}_CONST -> visitor.on${upperCamelName}();\n`,
601
+ );
602
+ }
603
+ for (const field of wrapperFields) {
604
+ const upperUnderscoreName = convertCase(
605
+ field.name.text,
606
+ "UPPER_UNDERSCORE",
607
+ );
608
+ const upperCamelName = convertCase(field.name.text, "UpperCamel");
609
+ const type = typeSpeller.getJavaType(field.type!, "frozen");
610
+ this.push(
611
+ `case ${upperUnderscoreName}_WRAPPER -> visitor.on${upperCamelName}((${type}) value);\n`,
612
+ );
613
+ }
614
+ this.push("default -> visitor.onUnknown();\n", "};\n", "}\n\n");
615
+
616
+ // equals()
617
+ this.push(
618
+ "@java.lang.Override\n",
619
+ "public boolean equals(Object other) {\n",
620
+ `if (!(other instanceof ${className})) return false;\n`,
621
+ `final ${className} otherEnum = (${className}) other;\n`,
622
+ "if (kind == Kind.UNKNOWN) return otherEnum.kind == Kind.UNKNOWN;\n",
623
+ "return kind == otherEnum.kind && java.util.Objects.equals(value, otherEnum.value);\n",
624
+ "}\n\n",
625
+ );
626
+
627
+ // hashCode()
628
+ this.push(
629
+ "@java.lang.Override\n",
630
+ "public int hashCode() {\n",
631
+ "final Object v = kind == Kind.UNKNOWN ? null : value;\n",
632
+ "return 31 * java.util.Objects.hashCode(v) + kind.ordinal();\n",
633
+ "}\n\n",
634
+ );
635
+
636
+ // toString()
637
+ this.push(
638
+ "@java.lang.Override\n",
639
+ "public java.lang.String toString() {\n",
640
+ `return SERIALIZER.toJsonCode(this, build.skir.JsonFlavor.READABLE);\n`,
641
+ "}\n\n",
642
+ );
643
+
644
+ // _serializerImpl
645
+ {
646
+ const serializerType = `build.skir.internal.EnumSerializer<${className}>`;
647
+ const unrecognizedVariantType = `build.skir.internal.UnrecognizedVariant<${className}>`;
648
+ this.push(
649
+ `private static final ${serializerType} _serializerImpl = (\n`,
650
+ "build.skir.internal.EnumSerializer.Companion.create(\n",
651
+ `"${getRecordId(recordLocation)}",\n`,
652
+ `${toJavaStringLiteral(docToCommentText(record.doc))},\n`,
653
+ `(${className} it) -> it.kind().ordinal(),\n`,
654
+ "Kind.values().length,\n",
655
+ "UNKNOWN,\n",
656
+ `(${unrecognizedVariantType} it) -> new ${className}(Kind.UNKNOWN, it),\n`,
657
+ `(${className} it) -> (${unrecognizedVariantType}) it.value\n`,
658
+ ")\n",
659
+ ");\n\n",
660
+ );
661
+ }
662
+
663
+ // SERIALIZER
664
+ this.push(
665
+ `public static final build.skir.Serializer<${className}> SERIALIZER = (\n`,
666
+ "build.skir.internal.SerializersKt.makeSerializer(_serializerImpl)\n",
667
+ ");\n\n",
668
+ );
669
+
670
+ // TYPE_DESCRIPTOR
671
+ {
672
+ const typeDescriptorType = `build.skir.reflection.EnumDescriptor.Reflective<${className}>`;
673
+ this.push(
674
+ `public static final ${typeDescriptorType} TYPE_DESCRIPTOR = (\n`,
675
+ "_serializerImpl\n",
676
+ ");\n\n",
677
+ );
678
+ }
679
+
680
+ // Finalize serializer
681
+ this.push("static {\n");
682
+ for (const field of constantFields) {
683
+ const name = field.name.text;
684
+ this.push(
685
+ "_serializerImpl.addConstantVariant(\n",
686
+ `${field.number},\n`,
687
+ `"${name}",\n`,
688
+ `Kind.${name}_CONST.ordinal(),\n`,
689
+ `${toJavaStringLiteral(docToCommentText(field.doc))},\n`,
690
+ `${toEnumConstantName(field)}\n`,
691
+ ");\n",
692
+ );
693
+ }
694
+ for (const field of wrapperFields) {
695
+ const type = field.type!;
696
+ const javaType = typeSpeller.getJavaType(
697
+ type,
698
+ "frozen",
699
+ "must-be-object",
700
+ );
701
+ const serializerExpression = typeSpeller.getSerializerExpression(type);
702
+ const skirName = field.name.text;
703
+ const upperCamelName = convertCase(skirName, "UpperCamel");
704
+ const kindConstName =
705
+ convertCase(skirName, "UPPER_UNDERSCORE") + "_WRAPPER";
706
+ this.push(
707
+ "_serializerImpl.addWrapperVariant(\n",
708
+ `${field.number},\n`,
709
+ `"${field.name.text}",\n`,
710
+ `Kind.${kindConstName}.ordinal(),\n`,
711
+ `${serializerExpression},\n`,
712
+ `${toJavaStringLiteral(docToCommentText(field.doc))},\n`,
713
+ `(${javaType} it) -> wrap${upperCamelName}(it),\n`,
714
+ `(${className} it) -> it.as${upperCamelName}()\n`,
715
+ ");\n",
716
+ );
717
+ }
718
+ for (const removedNumber of record.removedNumbers) {
719
+ this.push(`_serializerImpl.addRemovedNumber(${removedNumber});\n`);
720
+ }
721
+ this.push("_serializerImpl.finalizeEnum();\n", "}\n\n");
722
+
723
+ // Nested classes
724
+ this.writeClassesForNestedRecords(record);
725
+ this.push("}\n\n");
726
+ }
727
+
728
+ private writeClassesForNestedRecords(record: Record): void {
729
+ for (const nestedRecord of record.nestedRecords) {
730
+ this.writeClassForRecord(nestedRecord, "nested");
731
+ }
732
+ }
733
+
734
+ get path(): string {
735
+ const { target, modulePath } = this;
736
+ let className: string;
737
+ switch (target.kind) {
738
+ case "record": {
739
+ const record = this.recordMap.get(target.key)!;
740
+ className = this.namer.getClassName(record).name;
741
+ break;
742
+ }
743
+ case "methods": {
744
+ className = "Methods";
745
+ break;
746
+ }
747
+ case "constants": {
748
+ className = "Constants";
749
+ break;
750
+ }
751
+ }
752
+ return modulePath.replace(/\.skir$/, "") + `/${className}.java`;
753
+ }
754
+
755
+ private writeClassForMethods(methods: readonly Method[]): void {
756
+ this.push("public final class Methods {\n\n", "private Methods() {}\n\n");
757
+ for (const method of methods) {
758
+ this.writeMethod(method);
759
+ }
760
+ this.push("}\n\n");
761
+ }
762
+
763
+ private writeMethod(method: Method): void {
764
+ const { typeSpeller } = this;
765
+ const requestType = typeSpeller.getJavaType(method.requestType!, "frozen");
766
+ const requestSerializer = typeSpeller.getSerializerExpression(
767
+ method.requestType!,
768
+ );
769
+ const responseType = typeSpeller.getJavaType(
770
+ method.responseType!,
771
+ "frozen",
772
+ );
773
+ const responseSerializer = typeSpeller.getSerializerExpression(
774
+ method.responseType!,
775
+ );
776
+
777
+ const skirName = method.name.text;
778
+ const javaName = convertCase(skirName, "UPPER_UNDERSCORE");
779
+
780
+ const methodType = `build.skir.service.Method<${requestType}, ${responseType}>`;
781
+ this.push(
782
+ `public static final ${methodType} ${javaName} = (\n`,
783
+ "new build.skir.service.Method<>(\n",
784
+ `"${skirName}",\n`,
785
+ `${method.number}L,\n`,
786
+ `${requestSerializer},\n`,
787
+ `${responseSerializer},\n`,
788
+ `${toJavaStringLiteral(docToCommentText(method.doc))}\n`,
789
+ ")\n",
790
+ ");\n\n",
791
+ );
792
+ }
793
+
794
+ private writeClassForConstants(constants: readonly Constant[]): void {
795
+ this.push(
796
+ "public final class Constants {\n\n",
797
+ "private Constants() {}\n\n",
798
+ );
799
+ for (const constant of constants) {
800
+ this.writeConstant(constant);
801
+ }
802
+ this.push("}\n\n");
803
+ }
804
+
805
+ private writeConstant(constant: Constant): void {
806
+ const { typeSpeller } = this;
807
+ const javaType = typeSpeller.getJavaType(constant.type!, "frozen");
808
+ const name = constant.name.text;
809
+
810
+ const serializerExpression = typeSpeller.getSerializerExpression(
811
+ constant.type!,
812
+ );
813
+ const jsonStringLiteral = JSON.stringify(
814
+ JSON.stringify(constant.valueAsDenseJson),
815
+ );
816
+ this.push(
817
+ `public static final ${javaType} ${name} = (\n`,
818
+ `${serializerExpression}.fromJsonCode(${jsonStringLiteral})\n`,
819
+ ");\n\n",
820
+ );
821
+ }
822
+
823
+ private getDefaultExpression(type: ResolvedType): string {
824
+ switch (type.kind) {
825
+ case "primitive": {
826
+ switch (type.primitive) {
827
+ case "bool":
828
+ return "false";
829
+ case "int32":
830
+ case "int64":
831
+ case "uint64":
832
+ return "0";
833
+ case "float32":
834
+ return "0.0f";
835
+ case "float64":
836
+ return "0.0";
837
+ case "timestamp":
838
+ return "java.time.Instant.EPOCH";
839
+ case "string":
840
+ return '""';
841
+ case "bytes":
842
+ return "okio.ByteString.EMPTY";
843
+ default: {
844
+ const _: never = type.primitive;
845
+ throw Error();
846
+ }
847
+ }
848
+ }
849
+ case "array": {
850
+ if (type.key) {
851
+ return `build.skir.internal.FrozenListKt.emptyKeyedList()`;
852
+ } else {
853
+ return `build.skir.internal.FrozenListKt.emptyFrozenList()`;
854
+ }
855
+ }
856
+ case "optional": {
857
+ return "java.util.Optional.empty()";
858
+ }
859
+ case "record": {
860
+ const record = this.typeSpeller.recordMap.get(type.key)!;
861
+ const kotlinType = this.typeSpeller.getJavaType(type, "frozen");
862
+ switch (record.record.recordType) {
863
+ case "struct": {
864
+ return `${kotlinType}.DEFAULT`;
865
+ }
866
+ case "enum": {
867
+ return `${kotlinType}.UNKNOWN`;
868
+ }
869
+ }
870
+ break;
871
+ }
872
+ }
873
+ }
874
+
875
+ private toFrozenExpression(
876
+ inputExpr: string,
877
+ type: ResolvedType,
878
+ nullability: "can-be-null" | "never-null",
879
+ it: string,
880
+ ): string {
881
+ const { namer } = this;
882
+ switch (type.kind) {
883
+ case "primitive": {
884
+ switch (type.primitive) {
885
+ case "bool":
886
+ case "int32":
887
+ case "int64":
888
+ case "uint64":
889
+ case "float32":
890
+ case "float64":
891
+ return inputExpr;
892
+ case "timestamp":
893
+ case "string":
894
+ case "bytes":
895
+ return nullability === "can-be-null"
896
+ ? `java.util.Objects.requireNonNull(${inputExpr})`
897
+ : inputExpr;
898
+ default: {
899
+ const _: never = type.primitive;
900
+ throw Error();
901
+ }
902
+ }
903
+ }
904
+ case "array": {
905
+ const itemToFrozenExpr = this.toFrozenExpression(
906
+ it,
907
+ type.item,
908
+ "can-be-null",
909
+ it + "_",
910
+ );
911
+ const frozenListKt = "build.skir.internal.FrozenListKt";
912
+ if (type.key) {
913
+ const path = type.key.path
914
+ .map((f) => namer.structFieldToJavaName(f.name.text) + "()")
915
+ .join(".");
916
+ if (itemToFrozenExpr === it) {
917
+ return `${frozenListKt}.toKeyedList(\n${inputExpr},\n"${path}",\n(${it}) -> ${it}.${path}\n)`;
918
+ } else {
919
+ return `${frozenListKt}.toKeyedList(\n${inputExpr},\n"${path}",\n(${it}) -> ${it}.${path},\n(${it}) -> ${itemToFrozenExpr}\n)`;
920
+ }
921
+ } else {
922
+ if (itemToFrozenExpr === it) {
923
+ return `${frozenListKt}.toFrozenList(${inputExpr})`;
924
+ } else {
925
+ return `${frozenListKt}.toFrozenList(\n${inputExpr},\n(${it}) -> ${itemToFrozenExpr}\n)`;
926
+ }
927
+ }
928
+ }
929
+ case "optional": {
930
+ const otherExpr = this.toFrozenExpression(
931
+ it,
932
+ type.other,
933
+ "never-null",
934
+ it + "_",
935
+ );
936
+ return `${inputExpr}.map(\n(${it}) -> ${otherExpr}\n)`;
937
+ }
938
+ case "record": {
939
+ return nullability === "can-be-null"
940
+ ? `java.util.Objects.requireNonNull(${inputExpr})`
941
+ : inputExpr;
942
+ }
943
+ }
944
+ }
945
+
946
+ private push(...code: string[]): void {
947
+ this.code += code.join("");
948
+ }
949
+
950
+ private pushEol(): void {
951
+ this.code += "\n";
952
+ }
953
+
954
+ private joinLinesAndFixFormatting(): string {
955
+ const indentUnit = " ";
956
+ let result = "";
957
+ // The indent at every line is obtained by repeating indentUnit N times,
958
+ // where N is the length of this array.
959
+ const contextStack: Array<"{" | "(" | "[" | "<" | ":" | "."> = [];
960
+ // Returns the last element in `contextStack`.
961
+ const peakTop = (): string => contextStack.at(-1)!;
962
+ const getMatchingLeftBracket = (r: "}" | ")" | "]" | ">"): string => {
963
+ switch (r) {
964
+ case "}":
965
+ return "{";
966
+ case ")":
967
+ return "(";
968
+ case "]":
969
+ return "[";
970
+ case ">":
971
+ return "<";
972
+ }
973
+ };
974
+ for (let line of this.code.split("\n")) {
975
+ line = line.trim();
976
+ if (line.length <= 0) {
977
+ // Don't indent empty lines.
978
+ result += "\n";
979
+ continue;
980
+ }
981
+
982
+ const firstChar = line[0];
983
+ switch (firstChar) {
984
+ case "}":
985
+ case ")":
986
+ case "]":
987
+ case ">": {
988
+ const left = getMatchingLeftBracket(firstChar);
989
+ while (contextStack.pop() !== left) {
990
+ if (contextStack.length <= 0) {
991
+ throw Error();
992
+ }
993
+ }
994
+ break;
995
+ }
996
+ case ".": {
997
+ if (peakTop() !== ".") {
998
+ contextStack.push(".");
999
+ }
1000
+ break;
1001
+ }
1002
+ }
1003
+ const indent = indentUnit.repeat(contextStack.length);
1004
+ result += `${indent}${line.trimEnd()}\n`;
1005
+ if (line.startsWith("//")) {
1006
+ continue;
1007
+ }
1008
+ const lastChar = line.slice(-1);
1009
+ switch (lastChar) {
1010
+ case "{":
1011
+ case "(":
1012
+ case "[":
1013
+ case "<": {
1014
+ // The next line will be indented
1015
+ contextStack.push(lastChar);
1016
+ break;
1017
+ }
1018
+ case ":":
1019
+ case "=": {
1020
+ if (peakTop() !== ":") {
1021
+ contextStack.push(":");
1022
+ }
1023
+ break;
1024
+ }
1025
+ case ";":
1026
+ case ",": {
1027
+ if (peakTop() === "." || peakTop() === ":") {
1028
+ contextStack.pop();
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ return (
1035
+ result
1036
+ // Remove spaces enclosed within curly brackets if that's all there is.
1037
+ .replace(/\{\s+\}/g, "{}")
1038
+ // Remove spaces enclosed within round brackets if that's all there is.
1039
+ .replace(/\(\s+\)/g, "()")
1040
+ // Remove spaces enclosed within square brackets if that's all there is.
1041
+ .replace(/\[\s+\]/g, "[]")
1042
+ // Remove empty line following an open curly bracket.
1043
+ .replace(/(\{\n *)\n/g, "$1")
1044
+ // Remove empty line preceding a closed curly bracket.
1045
+ .replace(/\n(\n *\})/g, "$1")
1046
+ // Coalesce consecutive empty lines.
1047
+ .replace(/\n\n\n+/g, "\n\n")
1048
+ .replace(/\n\n$/g, "\n")
1049
+ );
1050
+ }
1051
+
1052
+ private readonly typeSpeller: TypeSpeller;
1053
+ private readonly packagePrefix: string;
1054
+ private readonly namer: Namer;
1055
+ private code = "";
1056
+ }
1057
+
1058
+ function getRecordId(struct: RecordLocation): string {
1059
+ const modulePath = struct.modulePath;
1060
+ const qualifiedRecordName = struct.recordAncestors
1061
+ .map((r) => r.name.text)
1062
+ .join(".");
1063
+ return `${modulePath}:${qualifiedRecordName}`;
1064
+ }
1065
+
1066
+ function toJavaStringLiteral(input: string): string {
1067
+ const escaped = input.replace(/[\\"\x00-\x1f\x7f-\xff]/g, (char) => {
1068
+ switch (char) {
1069
+ case '"':
1070
+ return '\\"';
1071
+ case "\\":
1072
+ return "\\\\";
1073
+ case "\b":
1074
+ return "\\b";
1075
+ case "\f":
1076
+ return "\\f";
1077
+ case "\n":
1078
+ return "\\n";
1079
+ case "\r":
1080
+ return "\\r";
1081
+ case "\t":
1082
+ return "\\t";
1083
+ default: {
1084
+ // Handle non-printable characters using Unicode escapes (\\uXXXX)
1085
+ const code = char.charCodeAt(0).toString(16).padStart(4, "0");
1086
+ return `\\u${code}`;
1087
+ }
1088
+ }
1089
+ });
1090
+ return `"${escaped}"`;
1091
+ }
1092
+
1093
+ function commentify(textOrLines: string | readonly string[]): string {
1094
+ const text = (
1095
+ typeof textOrLines === "string" ? textOrLines : textOrLines.join("\n")
1096
+ )
1097
+ .trim()
1098
+ .replace(/\n{3,}/g, "\n\n")
1099
+ .replace("*/", "* /");
1100
+ if (text.length <= 0) {
1101
+ return "";
1102
+ }
1103
+ const lines = text.split("\n");
1104
+ if (lines.length === 1) {
1105
+ return `/** ${text} */\n`;
1106
+ } else {
1107
+ return ["/**\n", ...lines.map((line) => ` * ${line}\n`), " */\n"].join("");
1108
+ }
1109
+ }
1110
+
1111
+ function docToCommentText(doc: Doc): string {
1112
+ return doc.pieces
1113
+ .map((p) => {
1114
+ switch (p.kind) {
1115
+ case "text":
1116
+ return p.text;
1117
+ case "reference":
1118
+ return "`" + p.referenceRange.text.slice(1, -1) + "`";
1119
+ }
1120
+ })
1121
+ .join("");
1122
+ }
1123
+
1124
+ export const GENERATOR = new JavaCodeGenerator();