@tutao/licc 3.98.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 +137 -0
- package/cli.js +0 -0
- package/dist/Accumulator.js +23 -0
- package/dist/KotlinGenerator.js +224 -0
- package/dist/Parser.js +49 -0
- package/dist/SwiftGenerator.js +232 -0
- package/dist/TypescriptGenerator.js +199 -0
- package/dist/cli.js +83 -0
- package/dist/common.js +18 -0
- package/dist/index.js +118 -0
- package/dist/tsbuildinfo +1 -0
- package/lib/Accumulator.ts +30 -0
- package/lib/KotlinGenerator.ts +241 -0
- package/lib/Parser.ts +68 -0
- package/lib/SwiftGenerator.ts +248 -0
- package/lib/TypescriptGenerator.ts +222 -0
- package/lib/cli.ts +94 -0
- package/lib/common.ts +91 -0
- package/lib/index.ts +126 -0
- package/package.json +25 -0
- package/test/ParserTest.ts +124 -0
- package/test/Suite.ts +6 -0
- package/test/tsconfig.json +19 -0
- package/tsconfig.json +111 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# licc - the little interprocess communications compiler
|
|
2
|
+
|
|
3
|
+
usage info: type `licc --help`
|
|
4
|
+
|
|
5
|
+
## output
|
|
6
|
+
|
|
7
|
+
there are three kinds of json files `licc` understands. they are distinguished by their top-level `type` property.
|
|
8
|
+
|
|
9
|
+
### structs
|
|
10
|
+
|
|
11
|
+
`"type": "struct"` definitions are simply generated into the output directory as a single source file of the
|
|
12
|
+
platform-appropriate language.
|
|
13
|
+
|
|
14
|
+
### facades
|
|
15
|
+
|
|
16
|
+
`"type": "facade"` definitions can lead to several output files, depending on the definitions `senders` and `receivers`
|
|
17
|
+
fields:
|
|
18
|
+
|
|
19
|
+
* **senders and receivers**: the interface containing all methods
|
|
20
|
+
* **senders**: one implementation of the interface in form of the `SendDispatcher`.
|
|
21
|
+
It takes a transport instance that does the actual sending of the message and must be implemented manually.
|
|
22
|
+
* **receivers**: one `ReceiveDispatcher`. It takes the actual, working implementation of the interface during
|
|
23
|
+
construction
|
|
24
|
+
and dispatches to it.
|
|
25
|
+
* **additionally**, every platform that is on the receiving side of any facades gets one `GlobalDispatcher`
|
|
26
|
+
implementation
|
|
27
|
+
that dispatches to all receive dispatchers.
|
|
28
|
+
|
|
29
|
+
this leads to the following flow, with manually implemented components marked with `*`:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
SENDING SIDE: *caller* => SendDispatcher => *outgoing transport*
|
|
33
|
+
RECEIVING SIDE: *incoming transport* => GlobalDispatcher => ReceiveDispatcher => *facade implementation*
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Dispatch is achieved via string identifiers; the incoming transport will
|
|
37
|
+
call `GlobalDispatcher.dispatch("FacadeName", "methodName", arrayOfArgs)` which calls the ReceiveDispatcher
|
|
38
|
+
for `FacadeName` with `ReceiveDispatcher.dispatch("methodName", arrayOfArgs)`.
|
|
39
|
+
This call will be dispatched to the appropriate method as `facadeName.methodName(arrayOfArgs[0], ..., arrayOfArgs[-1])`.
|
|
40
|
+
|
|
41
|
+
### typerefs
|
|
42
|
+
|
|
43
|
+
`"type": "typeref"` definitions are used to refer to types that are not defined in a definition file and are not
|
|
44
|
+
primitives (that
|
|
45
|
+
the generator therefore has no knowledge of). They have to contain a language-specific path to a definition of the
|
|
46
|
+
type that the generator will generate a reexport for, which can then be referred to by the facades (which don't actually
|
|
47
|
+
know the difference between a generated struct and such a reexport).
|
|
48
|
+
|
|
49
|
+
## definition syntax
|
|
50
|
+
|
|
51
|
+
the schema format is described in `lib/common.ts`.
|
|
52
|
+
each schema is a JSON file with a single data type or facade definition.
|
|
53
|
+
as discussed above, the type (`struct`, `facade` or `typeref`) is given by the `type` property of the contained json
|
|
54
|
+
object.
|
|
55
|
+
facades must have a `senders` and a `receivers` property listing the appropriate platforms.
|
|
56
|
+
|
|
57
|
+
**Note:** there is minimal validation. we don't detect duplicate method or argument definitions and do not do a very
|
|
58
|
+
good job to validate type syntax.
|
|
59
|
+
|
|
60
|
+
### structs
|
|
61
|
+
|
|
62
|
+
struct fields are given as an object with `"fieldName": "fieldType"` properties.
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"name": "Foo",
|
|
67
|
+
"type": "struct",
|
|
68
|
+
"doc": "optional doc comment that explains the type's purpose",
|
|
69
|
+
"fields": {
|
|
70
|
+
"fieldName": "fieldType",
|
|
71
|
+
...
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### facades
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"name": "BarFacade",
|
|
81
|
+
"type": "facade",
|
|
82
|
+
"senders": ["web"],
|
|
83
|
+
"receivers": ["desktop", "ios"],
|
|
84
|
+
"doc": "optional doc comment explaining the scope of the facade",
|
|
85
|
+
"methods": {
|
|
86
|
+
"methodName": {
|
|
87
|
+
"doc": "optional comment explaining the contract and purpose of the method",
|
|
88
|
+
"arg": [
|
|
89
|
+
{"argName1": "argType1"},
|
|
90
|
+
{"argName2": "argType2"},
|
|
91
|
+
...
|
|
92
|
+
],
|
|
93
|
+
"ret": "returnType"
|
|
94
|
+
},
|
|
95
|
+
...
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Note: method arg must be given as a list of single-property objects as above to preserve argument order.
|
|
101
|
+
|
|
102
|
+
### typerefs
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"name": "BazType",
|
|
107
|
+
"type": "typeref",
|
|
108
|
+
"location": {
|
|
109
|
+
"typescript": "../../src/somedir/BazType.js",
|
|
110
|
+
"kotlin": "de.tutao.tutanota.BazType"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Note the `.js` extension on the typescript reference. Reference paths are resolved relative to the `json` file
|
|
116
|
+
containing the typeref.
|
|
117
|
+
|
|
118
|
+
### supported types:
|
|
119
|
+
|
|
120
|
+
* nullable types, denoted with a `?` suffix: `string?`
|
|
121
|
+
* `List<elementType>`
|
|
122
|
+
* `Map<keyType, valueType>`
|
|
123
|
+
* the primitives listed in `dist/parser.ts`
|
|
124
|
+
* "external" types (the ones that don't fit any of the above but are otherwise valid identifiers)
|
|
125
|
+
* any combination of these
|
|
126
|
+
|
|
127
|
+
all type names must be valid identifiers in all supported output languages.
|
|
128
|
+
|
|
129
|
+
## Known issues
|
|
130
|
+
|
|
131
|
+
* the `bytes` primitive types is generating `DataWrapper` types for mobile, because we can't directly send byte arrays
|
|
132
|
+
over the bridge and need some kind of marker that distinguishes plain strings from encoded byte arrays. Currently,
|
|
133
|
+
this requirement to wrap byte arrays leaks out to consumers of the generated classes.
|
|
134
|
+
* struct definitions are generated for every language regardless if they're mentioned in that languages' generated
|
|
135
|
+
files.
|
|
136
|
+
* it's theoretically possible two separate compilations of the same source files to yield different output because field
|
|
137
|
+
order in json objects is not defined. this was not observed yet and is unlikely to become a problem.
|
package/cli.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const HEADER = "/* generated file, don't edit. */\n";
|
|
2
|
+
export class Accumulator {
|
|
3
|
+
appender;
|
|
4
|
+
code = "";
|
|
5
|
+
imports = new Set();
|
|
6
|
+
constructor(appender = (code) => this.code += code) {
|
|
7
|
+
this.appender = appender;
|
|
8
|
+
}
|
|
9
|
+
line(code = "") {
|
|
10
|
+
this.appender(code + "\n");
|
|
11
|
+
}
|
|
12
|
+
indent(indent = "\t") {
|
|
13
|
+
return new Accumulator((code) => {
|
|
14
|
+
this.appender(indent + code);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
addImport(imp) {
|
|
18
|
+
this.imports.add(imp);
|
|
19
|
+
}
|
|
20
|
+
finish() {
|
|
21
|
+
return HEADER + "\n" + Array.from(this.imports).join("\n") + "\n" + this.code;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { getArgs, minusculize } from "./common.js";
|
|
2
|
+
import { Accumulator } from "./Accumulator.js";
|
|
3
|
+
import { parseType } from "./Parser.js";
|
|
4
|
+
export class KotlinGenerator {
|
|
5
|
+
generateGlobalDispatcher(name, facadeNames) {
|
|
6
|
+
const acc = new Accumulator();
|
|
7
|
+
KotlinGenerator.generateImports(acc);
|
|
8
|
+
acc.line(`import de.tutao.tutanota.ipc.*`);
|
|
9
|
+
acc.line();
|
|
10
|
+
acc.line(`class ${name} (`);
|
|
11
|
+
const methodAcc = acc.indent();
|
|
12
|
+
methodAcc.line(`json: Json,`);
|
|
13
|
+
for (let facadeName of facadeNames) {
|
|
14
|
+
methodAcc.line(`${minusculize(facadeName)} : ${facadeName},`);
|
|
15
|
+
}
|
|
16
|
+
acc.line(") {");
|
|
17
|
+
for (let facadeName of facadeNames) {
|
|
18
|
+
methodAcc.line(`private val ${minusculize(facadeName)}: ${facadeName}ReceiveDispatcher = ${facadeName}ReceiveDispatcher(json, ${minusculize(facadeName)})`);
|
|
19
|
+
}
|
|
20
|
+
methodAcc.line();
|
|
21
|
+
methodAcc.line(`suspend fun dispatch(facadeName: String, methodName: String, args: List<String>): String {`);
|
|
22
|
+
const whenAcc = methodAcc.indent();
|
|
23
|
+
whenAcc.line(`return when (facadeName) {`);
|
|
24
|
+
const caseAcc = whenAcc.indent();
|
|
25
|
+
for (let facadeName of facadeNames) {
|
|
26
|
+
caseAcc.line(`"${facadeName}" -> this.${minusculize(facadeName)}.dispatch(methodName, args)`);
|
|
27
|
+
}
|
|
28
|
+
caseAcc.line(`else -> throw Error("unknown facade: $facadeName")`);
|
|
29
|
+
whenAcc.line(`}`);
|
|
30
|
+
methodAcc.line(`}`);
|
|
31
|
+
acc.line(`}`);
|
|
32
|
+
return acc.finish();
|
|
33
|
+
}
|
|
34
|
+
handleStructDefinition(definition) {
|
|
35
|
+
const acc = new Accumulator();
|
|
36
|
+
KotlinGenerator.generateImports(acc);
|
|
37
|
+
acc.line();
|
|
38
|
+
if (definition.doc) {
|
|
39
|
+
this.generateDocComment(acc, definition.doc);
|
|
40
|
+
}
|
|
41
|
+
acc.line("@Serializable");
|
|
42
|
+
acc.line(`data class ${definition.name}(`);
|
|
43
|
+
const fieldGenerator = acc.indent();
|
|
44
|
+
for (const [name, fieldDefinition] of Object.entries(definition.fields)) {
|
|
45
|
+
const renderedType = typeNameKotlin(fieldDefinition);
|
|
46
|
+
fieldGenerator.line(`val ${name}: ${renderedType.name},`);
|
|
47
|
+
}
|
|
48
|
+
acc.line(")");
|
|
49
|
+
return acc.finish();
|
|
50
|
+
}
|
|
51
|
+
static generateImports(acc) {
|
|
52
|
+
acc.line("package de.tutao.tutanota.ipc");
|
|
53
|
+
acc.line();
|
|
54
|
+
acc.line("import kotlinx.serialization.*");
|
|
55
|
+
acc.line("import kotlinx.serialization.json.*");
|
|
56
|
+
acc.line();
|
|
57
|
+
}
|
|
58
|
+
generateDocComment(acc, comment) {
|
|
59
|
+
if (!comment)
|
|
60
|
+
return;
|
|
61
|
+
acc.line("/**");
|
|
62
|
+
acc.line(` * ${comment}`);
|
|
63
|
+
acc.line(" */");
|
|
64
|
+
}
|
|
65
|
+
generateFacade(definition) {
|
|
66
|
+
const acc = new Accumulator();
|
|
67
|
+
KotlinGenerator.generateImports(acc);
|
|
68
|
+
this.generateDocComment(acc, definition.doc);
|
|
69
|
+
acc.line(`interface ${definition.name} {`);
|
|
70
|
+
const methodAcc = acc.indent();
|
|
71
|
+
for (const [name, methodDefinition] of Object.entries(definition.methods)) {
|
|
72
|
+
this.generateDocComment(methodAcc, methodDefinition.doc);
|
|
73
|
+
KotlinGenerator.generateMethodSignature(methodAcc, name, methodDefinition);
|
|
74
|
+
}
|
|
75
|
+
acc.line("}");
|
|
76
|
+
return acc.finish();
|
|
77
|
+
}
|
|
78
|
+
generateReceiveDispatcher(definition) {
|
|
79
|
+
const acc = new Accumulator();
|
|
80
|
+
// Some names might clash, we don't read this file, we don't care
|
|
81
|
+
acc.line(`@file:Suppress("NAME_SHADOWING")`);
|
|
82
|
+
KotlinGenerator.generateImports(acc);
|
|
83
|
+
acc.line(`class ${definition.name}ReceiveDispatcher(`);
|
|
84
|
+
acc.indent().line(`private val json: Json,`);
|
|
85
|
+
acc.indent().line(`private val facade: ${definition.name},`);
|
|
86
|
+
acc.line(`) {`);
|
|
87
|
+
const methAcc = acc.indent();
|
|
88
|
+
methAcc.line();
|
|
89
|
+
methAcc.line(`suspend fun dispatch(method: String, arg: List<String>): String {`);
|
|
90
|
+
const whenAcc = methAcc.indent();
|
|
91
|
+
whenAcc.line(`when (method) {`);
|
|
92
|
+
const caseAcc = whenAcc.indent();
|
|
93
|
+
for (const [methodName, methodDef] of Object.entries(definition.methods)) {
|
|
94
|
+
caseAcc.line(`"${methodName}" -> {`);
|
|
95
|
+
const arg = getArgs(methodName, methodDef);
|
|
96
|
+
const decodedArgs = [];
|
|
97
|
+
for (let i = 0; i < arg.length; i++) {
|
|
98
|
+
const { name: argName, type } = arg[i];
|
|
99
|
+
const renderedArgType = typeNameKotlin(type);
|
|
100
|
+
decodedArgs.push([argName, renderedArgType]);
|
|
101
|
+
}
|
|
102
|
+
const varAcc = caseAcc.indent();
|
|
103
|
+
for (let i = 0; i < arg.length; i++) {
|
|
104
|
+
const [argName, renderedType] = decodedArgs[i];
|
|
105
|
+
varAcc.line(`val ${argName}: ${renderedType.name} = json.decodeFromString(arg[${i}])`);
|
|
106
|
+
}
|
|
107
|
+
varAcc.line(`val result: ${typeNameKotlin(methodDef.ret).name} = this.facade.${methodName}(`);
|
|
108
|
+
for (let i = 0; i < arg.length; i++) {
|
|
109
|
+
const [argName] = decodedArgs[i];
|
|
110
|
+
varAcc.indent().line(`${argName},`);
|
|
111
|
+
}
|
|
112
|
+
varAcc.line(`)`);
|
|
113
|
+
varAcc.line(`return json.encodeToString(result)`);
|
|
114
|
+
caseAcc.line(`}`);
|
|
115
|
+
}
|
|
116
|
+
caseAcc.line(`else -> throw Error("unknown method for ${definition.name}: $method")`);
|
|
117
|
+
whenAcc.line(`}`);
|
|
118
|
+
methAcc.line(`}`);
|
|
119
|
+
acc.line(`}`);
|
|
120
|
+
return acc.finish();
|
|
121
|
+
}
|
|
122
|
+
static generateNativeInterface() {
|
|
123
|
+
const acc = new Accumulator();
|
|
124
|
+
KotlinGenerator.generateImports(acc);
|
|
125
|
+
acc.line(`interface NativeInterface {`);
|
|
126
|
+
acc.indent().line(`suspend fun sendRequest(requestType: String, args: List<String>): String`);
|
|
127
|
+
acc.line(`}`);
|
|
128
|
+
return acc.finish();
|
|
129
|
+
}
|
|
130
|
+
static generateMethodSignature(methodGenerator, name, methodDefinition, prefix = "") {
|
|
131
|
+
methodGenerator.line(`${prefix} suspend fun ${name}(`);
|
|
132
|
+
const argGenerator = methodGenerator.indent();
|
|
133
|
+
for (const argument of getArgs(name, methodDefinition)) {
|
|
134
|
+
const renderedArgument = typeNameKotlin(argument.type);
|
|
135
|
+
argGenerator.line(`${argument.name}: ${renderedArgument.name},`);
|
|
136
|
+
}
|
|
137
|
+
const renderedReturn = typeNameKotlin(methodDefinition.ret);
|
|
138
|
+
methodGenerator.line(`): ${renderedReturn.name}`);
|
|
139
|
+
}
|
|
140
|
+
generateSendDispatcher(definition) {
|
|
141
|
+
const acc = new Accumulator();
|
|
142
|
+
KotlinGenerator.generateImports(acc);
|
|
143
|
+
const classBodyAcc = acc.indent();
|
|
144
|
+
acc.line(`class ${definition.name}SendDispatcher (`);
|
|
145
|
+
classBodyAcc.line(`private val json: Json,`);
|
|
146
|
+
classBodyAcc.line(`private val transport : NativeInterface,`);
|
|
147
|
+
acc.line(`) : ${definition.name} {`);
|
|
148
|
+
classBodyAcc.line(`private val encodedFacade = json.encodeToString("${definition.name}")`);
|
|
149
|
+
for (const [methodName, methodDefinition] of Object.entries(definition.methods)) {
|
|
150
|
+
KotlinGenerator.generateMethodSignature(classBodyAcc, methodName, methodDefinition, "override");
|
|
151
|
+
classBodyAcc.line("{");
|
|
152
|
+
const methodBodyAcc = classBodyAcc.indent();
|
|
153
|
+
methodBodyAcc.line(`val encodedMethod = json.encodeToString("${methodName}")`);
|
|
154
|
+
methodBodyAcc.line("val args : MutableList<String> = mutableListOf()");
|
|
155
|
+
for (let arg of getArgs(methodName, methodDefinition)) {
|
|
156
|
+
methodBodyAcc.line(`args.add(json.encodeToString(${arg.name}))`);
|
|
157
|
+
}
|
|
158
|
+
if (methodDefinition.ret !== "void") {
|
|
159
|
+
methodBodyAcc.line(`val result = this.transport.sendRequest("ipc", listOf(encodedFacade, encodedMethod) + args)`);
|
|
160
|
+
methodBodyAcc.line(`return json.decodeFromString(result)`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
methodBodyAcc.line(`this.transport.sendRequest("ipc", listOf(encodedFacade, encodedMethod) + args)`);
|
|
164
|
+
}
|
|
165
|
+
classBodyAcc.line(`}`);
|
|
166
|
+
classBodyAcc.line();
|
|
167
|
+
}
|
|
168
|
+
acc.line(`}`);
|
|
169
|
+
return acc.finish();
|
|
170
|
+
}
|
|
171
|
+
generateExtraFiles() {
|
|
172
|
+
return {
|
|
173
|
+
"NativeInterface": KotlinGenerator.generateNativeInterface()
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
generateTypeRef(outDir, definitionPath, definition) {
|
|
177
|
+
if (definition.location.kotlin) {
|
|
178
|
+
const acc = new Accumulator();
|
|
179
|
+
acc.line(`package de.tutao.tutanota.ipc`);
|
|
180
|
+
acc.line(`typealias ${definition.name} = ${definition.location.kotlin}`);
|
|
181
|
+
return acc.finish();
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function typeNameKotlin(name) {
|
|
189
|
+
const parsed = parseType(name);
|
|
190
|
+
return renderKotlinType(parsed);
|
|
191
|
+
}
|
|
192
|
+
function renderKotlinType(parsed) {
|
|
193
|
+
const { baseName, nullable, external } = parsed;
|
|
194
|
+
switch (baseName) {
|
|
195
|
+
case "List":
|
|
196
|
+
const renderedListInner = renderKotlinType(parsed.generics[0]);
|
|
197
|
+
return {
|
|
198
|
+
externals: renderedListInner.externals,
|
|
199
|
+
name: maybeNullable(`List<${renderedListInner.name}>`, nullable)
|
|
200
|
+
};
|
|
201
|
+
case "Map":
|
|
202
|
+
const renderedKey = renderKotlinType(parsed.generics[0]);
|
|
203
|
+
const renderedValue = renderKotlinType(parsed.generics[1]);
|
|
204
|
+
return {
|
|
205
|
+
externals: [...renderedKey.externals, ...renderedValue.externals],
|
|
206
|
+
name: maybeNullable(`Map<${renderedKey.name}, ${renderedValue.name}>`, nullable)
|
|
207
|
+
};
|
|
208
|
+
case "string":
|
|
209
|
+
return { externals: [], name: maybeNullable("String", nullable) };
|
|
210
|
+
case "boolean":
|
|
211
|
+
return { externals: [], name: maybeNullable("Boolean", nullable) };
|
|
212
|
+
case "number":
|
|
213
|
+
return { externals: [], name: maybeNullable("Int", nullable) };
|
|
214
|
+
case "bytes":
|
|
215
|
+
return { externals: [], name: maybeNullable("DataWrapper", nullable) };
|
|
216
|
+
case "void":
|
|
217
|
+
return { externals: [], name: maybeNullable("Unit", nullable) };
|
|
218
|
+
default:
|
|
219
|
+
return { externals: [baseName], name: maybeNullable(baseName, nullable) };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function maybeNullable(name, nullable) {
|
|
223
|
+
return nullable ? `${name}?` : name;
|
|
224
|
+
}
|
package/dist/Parser.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const PRIMITIVES = ["string", "boolean", "number", "bytes", "void"];
|
|
2
|
+
const KOTLIN_KEYWORDS = [
|
|
3
|
+
"as", "break", "class", "continue", "do", "else", "false", "for", "fun", "if", "in", "interface", "is", "null", "object", "package", "return", "super",
|
|
4
|
+
"this", "throw", "true", "try", "typealias", "typeof", "val", "var", "when", "while"
|
|
5
|
+
];
|
|
6
|
+
const TYPESCRIPT_KEYWORDS = [
|
|
7
|
+
"var", "const", "let", "break", "return", "case", "catch", "class", "continue", "debugger", "default", "delete", "do", "else", "enum", "export", "extends",
|
|
8
|
+
"false", "finally", "for", "function", "if", "import", "in", "instanceOf", "new", "null", "return", "super", "switch", "this", "throw", "true", "try",
|
|
9
|
+
"typeOf", "void", "while", "with"
|
|
10
|
+
];
|
|
11
|
+
const SWIFT_KEYWORDS = [
|
|
12
|
+
"Class", "deinit", "Enum", "extension", "Func", "import", "Init", "internal", "Let", "operator", "private", "protocol", "public", "static", "struct", "subscript",
|
|
13
|
+
"typealias", "var", "break", "case", "continue", "default", "do", "else", "fallthrough", "for", "if", "in", "return", "switch", "where", "while",
|
|
14
|
+
"as", "dynamicType", "false", "is", "nil", "self", "Self", "super", "true", "_COLUMN_", "_FILE_", "_FUNCTION_", "_LINE_",
|
|
15
|
+
"associativity", "convenience", "dynamic", "didSet", "final", "get", "infix", "inout", "lazy", "left", "mutating", "none", "nonmutating",
|
|
16
|
+
"optional", "override", "postfix", "precedence", "prefix", "Protocol", "required", "right", "set", "Type", "unowned", "weak", "willSet"
|
|
17
|
+
];
|
|
18
|
+
const FORBIDDEN_IDENTIFIERS = new Set([...KOTLIN_KEYWORDS, ...TYPESCRIPT_KEYWORDS, ...SWIFT_KEYWORDS]);
|
|
19
|
+
/**
|
|
20
|
+
* parse a type definition string from the json into a structure that contains all information needed to render
|
|
21
|
+
* language-specific type defs
|
|
22
|
+
*/
|
|
23
|
+
export function parseType(typeString) {
|
|
24
|
+
let nullable = false;
|
|
25
|
+
typeString = typeString.trim();
|
|
26
|
+
if (typeString.endsWith("?")) {
|
|
27
|
+
nullable = true;
|
|
28
|
+
typeString = typeString.slice(0, -1);
|
|
29
|
+
}
|
|
30
|
+
const listMatch = typeString.match(/^\s*List<\s*(.*)\s*>\s*$/);
|
|
31
|
+
if (listMatch) {
|
|
32
|
+
const nested = parseType(listMatch[1]);
|
|
33
|
+
return { baseName: "List", generics: [nested], nullable, external: false };
|
|
34
|
+
}
|
|
35
|
+
const mapMatch = typeString.match(/^\s*Map<\s*(.*?),\s*(.*?)\s*>\s*$/);
|
|
36
|
+
if (mapMatch) {
|
|
37
|
+
const nestedKey = parseType(mapMatch[1]);
|
|
38
|
+
const nestedValue = parseType(mapMatch[2]);
|
|
39
|
+
return { baseName: "Map", generics: [nestedKey, nestedValue], nullable, external: false };
|
|
40
|
+
}
|
|
41
|
+
// this is a basic type without generic params
|
|
42
|
+
const external = !PRIMITIVES.includes(typeString);
|
|
43
|
+
const willBreakAtLeastOneLang = FORBIDDEN_IDENTIFIERS.has(typeString) && external;
|
|
44
|
+
const startsWithLetterOrUnderscore = typeString.match(/^[_a-zA-Z]/);
|
|
45
|
+
if (willBreakAtLeastOneLang || !startsWithLetterOrUnderscore) {
|
|
46
|
+
throw new Error(`illegal identifier: "${typeString}"`);
|
|
47
|
+
}
|
|
48
|
+
return { baseName: typeString, generics: [], external, nullable };
|
|
49
|
+
} // frontend type
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { getArgs, minusculize } from "./common.js";
|
|
2
|
+
import { Accumulator } from "./Accumulator.js";
|
|
3
|
+
import { parseType } from "./Parser.js";
|
|
4
|
+
export class SwiftGenerator {
|
|
5
|
+
handleStructDefinition(definition) {
|
|
6
|
+
const acc = new Accumulator();
|
|
7
|
+
this.generateDocComment(acc, definition.doc);
|
|
8
|
+
acc.line(`public struct ${definition.name} : Codable {`);
|
|
9
|
+
const fieldGenerator = acc.indent();
|
|
10
|
+
for (const [name, fieldDefinition] of Object.entries(definition.fields)) {
|
|
11
|
+
const renderedType = typeNameSwift(fieldDefinition);
|
|
12
|
+
fieldGenerator.line(`let ${name}: ${renderedType.name}`);
|
|
13
|
+
}
|
|
14
|
+
acc.line("}");
|
|
15
|
+
return acc.finish();
|
|
16
|
+
}
|
|
17
|
+
generateDocComment(acc, comment) {
|
|
18
|
+
if (!comment)
|
|
19
|
+
return;
|
|
20
|
+
acc.line("/**");
|
|
21
|
+
acc.line(` * ${comment}`);
|
|
22
|
+
acc.line(" */");
|
|
23
|
+
}
|
|
24
|
+
generateFacade(definition) {
|
|
25
|
+
const acc = new Accumulator();
|
|
26
|
+
acc.line("import Foundation");
|
|
27
|
+
acc.line();
|
|
28
|
+
this.generateDocComment(acc, definition.doc);
|
|
29
|
+
acc.line(`public protocol ${definition.name} {`);
|
|
30
|
+
const methodAcc = acc.indent();
|
|
31
|
+
for (const [name, methodDefinition] of Object.entries(definition.methods)) {
|
|
32
|
+
this.generateDocComment(methodAcc, methodDefinition.doc);
|
|
33
|
+
SwiftGenerator.generateMethodSignature(methodAcc, name, methodDefinition);
|
|
34
|
+
}
|
|
35
|
+
acc.line("}");
|
|
36
|
+
return acc.finish();
|
|
37
|
+
}
|
|
38
|
+
static generateMethodSignature(methodGenerator, name, methodDefinition) {
|
|
39
|
+
methodGenerator.line(`func ${name}(`);
|
|
40
|
+
const argGenerator = methodGenerator.indent();
|
|
41
|
+
const args = getArgs(name, methodDefinition);
|
|
42
|
+
const lastArg = args[args.length - 1];
|
|
43
|
+
for (const argument of args) {
|
|
44
|
+
const renderedArgument = typeNameSwift(argument.type);
|
|
45
|
+
const argLine = `_ ${argument.name}: ${renderedArgument.name}` +
|
|
46
|
+
((argument === lastArg) ? "" : ",");
|
|
47
|
+
argGenerator.line(argLine);
|
|
48
|
+
}
|
|
49
|
+
const renderedReturn = typeNameSwift(methodDefinition.ret);
|
|
50
|
+
methodGenerator.line(`) async throws -> ${renderedReturn.name}`);
|
|
51
|
+
}
|
|
52
|
+
generateReceiveDispatcher(definition) {
|
|
53
|
+
const acc = new Accumulator();
|
|
54
|
+
acc.line("import Foundation");
|
|
55
|
+
acc.line();
|
|
56
|
+
acc.line(`public class ${definition.name}ReceiveDispatcher {`);
|
|
57
|
+
const methodAcc = acc.indent();
|
|
58
|
+
methodAcc.line(`let facade: ${definition.name}`);
|
|
59
|
+
methodAcc.line(`init(facade: ${definition.name}) {`);
|
|
60
|
+
methodAcc.indent().line(`self.facade = facade`);
|
|
61
|
+
methodAcc.line(`}`);
|
|
62
|
+
methodAcc.line(`func dispatch(method: String, arg: [String]) async throws -> String {`);
|
|
63
|
+
const switchAcc = methodAcc.indent();
|
|
64
|
+
switchAcc.line(`switch method {`);
|
|
65
|
+
const caseAcc = switchAcc.indent();
|
|
66
|
+
for (const [methodName, method] of Object.entries(definition.methods)) {
|
|
67
|
+
const arg = getArgs(methodName, method);
|
|
68
|
+
const decodedArgs = [];
|
|
69
|
+
for (let i = 0; i < arg.length; i++) {
|
|
70
|
+
const { name: argName, type } = arg[i];
|
|
71
|
+
const renderedArgType = typeNameSwift(type);
|
|
72
|
+
decodedArgs.push([argName, renderedArgType]);
|
|
73
|
+
}
|
|
74
|
+
switchAcc.line(`case "${methodName}":`);
|
|
75
|
+
for (let i = 0; i < arg.length; i++) {
|
|
76
|
+
const [argName, argType] = decodedArgs[i];
|
|
77
|
+
caseAcc.line(`let ${argName} = try! JSONDecoder().decode(${argType.name}.self, from: arg[${i}].data(using: .utf8)!)`);
|
|
78
|
+
}
|
|
79
|
+
if (method.ret === "void") {
|
|
80
|
+
caseAcc.line(`try await self.facade.${methodName}(`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
caseAcc.line(`let result = try await self.facade.${methodName}(`);
|
|
84
|
+
}
|
|
85
|
+
for (let i = 0; i < arg.length; i++) {
|
|
86
|
+
const comma = i === arg.length - 1 ? "" : ",";
|
|
87
|
+
caseAcc.indent().line(arg[i].name + comma);
|
|
88
|
+
}
|
|
89
|
+
caseAcc.line(")");
|
|
90
|
+
if (method.ret === "void") {
|
|
91
|
+
caseAcc.line(`return "null"`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
caseAcc.line(`return toJson(result)`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
switchAcc.line(`default:`);
|
|
98
|
+
caseAcc.line(`fatalError("licc messed up! \\(method)")`);
|
|
99
|
+
switchAcc.line(`}`);
|
|
100
|
+
methodAcc.line(`}`);
|
|
101
|
+
acc.line(`}`);
|
|
102
|
+
return acc.finish();
|
|
103
|
+
}
|
|
104
|
+
generateGlobalDispatcher(name, facadeNames) {
|
|
105
|
+
const acc = new Accumulator();
|
|
106
|
+
acc.line(`public class ${name} {`);
|
|
107
|
+
const methodAcc = acc.indent();
|
|
108
|
+
for (let facadeName of facadeNames) {
|
|
109
|
+
methodAcc.line(`private let ${minusculize(facadeName)}: ${facadeName}ReceiveDispatcher`);
|
|
110
|
+
}
|
|
111
|
+
methodAcc.line();
|
|
112
|
+
methodAcc.line(`init(`);
|
|
113
|
+
const lastFacadeName = facadeNames[facadeNames.length - 1];
|
|
114
|
+
for (let facadeName of facadeNames) {
|
|
115
|
+
const comma = facadeName === lastFacadeName ? "" : ",";
|
|
116
|
+
methodAcc.indent().line(`${minusculize(facadeName)} : ${facadeName}${comma}`);
|
|
117
|
+
}
|
|
118
|
+
methodAcc.line(`) {`);
|
|
119
|
+
for (let facadeName of facadeNames) {
|
|
120
|
+
methodAcc.indent().line(`self.${minusculize(facadeName)} = ${facadeName}ReceiveDispatcher(facade: ${minusculize(facadeName)})`);
|
|
121
|
+
}
|
|
122
|
+
methodAcc.line(`}`);
|
|
123
|
+
methodAcc.line();
|
|
124
|
+
methodAcc.line(`func dispatch(facadeName: String, methodName: String, args: Array<String>) async throws -> String {`);
|
|
125
|
+
const switchAcc = methodAcc.indent();
|
|
126
|
+
switchAcc.line(`switch facadeName {`);
|
|
127
|
+
const caseAcc = switchAcc.indent();
|
|
128
|
+
for (let facadeName of facadeNames) {
|
|
129
|
+
caseAcc.line(`case "${facadeName}":`);
|
|
130
|
+
caseAcc.indent().line(`return try await self.${minusculize(facadeName)}.dispatch(method: methodName, arg: args)`);
|
|
131
|
+
}
|
|
132
|
+
caseAcc.line(`default:`);
|
|
133
|
+
caseAcc.indent().line(`fatalError("licc messed up! " + facadeName)`);
|
|
134
|
+
switchAcc.line(`}`);
|
|
135
|
+
methodAcc.line(`}`);
|
|
136
|
+
acc.line(`}`);
|
|
137
|
+
return acc.finish();
|
|
138
|
+
}
|
|
139
|
+
generateSendDispatcher(definition) {
|
|
140
|
+
const acc = new Accumulator();
|
|
141
|
+
acc.line("import Foundation");
|
|
142
|
+
acc.line();
|
|
143
|
+
acc.line(`class ${definition.name}SendDispatcher : ${definition.name} {`);
|
|
144
|
+
const classBodyAcc = acc.indent();
|
|
145
|
+
classBodyAcc.line(`private let transport: NativeInterface`);
|
|
146
|
+
classBodyAcc.line(`init(transport: NativeInterface) { self.transport = transport }`);
|
|
147
|
+
classBodyAcc.line();
|
|
148
|
+
for (const [methodName, methodDefinition] of Object.entries(definition.methods)) {
|
|
149
|
+
SwiftGenerator.generateMethodSignature(classBodyAcc, methodName, methodDefinition);
|
|
150
|
+
const methodBodyAcc = classBodyAcc.indent();
|
|
151
|
+
methodBodyAcc.line("{");
|
|
152
|
+
const args = getArgs(methodName, methodDefinition);
|
|
153
|
+
if (args.length > 0) {
|
|
154
|
+
methodBodyAcc.line("var args = [String]()");
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// let instead of var so that it shuts up about mutability unused
|
|
158
|
+
methodBodyAcc.line("let args = [String]()");
|
|
159
|
+
}
|
|
160
|
+
for (let arg of args) {
|
|
161
|
+
methodBodyAcc.line(`args.append(toJson(${arg.name}))`);
|
|
162
|
+
}
|
|
163
|
+
methodBodyAcc.line(`let encodedFacadeName = toJson("${definition.name}")`);
|
|
164
|
+
methodBodyAcc.line(`let encodedMethodName = toJson("${methodName}")`);
|
|
165
|
+
if (methodDefinition.ret !== "void") {
|
|
166
|
+
methodBodyAcc.line(`let returnValue = try await self.transport.sendRequest(requestType: "ipc", args: [encodedFacadeName, encodedMethodName] + args)`);
|
|
167
|
+
methodBodyAcc.line(`return try! JSONDecoder().decode(${typeNameSwift(methodDefinition.ret).name}.self, from: returnValue.data(using: .utf8)!)`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
methodBodyAcc.line(`let _ = try await self.transport.sendRequest(requestType: "ipc", args: [encodedFacadeName, encodedMethodName] + args)`);
|
|
171
|
+
}
|
|
172
|
+
methodBodyAcc.line(`}`);
|
|
173
|
+
classBodyAcc.line();
|
|
174
|
+
}
|
|
175
|
+
acc.line(`}`);
|
|
176
|
+
return acc.finish();
|
|
177
|
+
}
|
|
178
|
+
generateExtraFiles() {
|
|
179
|
+
return {
|
|
180
|
+
"NativeInterface": SwiftGenerator.generateNativeInterface()
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
static generateNativeInterface() {
|
|
184
|
+
const acc = new Accumulator();
|
|
185
|
+
acc.line(`protocol NativeInterface {`);
|
|
186
|
+
acc.indent().line(`func sendRequest(requestType: String, args: [String]) async throws -> String`);
|
|
187
|
+
acc.line(`}`);
|
|
188
|
+
acc.line();
|
|
189
|
+
acc.line("func toJson<T>(_ thing: T) -> String where T : Encodable {");
|
|
190
|
+
acc.indent().line("return String(data: try! JSONEncoder().encode(thing), encoding: .utf8)!");
|
|
191
|
+
acc.line("}");
|
|
192
|
+
acc.line();
|
|
193
|
+
return acc.finish();
|
|
194
|
+
}
|
|
195
|
+
generateTypeRef(outDir, definitionPath, definition) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function typeNameSwift(name) {
|
|
200
|
+
const parsed = parseType(name);
|
|
201
|
+
return renderSwiftType(parsed);
|
|
202
|
+
}
|
|
203
|
+
function renderSwiftType(parsed) {
|
|
204
|
+
const { baseName, nullable } = parsed;
|
|
205
|
+
switch (baseName) {
|
|
206
|
+
case "List":
|
|
207
|
+
const renderedListInner = renderSwiftType(parsed.generics[0]);
|
|
208
|
+
return { externals: renderedListInner.externals, name: maybeNullable(`[${renderedListInner.name}]`, nullable) };
|
|
209
|
+
case "Map":
|
|
210
|
+
const renderedKey = renderSwiftType(parsed.generics[0]);
|
|
211
|
+
const renderedValue = renderSwiftType(parsed.generics[1]);
|
|
212
|
+
return {
|
|
213
|
+
externals: [...renderedKey.externals, ...renderedValue.externals],
|
|
214
|
+
name: maybeNullable(`[${renderedKey.name} : ${renderedValue.name}]`, nullable)
|
|
215
|
+
};
|
|
216
|
+
case "string":
|
|
217
|
+
return { externals: [], name: maybeNullable("String", nullable) };
|
|
218
|
+
case "boolean":
|
|
219
|
+
return { externals: [], name: maybeNullable("Bool", nullable) };
|
|
220
|
+
case "number":
|
|
221
|
+
return { externals: [], name: maybeNullable("Int", nullable) };
|
|
222
|
+
case "bytes":
|
|
223
|
+
return { externals: [], name: maybeNullable("DataWrapper", nullable) };
|
|
224
|
+
case "void":
|
|
225
|
+
return { externals: [], name: maybeNullable("Void", nullable) };
|
|
226
|
+
default:
|
|
227
|
+
return { externals: [baseName], name: maybeNullable(baseName, nullable) };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function maybeNullable(name, nullable) {
|
|
231
|
+
return nullable ? `${name}?` : name;
|
|
232
|
+
}
|