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/README.md +424 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +730 -0
- package/dist/index.js.map +1 -0
- package/dist/naming.d.ts +20 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +138 -0
- package/dist/naming.js.map +1 -0
- package/dist/type_speller.d.ts +15 -0
- package/dist/type_speller.d.ts.map +1 -0
- package/dist/type_speller.js +167 -0
- package/dist/type_speller.js.map +1 -0
- package/package.json +50 -0
- package/src/index.ts +1124 -0
- package/src/naming.ts +161 -0
- package/src/type_speller.ts +185 -0
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();
|