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/src/naming.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { type Field, type RecordLocation, convertCase } from "skir-internal";
2
+
3
+ export class Namer {
4
+ private readonly genPackageFirstName: string;
5
+
6
+ constructor(private readonly packagePrefix: string) {
7
+ if (packagePrefix.length <= 0) {
8
+ this.genPackageFirstName = "skirout";
9
+ } else {
10
+ this.genPackageFirstName = packagePrefix.split(".")[0]!;
11
+ }
12
+ }
13
+
14
+ structFieldToKotlinName(field: Field | string): string {
15
+ const skirName = typeof field === "string" ? field : field.name.text;
16
+ const lowerCamel = convertCase(skirName, "lowerCamel");
17
+ const nameConflict =
18
+ KOTLIN_HARD_KEYWORDS.has(lowerCamel) ||
19
+ TOP_LEVEL_PACKAGE_NAMES.has(lowerCamel) ||
20
+ KOTLIN_OBJECT_SYMBOLS.has(lowerCamel) ||
21
+ GENERATED_STRUCT_SYMBOLS.has(lowerCamel) ||
22
+ lowerCamel === this.genPackageFirstName ||
23
+ skirName.startsWith("mutable_");
24
+ return nameConflict ? lowerCamel + "_" : lowerCamel;
25
+ }
26
+
27
+ /** Returns the name of the frozen Kotlin class for the given record. */
28
+ getClassName(record: RecordLocation): ClassName {
29
+ const { recordAncestors } = record;
30
+ const parts: string[] = [];
31
+ for (let i = 0; i < recordAncestors.length; ++i) {
32
+ const record = recordAncestors[i]!;
33
+ let name = record.name.text;
34
+ const parentType = i > 0 ? recordAncestors[i - 1]!.recordType : undefined;
35
+ if (
36
+ (parentType === "struct" && STRUCT_NESTED_TYPE_NAMES.has(name)) ||
37
+ (parentType === "enum" &&
38
+ (ENUM_NESTED_TYPE_NAMES.has(name) || /[a-z]Wrapper$/.test(name)))
39
+ ) {
40
+ name += "_";
41
+ }
42
+ parts.push(name);
43
+ }
44
+
45
+ const name = parts.at(-1)!;
46
+
47
+ const path = record.modulePath;
48
+ const importPath = path.replace(/\.skir$/, "").replace("/", ".");
49
+ const qualifiedName = `${this.packagePrefix}skirout.${importPath}.${parts.join(".")}`;
50
+
51
+ return { name, qualifiedName };
52
+ }
53
+ }
54
+
55
+ const KOTLIN_HARD_KEYWORDS: ReadonlySet<string> = new Set([
56
+ "abstract",
57
+ "annotation",
58
+ "as",
59
+ "break",
60
+ "catch",
61
+ "class",
62
+ "const",
63
+ "continue",
64
+ "crossinline",
65
+ "data",
66
+ "else",
67
+ "enum",
68
+ "external",
69
+ "final",
70
+ "finally",
71
+ "for",
72
+ "fun",
73
+ "if",
74
+ "import",
75
+ "in",
76
+ "inline",
77
+ "interface",
78
+ "internal",
79
+ "is",
80
+ "lateinit",
81
+ "noinline",
82
+ "object",
83
+ "open",
84
+ "operator",
85
+ "override",
86
+ "package",
87
+ "private",
88
+ "protected",
89
+ "public",
90
+ "reified",
91
+ "return",
92
+ "sealed",
93
+ "super",
94
+ "suspend",
95
+ "this",
96
+ "throw",
97
+ "try",
98
+ "typealias",
99
+ "val",
100
+ "var",
101
+ "when",
102
+ "while",
103
+ ]);
104
+
105
+ const TOP_LEVEL_PACKAGE_NAMES: ReadonlySet<string> = new Set<string>([
106
+ "build",
107
+ "java",
108
+ "kotlin",
109
+ "okio",
110
+ ]);
111
+
112
+ const KOTLIN_OBJECT_SYMBOLS: ReadonlySet<string> = new Set([
113
+ "equals",
114
+ "hashCode",
115
+ "toString",
116
+ "javaClass",
117
+ ]);
118
+
119
+ const GENERATED_STRUCT_SYMBOLS: ReadonlySet<string> = new Set([
120
+ "copy",
121
+ "toFrozen",
122
+ "toMutable",
123
+ ]);
124
+
125
+ export function toEnumConstantName(field: Field): string {
126
+ return field.name.text;
127
+ }
128
+
129
+ export interface ClassName {
130
+ /** The name right after the 'class' keyword.. */
131
+ name: string;
132
+ /**
133
+ * Fully qualified class name.
134
+ * Examples: 'skirout.Foo', 'skirout.Foo.Bar'
135
+ */
136
+ qualifiedName: string;
137
+ }
138
+
139
+ /** Generated types nested within a struct class. */
140
+ const STRUCT_NESTED_TYPE_NAMES: ReadonlySet<string> = new Set(["Mutable"]);
141
+
142
+ /** Generated types nested within an enum class. */
143
+ const ENUM_NESTED_TYPE_NAMES: ReadonlySet<string> = new Set([
144
+ "Kind",
145
+ "Unknown",
146
+ ]);
@@ -0,0 +1,225 @@
1
+ import type { RecordKey, RecordLocation, ResolvedType } from "skir-internal";
2
+ import { ClassName, Namer } from "./naming.js";
3
+
4
+ export type TypeFlavor =
5
+ | "initializer"
6
+ | "frozen"
7
+ | "maybe-mutable"
8
+ | "mutable"
9
+ | "kind";
10
+
11
+ /**
12
+ * Transforms a type found in a `.skir` file into a Kotlin type.
13
+ *
14
+ * The flavors are:
15
+ * · initializer
16
+ * The value can be passed by parameter to the constructor of a frozen
17
+ * class.
18
+ * · frozen:
19
+ * The type is deeply immutable. All the fields of a frozen class are also
20
+ * frozen.
21
+ * · maybe-mutable:
22
+ * Type union of the frozen type and the mutable type. All the fields of a
23
+ * mutable class are maybe-mutable.
24
+ * · mutable:
25
+ * A mutable value. Not all types found in `.skir` files support this, e.g.
26
+ * strings and numbers are always immutable.
27
+ */
28
+ export class TypeSpeller {
29
+ constructor(
30
+ readonly recordMap: ReadonlyMap<RecordKey, RecordLocation>,
31
+ private readonly namer: Namer,
32
+ ) {}
33
+
34
+ getKotlinType(
35
+ type: ResolvedType,
36
+ flavor: "initializer" | "frozen" | "mutable",
37
+ allRecordsFrozen?: undefined,
38
+ ): string;
39
+
40
+ getKotlinType(
41
+ type: ResolvedType,
42
+ flavor: TypeFlavor,
43
+ // Only matters if mode is "maybe-mutable"
44
+ allRecordsFrozen: boolean | undefined,
45
+ ): string;
46
+
47
+ getKotlinType(
48
+ type: ResolvedType,
49
+ flavor: TypeFlavor,
50
+ // Only matters if mode is "maybe-mutable"
51
+ allRecordsFrozen: boolean | undefined,
52
+ ): string {
53
+ switch (type.kind) {
54
+ case "record": {
55
+ const recordLocation = this.recordMap.get(type.key)!;
56
+ const record = recordLocation.record;
57
+ const className = this.namer.getClassName(recordLocation).qualifiedName;
58
+ if (record.recordType === "struct") {
59
+ if (flavor === "frozen" || allRecordsFrozen) {
60
+ return className;
61
+ } else if (flavor === "maybe-mutable" || flavor === "initializer") {
62
+ return allRecordsFrozen ? className : `${className}_OrMutable`;
63
+ } else if (flavor === "mutable") {
64
+ return `${className}.Mutable`;
65
+ } else {
66
+ const _: "kind" = flavor;
67
+ throw TypeError();
68
+ }
69
+ }
70
+ // An enum.
71
+ const _: "enum" = record.recordType;
72
+ if (
73
+ flavor === "initializer" ||
74
+ flavor === "frozen" ||
75
+ flavor === "maybe-mutable" ||
76
+ flavor === "mutable"
77
+ ) {
78
+ return className;
79
+ } else if (flavor === "kind") {
80
+ return `${className}.Kind`;
81
+ } else {
82
+ const _: never = flavor;
83
+ throw TypeError();
84
+ }
85
+ }
86
+ case "array": {
87
+ if (flavor === "initializer") {
88
+ const itemType = this.getKotlinType(
89
+ type.item,
90
+ "maybe-mutable",
91
+ allRecordsFrozen,
92
+ );
93
+ return `kotlin.collections.Iterable<${itemType}>`;
94
+ } else if (flavor === "frozen") {
95
+ const itemType = this.getKotlinType(
96
+ type.item,
97
+ "frozen",
98
+ allRecordsFrozen,
99
+ );
100
+ if (type.key) {
101
+ const { keyType } = type.key;
102
+ let kotlinKeyType = this.getKotlinType(keyType, "frozen");
103
+ if (keyType.kind === "record") {
104
+ kotlinKeyType += ".Kind";
105
+ }
106
+ return `build.skir.KeyedList<${itemType}, ${kotlinKeyType}>`;
107
+ } else {
108
+ return `kotlin.collections.List<${itemType}>`;
109
+ }
110
+ } else if (flavor === "maybe-mutable") {
111
+ const itemType = this.getKotlinType(
112
+ type.item,
113
+ "maybe-mutable",
114
+ allRecordsFrozen,
115
+ );
116
+ return `kotlin.collections.List<${itemType}>`;
117
+ } else if (flavor === "mutable") {
118
+ const itemType = this.getKotlinType(
119
+ type.item,
120
+ "maybe-mutable",
121
+ allRecordsFrozen,
122
+ );
123
+ return `kotlin.collections.MutableList<${itemType}>`;
124
+ } else {
125
+ const _: "kind" = flavor;
126
+ throw TypeError();
127
+ }
128
+ }
129
+ case "optional": {
130
+ const otherType = this.getKotlinType(
131
+ type.other,
132
+ flavor,
133
+ allRecordsFrozen,
134
+ );
135
+ return `${otherType}?`;
136
+ }
137
+ case "primitive": {
138
+ const { primitive } = type;
139
+ switch (primitive) {
140
+ case "bool":
141
+ return "kotlin.Boolean";
142
+ case "int32":
143
+ return "kotlin.Int";
144
+ case "int64":
145
+ return "kotlin.Long";
146
+ case "uint64":
147
+ return "kotlin.ULong";
148
+ case "float32":
149
+ return "kotlin.Float";
150
+ case "float64":
151
+ return "kotlin.Double";
152
+ case "timestamp":
153
+ return "java.time.Instant";
154
+ case "string":
155
+ return "kotlin.String";
156
+ case "bytes":
157
+ return "okio.ByteString";
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ getClassName(recordKey: RecordKey): ClassName {
164
+ const record = this.recordMap.get(recordKey)!;
165
+ return this.namer.getClassName(record);
166
+ }
167
+
168
+ getSerializerExpression(type: ResolvedType): string {
169
+ switch (type.kind) {
170
+ case "primitive": {
171
+ switch (type.primitive) {
172
+ case "bool":
173
+ return "build.skir.Serializers.bool";
174
+ case "int32":
175
+ return "build.skir.Serializers.int32";
176
+ case "int64":
177
+ return "build.skir.Serializers.int64";
178
+ case "uint64":
179
+ return "build.skir.Serializers.uint64";
180
+ case "float32":
181
+ return "build.skir.Serializers.float32";
182
+ case "float64":
183
+ return "build.skir.Serializers.float64";
184
+ case "timestamp":
185
+ return "build.skir.Serializers.timestamp";
186
+ case "string":
187
+ return "build.skir.Serializers.string";
188
+ case "bytes":
189
+ return "build.skir.Serializers.bytes";
190
+ }
191
+ const _: never = type.primitive;
192
+ throw TypeError();
193
+ }
194
+ case "array": {
195
+ if (type.key) {
196
+ const keyChain = type.key.path.map((f) => f.name.text).join(".");
197
+ const path = type.key.path
198
+ .map((f) => this.namer.structFieldToKotlinName(f.name.text))
199
+ .join(".");
200
+ return (
201
+ "build.skir.internal.keyedListSerializer(\n" +
202
+ this.getSerializerExpression(type.item) +
203
+ `,\n"${keyChain}",\n{ it.${path} },\n)`
204
+ );
205
+ } else {
206
+ return (
207
+ "build.skir.Serializers.list(\n" +
208
+ this.getSerializerExpression(type.item) +
209
+ ",\n)"
210
+ );
211
+ }
212
+ }
213
+ case "optional": {
214
+ return (
215
+ `build.skir.Serializers.optional(\n` +
216
+ this.getSerializerExpression(type.other) +
217
+ `,\n)`
218
+ );
219
+ }
220
+ case "record": {
221
+ return this.getClassName(type.key).qualifiedName + ".serializer";
222
+ }
223
+ }
224
+ }
225
+ }