@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.
@@ -0,0 +1,222 @@
1
+ import {Accumulator} from "./Accumulator.js"
2
+ import {FacadeDefinition, getArgs, LangGenerator, minusculize, RenderedType, StructDefinition, TypeRefDefinition} from "./common.js"
3
+ import {ParsedType, parseType} from "./Parser.js"
4
+ import path from "path"
5
+
6
+ export class TypescriptGenerator implements LangGenerator {
7
+
8
+ generateGlobalDispatcher(name: string, facadeNames: Array<string>): string {
9
+ const acc = new Accumulator()
10
+ for (let facadeName of facadeNames) {
11
+ acc.line(`import {${facadeName}} from "./${facadeName}.js"`)
12
+ acc.line(`import {${facadeName}ReceiveDispatcher} from "./${facadeName}ReceiveDispatcher.js"`)
13
+ }
14
+ acc.line()
15
+ acc.line(`export class ${name} {`)
16
+ const methodAcc = acc.indent()
17
+ for (let facadeName of facadeNames) {
18
+ methodAcc.line(`private readonly ${minusculize(facadeName)} : ${facadeName}ReceiveDispatcher`)
19
+ }
20
+ methodAcc.line(`constructor(`)
21
+ for (let facadeName of facadeNames) {
22
+ methodAcc.indent().line(`${minusculize(facadeName)} : ${facadeName},`)
23
+ }
24
+ methodAcc.line(`) {`)
25
+ for (let facadeName of facadeNames) {
26
+ methodAcc.indent().line(`this.${minusculize(facadeName)} = new ${facadeName}ReceiveDispatcher(${minusculize(facadeName)})`)
27
+ }
28
+ methodAcc.line("}")
29
+ methodAcc.line()
30
+
31
+ methodAcc.line(`async dispatch(facadeName: string, methodName: string, args: Array<any>) {`)
32
+ const switchAcc = methodAcc.indent()
33
+ switchAcc.line(`switch (facadeName) {`)
34
+ const caseAcc = switchAcc.indent()
35
+ for (let facadeName of facadeNames) {
36
+ caseAcc.line(`case "${facadeName}":`)
37
+ caseAcc.indent().line(`return this.${minusculize(facadeName)}.dispatch(methodName, args)`)
38
+ }
39
+ caseAcc.line(`default:`)
40
+ caseAcc.indent().line(`throw new Error("licc messed up! " + facadeName)`)
41
+ switchAcc.line(`}`)
42
+ methodAcc.line(`}`)
43
+ acc.line(`}`)
44
+ return acc.finish()
45
+ }
46
+
47
+ handleStructDefinition(definition: StructDefinition): string {
48
+ let acc = new Accumulator()
49
+ this.generateDocComment(acc, definition.doc)
50
+ acc.line(`export interface ${definition.name} {`)
51
+ let bodyGenerator = acc.indent()
52
+ for (const [fieldName, fieldType] of Object.entries(definition.fields)) {
53
+ const {name, externals} = typeNameTypescript(fieldType)
54
+ for (const external of externals) {
55
+ acc.addImport(`import {${external}} from "./${external}.js"`)
56
+ }
57
+ bodyGenerator.line(`readonly ${fieldName}: ${name}`)
58
+ }
59
+ acc.line("}")
60
+
61
+ return acc.finish()
62
+
63
+ }
64
+
65
+ private generateDocComment(acc: Accumulator, comment: string | null | undefined) {
66
+ if (!comment) return
67
+
68
+ acc.line("/**")
69
+ acc.line(` * ${comment}`)
70
+ acc.line(" */")
71
+ }
72
+
73
+ private static generateNativeInterface(accumulator: Accumulator) {
74
+ // Duplicate interface to not import it
75
+ accumulator.line("interface NativeInterface {")
76
+ accumulator.indent().line("invokeNative(requestType: string, args: unknown[]): Promise<any>")
77
+ accumulator.line("}")
78
+ }
79
+
80
+ generateFacade(definition: FacadeDefinition): string {
81
+ const acc = new Accumulator()
82
+ this.generateDocComment(acc, definition.doc)
83
+ acc.line(`export interface ${definition.name} {\n`)
84
+ let methodAcc = acc.indent()
85
+ for (const [name, method] of Object.entries(definition.methods)) {
86
+ this.generateDocComment(methodAcc, method.doc)
87
+ methodAcc.line(`${name}(`)
88
+ let argAccumulator = methodAcc.indent()
89
+ for (const arg of getArgs(name, method)) {
90
+ const name = renderTypeAndAddImports(arg.type, acc)
91
+ argAccumulator.line(`${arg.name}: ${name},`)
92
+ }
93
+ const resolvedReturnType = renderTypeAndAddImports(method.ret, acc)
94
+ methodAcc.line(`): Promise<${resolvedReturnType}>`)
95
+ methodAcc.line()
96
+ }
97
+ acc.line("}")
98
+ return acc.finish()
99
+ }
100
+
101
+ generateReceiveDispatcher(definition: FacadeDefinition): string {
102
+ const acc = new Accumulator()
103
+ acc.line(`import {${definition.name}} from "./${definition.name}.js"`)
104
+ acc.line()
105
+ acc.line(`export class ${definition.name}ReceiveDispatcher {`)
106
+ acc.indent().line(`constructor(private readonly facade: ${definition.name}) {}`)
107
+ acc.indent().line(`async dispatch(method: string, arg: Array<any>) : Promise<any> {`)
108
+ acc.indent().indent().line(`switch(method) {`)
109
+ const switchAccumulator = acc.indent().indent().indent()
110
+ for (const [methodName, methodDef] of Object.entries(definition.methods)) {
111
+ switchAccumulator.line(`case "${methodName}": {`)
112
+ const arg = getArgs(methodName, methodDef)
113
+ const decodedArgs = []
114
+ for (let i = 0; i < arg.length; i++) {
115
+ const {name: argName, type} = arg[i]
116
+ const renderedArgType = renderTypeAndAddImports(type, acc)
117
+ decodedArgs.push([argName, renderedArgType] as const)
118
+ }
119
+
120
+ for (let i = 0; i < arg.length; i++) {
121
+ const [argName, renderedType] = decodedArgs[i]
122
+ switchAccumulator.indent().line(`const ${argName}: ${renderedType} = arg[${i}]`)
123
+ }
124
+ switchAccumulator.indent().line(`return this.facade.${methodName}(`)
125
+ for (let i = 0; i < arg.length; i++) {
126
+ const [argName] = decodedArgs[i]
127
+ switchAccumulator.indent().indent().line(`${argName},`)
128
+ }
129
+ switchAccumulator.indent().line(`)`)
130
+ switchAccumulator.line(`}`)
131
+ }
132
+ acc.indent().indent().line(`}`)
133
+ acc.indent().line(`}`)
134
+ acc.line(`}`)
135
+ return acc.finish()
136
+ }
137
+
138
+ generateSendDispatcher(definition: FacadeDefinition): string {
139
+ const acc = new Accumulator()
140
+ acc.line(`import {${definition.name}} from "./${definition.name}.js"`)
141
+ acc.line()
142
+ TypescriptGenerator.generateNativeInterface(acc)
143
+ acc.line(`export class ${definition.name}SendDispatcher implements ${definition.name} {`)
144
+ acc.indent().line(`constructor(private readonly transport: NativeInterface) {}`)
145
+ for (const [methodName, _] of Object.entries(definition.methods)) {
146
+ const methodAccumulator = acc.indent()
147
+ methodAccumulator.line(`async ${methodName}(...args: Parameters<${definition.name}["${methodName}"]>) {`)
148
+ methodAccumulator.indent().line(`return this.transport.invokeNative("ipc", ["${definition.name}", "${methodName}", ...args])`)
149
+ methodAccumulator.line(`}`)
150
+ }
151
+
152
+ acc.line(`}`)
153
+ return acc.finish()
154
+ }
155
+
156
+ generateExtraFiles(): Record<string, string> {
157
+ return {};
158
+ }
159
+
160
+ generateTypeRef(outDir: string, definitionPath: string, definition: TypeRefDefinition): string {
161
+ const acc = new Accumulator()
162
+ let tsPath = definition.location.typescript
163
+ const isRelative = tsPath.startsWith(".")
164
+ const actualPath = (isRelative)
165
+ ? path.relative(
166
+ path.resolve(outDir),
167
+ path.resolve(definitionPath, tsPath)
168
+ )
169
+ : tsPath
170
+ acc.line(`export {${definition.name}} from "${actualPath}"`)
171
+
172
+ return acc.finish()
173
+ }
174
+ }
175
+
176
+ function renderTypescriptType(parsed: ParsedType): RenderedType {
177
+ const {baseName, nullable, external} = parsed
178
+ switch (baseName) {
179
+ case "List":
180
+ const renderedListInner = renderTypescriptType(parsed.generics[0])
181
+ return {
182
+ externals: renderedListInner.externals,
183
+ name: maybeNullable(`ReadonlyArray<${renderedListInner.name}>`, nullable)
184
+ }
185
+ case "Map":
186
+ const renderedKey = renderTypescriptType(parsed.generics[0])
187
+ const renderedValue = renderTypescriptType(parsed.generics[1])
188
+ return {
189
+ externals: [...renderedKey.externals, ...renderedValue.externals],
190
+ name: maybeNullable(`Record<${renderedKey.name}, ${renderedValue.name}>`, nullable)
191
+ }
192
+ case "string":
193
+ return {externals: [], name: maybeNullable("string", nullable)}
194
+ case "boolean":
195
+ return {externals: [], name: maybeNullable("boolean", nullable)}
196
+ case "number":
197
+ return {externals: [], name: maybeNullable("number", nullable)}
198
+ case "bytes":
199
+ return {externals: [], name: maybeNullable("Uint8Array", nullable)}
200
+ case "void":
201
+ return {externals: [], name: maybeNullable("void", nullable)}
202
+ default:
203
+ return {externals: [baseName], name: maybeNullable(baseName, nullable)}
204
+ }
205
+ }
206
+
207
+ function maybeNullable(name: string, nullable: boolean): string {
208
+ return nullable ? name + " | null" : name
209
+ }
210
+
211
+ function typeNameTypescript(name: string): RenderedType {
212
+ const parsed = parseType(name)
213
+ return renderTypescriptType(parsed)
214
+ }
215
+
216
+ function renderTypeAndAddImports(name: string, acc: Accumulator) {
217
+ const rendered = typeNameTypescript(name)
218
+ for (const external of rendered.externals) {
219
+ acc.addImport(`import {${external}} from "./${external}.js"`)
220
+ }
221
+ return rendered.name
222
+ }
package/lib/cli.ts ADDED
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import {generate} from "./index.js"
3
+ import * as fs from "fs"
4
+ import * as path from "path"
5
+ import {globby} from "zx"
6
+ import {Platform} from "./common"
7
+ import {Argument, Option, program} from "commander"
8
+
9
+ const PLATFORMS: Array<Platform> = ["ios", "web", "android", "desktop"]
10
+
11
+ const USAGE = `licc [options] from_dir to_dir
12
+
13
+ will recursively take all JSON files from \`from-dir\` and compile them.
14
+ output files are written into \`to-dir\`, without preserving subdirectory structure.`
15
+
16
+ await program
17
+ .usage(USAGE)
18
+ .addArgument(new Argument("from_dir").argRequired())
19
+ .addArgument(new Argument("to_dir").argOptional())
20
+ .addOption(new Option(
21
+ '-p, --platform <platform>',
22
+ 'platform to generate code for. if not specified, from_dir must be omitted as well. In this case, licc will read <from_dir>/.liccc as a json map from platform to output dir. if -p is set, from_dir must be set as well.'
23
+ )
24
+ .makeOptionMandatory(false)
25
+ .choices(PLATFORMS))
26
+ .action(async (from_dir, to_dir, {platform}) => {
27
+ assert(!(platform == null && to_dir != null),
28
+ "can't omit platform and use an explicit output dir. specify both -p <platform> and to_dir or none of them.")
29
+ assert(!(platform != null && to_dir == null),
30
+ "can't use an explicit platform but no output dir. specify both -p <platform> and to_dir or none of them.")
31
+
32
+ let conf: Record<string, string> = {}
33
+ if (platform != null) {
34
+ conf[platform] = path.resolve(process.cwd(), to_dir)
35
+ } else {
36
+ // check if there's a .liccc file that states the desired platforms and output dirs
37
+ const confPath = path.join(from_dir, ".liccc")
38
+ try {
39
+ const relConf: Record<Platform, string> = JSON.parse(await fs.promises.readFile(confPath, {encoding: "utf-8"}))
40
+ for (let [relPlatform, relPath] of Object.entries(relConf) as [Platform | "__comment", string][]) {
41
+ if (relPlatform === "__comment") continue
42
+ assert(PLATFORMS.includes(relPlatform), `invalid platform in .liccc: ${relPlatform}`)
43
+ conf[relPlatform] = path.resolve(process.cwd(), from_dir, relPath)
44
+ }
45
+ } catch (e) {
46
+ console.log(`unable to read ${confPath} as JSON: ${e}`)
47
+ process.exit(1)
48
+ }
49
+ }
50
+ await run(from_dir, conf)
51
+ })
52
+ .parseAsync(process.argv)
53
+
54
+ async function run(from_dir: string, conf: Record<Platform, string>): Promise<void> {
55
+ const inputFiles = await globby(path.join(process.cwd(), from_dir, "*/**/*.json"))
56
+ const inputMap = new Map(inputFiles.map((n: string) => (
57
+ [path.basename(n, ".json"), fs.readFileSync(n, "utf8")]
58
+ )))
59
+
60
+ // doing it here because some platforms generate into the same dir.
61
+ for (let outDir of Object.values(conf)) {
62
+ clearDir(outDir)
63
+ }
64
+
65
+ for (let [confPlatform, confOutDir] of Object.entries(conf)) {
66
+ console.log("generating for", confPlatform, "into", confOutDir)
67
+ try {
68
+ await generate(confPlatform as Platform, inputMap, confOutDir,)
69
+ } catch (e) {
70
+ assert(false, `compilation failed with ${e}`)
71
+ }
72
+ console.log("done; no errors\n")
73
+ }
74
+ }
75
+
76
+ function assert(proposition: boolean, text: string): void {
77
+ if (proposition) return
78
+ console.log("\nFatal Error:\n", text)
79
+ console.log("")
80
+ console.log(program.helpInformation())
81
+ process.exit(1)
82
+ }
83
+
84
+ function clearDir(dir: string) {
85
+ console.log("clearing dir:", dir)
86
+ try {
87
+ const files = fs.readdirSync(dir)
88
+ for (const file of files) {
89
+ fs.unlinkSync(path.join(dir, file))
90
+ }
91
+ } catch (e) {
92
+ console.log("could not clear dir:", e)
93
+ }
94
+ }
package/lib/common.ts ADDED
@@ -0,0 +1,91 @@
1
+ export interface LangGenerator {
2
+ /**
3
+ * generate a structured type definition
4
+ * @param definition
5
+ */
6
+ handleStructDefinition(definition: StructDefinition): string
7
+
8
+ /**
9
+ * generate a collection of methods with arguments and return types
10
+ * and its associated dispatchers
11
+ * @param definition
12
+ */
13
+ generateFacade(definition: FacadeDefinition): string
14
+
15
+ generateReceiveDispatcher(definition: FacadeDefinition): string
16
+
17
+ generateSendDispatcher(definition: FacadeDefinition): string
18
+
19
+ /**
20
+ * generate the receiving dispatcher for the facades we are on the receiving side of
21
+ */
22
+ generateGlobalDispatcher(name: string, facadeNames: Array<string>): string
23
+
24
+ /**
25
+ * generate extra type definitions needed to make the interface work
26
+ */
27
+ generateExtraFiles(): Record<string, string>
28
+
29
+ /**
30
+ * external types that don't get generated but are located somewhere else
31
+ */
32
+ generateTypeRef(outDir: string, definitionPath: string, definition: TypeRefDefinition): string | null
33
+ }
34
+
35
+ export type Platform = "ios" | "web" | "android" | "desktop"
36
+ export type Language = "kotlin" | "swift" | "typescript"
37
+
38
+ export interface StructDefinition {
39
+ type: "struct"
40
+ name: string
41
+ fields: Record<string, string>
42
+ doc?: string
43
+ }
44
+
45
+ export interface FacadeDefinition {
46
+ type: "facade"
47
+ name: string
48
+ senders: Array<Platform>
49
+ receivers: Array<Platform>
50
+ methods: Record<string, MethodDefinition>
51
+ doc?: string,
52
+ }
53
+
54
+ export interface TypeRefDefinition {
55
+ "type": "typeref"
56
+ "name": string
57
+ "location": Record<Language, string>
58
+ }
59
+
60
+ export interface MethodDefinition {
61
+ arg: Array<ArgumentDefinition>
62
+ ret: string
63
+ doc?: string
64
+ }
65
+
66
+ export type ArgumentDefinition = Record<string, string>
67
+
68
+ export interface RenderedType {
69
+ externals: string[],
70
+ name: string
71
+ }
72
+
73
+ export function getArgs(methName: string, methodDef: MethodDefinition): {name: string, type: string}[] {
74
+ return methodDef.arg.map((a, i) => {
75
+ const entries = Object.entries(a)
76
+ if (entries.length === 0) {
77
+ throw new Error(`Syntax Error: method ${methName} argument ${i} is empty`)
78
+ } else if (entries.length > 1) {
79
+ throw new Error(`Syntax Error: method ${methName} argument ${i} has too many entries`)
80
+ }
81
+ return {"name": entries[0][0], "type": entries[0][1]}
82
+ })
83
+ }
84
+
85
+ export function capitalize(input: string): string {
86
+ return input.replace(/^\w/, c => c.toUpperCase())
87
+ }
88
+
89
+ export function minusculize(input: string): string {
90
+ return input.replace(/^\w/, c => c.toLowerCase())
91
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,126 @@
1
+ import {TypescriptGenerator} from "./TypescriptGenerator.js"
2
+ import {capitalize, FacadeDefinition, LangGenerator, Language, Platform, StructDefinition, TypeRefDefinition} from "./common.js"
3
+ import {SwiftGenerator} from "./SwiftGenerator.js"
4
+ import {KotlinGenerator} from "./KotlinGenerator.js"
5
+ import * as path from "path"
6
+ import * as fs from "fs"
7
+
8
+ function generatorForLang(lang: Language): LangGenerator {
9
+ switch (lang) {
10
+ case "typescript":
11
+ return new TypescriptGenerator()
12
+ case "swift":
13
+ return new SwiftGenerator()
14
+ case "kotlin":
15
+ return new KotlinGenerator()
16
+ default:
17
+ throw new Error("Unknown output language:" + lang)
18
+ }
19
+ }
20
+
21
+ function mapPlatformToLang(platform: string): Language {
22
+ switch (platform) {
23
+ case "ios":
24
+ return "swift"
25
+ case "android":
26
+ return "kotlin"
27
+ case "web":
28
+ case "desktop":
29
+ return "typescript"
30
+ default:
31
+ throw new Error("unknown platform " + platform)
32
+ }
33
+ }
34
+
35
+ /**
36
+ * generate and write all target language source files.
37
+ *
38
+ * @param platform one of the supported platform names
39
+ * @param sources a map from the definition file name to the definition json string
40
+ * @param outDir the directory the output files should be written to
41
+ */
42
+ export function generate(platform: Platform, sources: Map<string, string>, outDir: string) {
43
+ const lang = mapPlatformToLang(platform)
44
+ const ext = getFileExtensionForLang(lang)
45
+ const generator = generatorForLang(lang)
46
+ const facadesToImplement: Array<string> = []
47
+ for (const [inputPath, source] of Array.from(sources.entries())) {
48
+ console.log("handling ipc schema file", inputPath)
49
+ const definition = JSON.parse(source) as FacadeDefinition | StructDefinition | TypeRefDefinition
50
+ if (!("name" in definition)) {
51
+ throw new Error(`malformed definition: ${inputPath} doesn't have name field`)
52
+ }
53
+ if (!("type" in definition)) {
54
+ throw new Error(`missing type declaration: ${inputPath}`)
55
+ }
56
+ switch (definition.type) {
57
+ case "facade":
58
+ assertReturnTypesPresent(definition)
59
+ const isReceiving = definition.receivers.includes(platform)
60
+ const isSending = definition.senders.includes(platform)
61
+ if (!isReceiving && !isSending) {
62
+ continue
63
+ }
64
+ const facadeOutput = generator.generateFacade(definition)
65
+ write(facadeOutput, outDir, definition.name + ext)
66
+ if (isReceiving) {
67
+ const receiveOutput = generator.generateReceiveDispatcher(definition)
68
+ write(receiveOutput, outDir, definition.name + "ReceiveDispatcher" + ext)
69
+ facadesToImplement.push(definition.name)
70
+ }
71
+ if (isSending) {
72
+ const sendOutput = generator.generateSendDispatcher(definition)
73
+ write(sendOutput, outDir, definition.name + "SendDispatcher" + ext)
74
+ }
75
+ break
76
+ case "struct":
77
+ const structOutput = generator.handleStructDefinition(definition)
78
+ write(structOutput, outDir, definition.name + ext)
79
+ break
80
+ case "typeref":
81
+ const refOutput = generator.generateTypeRef(outDir, inputPath, definition)
82
+ if (refOutput != null) {
83
+ write(refOutput, outDir, definition.name + ext)
84
+ }
85
+ break
86
+ default:
87
+ throw new Error(`unknown definition type in ${inputPath}: ` + (definition as any).type)
88
+ }
89
+ }
90
+
91
+ const extraFiles = generator.generateExtraFiles()
92
+ for (let extraFilesKey in extraFiles) {
93
+ write(extraFiles[extraFilesKey], outDir, extraFilesKey + ext)
94
+ }
95
+
96
+ const dispatcherName = `${capitalize(platform)}GlobalDispatcher`
97
+ const dispatcherCode = generator.generateGlobalDispatcher(dispatcherName, facadesToImplement)
98
+ write(dispatcherCode, outDir, dispatcherName + ext)
99
+ }
100
+
101
+ function getFileExtensionForLang(lang: string): string {
102
+ switch (lang) {
103
+ case "typescript":
104
+ return ".ts"
105
+ case "swift":
106
+ return ".swift"
107
+ case "kotlin":
108
+ return ".kt"
109
+ default:
110
+ throw new Error("unknown output lang: " + lang)
111
+ }
112
+ }
113
+
114
+ function write(code: string, outDir: string, target: string) {
115
+ fs.mkdirSync(outDir, {recursive: true})
116
+ const filePath = path.join(outDir, target)
117
+ fs.writeFileSync(filePath, code)
118
+ console.log("written:", filePath)
119
+ }
120
+
121
+ function assertReturnTypesPresent(definition: FacadeDefinition): void {
122
+ const methNoRet = Object.entries(definition.methods).find(([_, {ret}]) => ret == null)
123
+ if (methNoRet) {
124
+ throw new Error(`missing return type on method ${methNoRet[0]} in ${definition.name}`)
125
+ }
126
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@tutao/licc",
3
+ "version": "3.98.2",
4
+ "bin": {
5
+ "licc": "dist/cli.js"
6
+ },
7
+ "description": "little interface compiler",
8
+ "main": "index.js",
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc -b",
12
+ "test": "rm -rf ./test/build && tsc -b test && cd test/build && node test/Suite.js",
13
+ "preinstall": "mkdir -p dist && touch dist/cli.js"
14
+ },
15
+ "author": "",
16
+ "license": "GPL-3.0",
17
+ "dependencies": {
18
+ "zx": "6.1.0",
19
+ "commander": "9.3.0"
20
+ },
21
+ "devDependencies": {
22
+ "@tutao/tutanota-test-utils": "3.98.2",
23
+ "ospec": "https://github.com/tutao/ospec.git#0472107629ede33be4c4d19e89f237a6d7b0cb11"
24
+ }
25
+ }
@@ -0,0 +1,124 @@
1
+ import o from "ospec"
2
+ import {ParsedType, parseType} from "../lib/Parser.js"
3
+ import {assertThrows} from "@tutao/tutanota-test-utils"
4
+
5
+ o.spec('Parser', function () {
6
+ o.spec("illegal identifiers are caught", function () {
7
+ o("empty string", async function () {
8
+ await assertThrows(Error, () => Promise.resolve(parseType("")))
9
+ })
10
+
11
+ o("number at start", async function () {
12
+ await assertThrows(Error, () => Promise.resolve(parseType("0hello")))
13
+ })
14
+
15
+ o("special character at start", async function () {
16
+ await assertThrows(Error, () => Promise.resolve(parseType("?some")))
17
+ })
18
+ })
19
+
20
+ o.spec("simple types are parsed correctly", function () {
21
+ o("string", async function () {
22
+ o(parseType("string")).deepEquals({baseName: "string", generics: [], nullable: false, external: false})
23
+ })
24
+ o("string?", async function () {
25
+ o(parseType("string?")).deepEquals({baseName: "string", generics: [], nullable: true, external: false})
26
+ })
27
+ o("external type", async function () {
28
+ o(parseType("SomeExternalType")).deepEquals({baseName: "SomeExternalType", external: true, generics: [], nullable: false})
29
+ })
30
+ o("nullable external", async function () {
31
+ o(parseType("SomeExternalType?")).deepEquals({baseName: "SomeExternalType", external: true, generics: [], nullable: true})
32
+ })
33
+
34
+ o("void", async function () {
35
+ o(parseType("void ")).deepEquals({baseName: "void", nullable: false, external: false, generics: []})
36
+ })
37
+ })
38
+
39
+ function stringToTest(typeString: string, expected: ParsedType) {
40
+ o(`"${typeString}"`, function () {
41
+ o(parseType(typeString)).deepEquals(expected)
42
+ })
43
+ }
44
+
45
+ o.spec("list type is parsed correctly", function () {
46
+ stringToTest("List<boolean>", {
47
+ baseName: "List",
48
+ generics: [{baseName: "boolean", nullable: false, generics: [], external: false}],
49
+ nullable: false,
50
+ external: false
51
+ })
52
+ stringToTest("List<string?>", {
53
+ baseName: "List",
54
+ generics: [{baseName: "string", nullable: true, generics: [], external: false}],
55
+ nullable: false,
56
+ external: false
57
+ })
58
+ stringToTest("List<number>?", {
59
+ baseName: "List",
60
+ generics: [{baseName: "number", nullable: false, generics: [], external: false}],
61
+ nullable: true,
62
+ external: false
63
+ })
64
+ stringToTest("List<List<External?>?>", {
65
+ baseName: "List",
66
+ generics: [{
67
+ baseName: "List", nullable: true, generics: [
68
+ {baseName: "External", nullable: true, external: true, generics: []}
69
+ ], external: false
70
+ }],
71
+ nullable: false,
72
+ external: false
73
+ })
74
+ stringToTest(" List< number > ", {
75
+ baseName: "List",
76
+ generics: [{baseName: "number", nullable: false, generics: [], external: false}],
77
+ nullable: false,
78
+ external: false
79
+ })
80
+ })
81
+
82
+ o.spec("map type is parsed correctly", function () {
83
+ stringToTest("Map<boolean?, Foo>", {
84
+ baseName: "Map", external: false, generics: [{
85
+ baseName: 'boolean',
86
+ generics: [],
87
+ external: false,
88
+ nullable: true
89
+ },
90
+ {
91
+ baseName: 'Foo',
92
+ generics: [],
93
+ external: true,
94
+ nullable: false
95
+ }], nullable: false
96
+ })
97
+
98
+ stringToTest("Map<number, number>?", {
99
+ baseName: "Map", external: false, generics: [
100
+ {baseName: "number", nullable: false, external: false, generics: []},
101
+ {baseName: "number", nullable: false, external: false, generics: []}
102
+ ], nullable: true
103
+ })
104
+
105
+ stringToTest("Map<string, string>", {
106
+ baseName: "Map", external: false, generics: [
107
+ {baseName: "string", nullable: false, external: false, generics: []},
108
+ {baseName: "string", nullable: false, external: false, generics: []}
109
+ ], nullable: false
110
+ })
111
+
112
+ stringToTest("Map<List<number>?, number?>", {
113
+ baseName: "Map", external: false, generics: [
114
+ {
115
+ baseName: "List",
116
+ generics: [{baseName: "number", nullable: false, generics: [], external: false}],
117
+ nullable: true,
118
+ external: false
119
+ },
120
+ {baseName: "number", nullable: true, external: false, generics: []}
121
+ ], nullable: false
122
+ })
123
+ })
124
+ })
package/test/Suite.ts ADDED
@@ -0,0 +1,6 @@
1
+ import o from "ospec"
2
+ import "./ParserTest.js"
3
+
4
+ (async function () {
5
+ await o.run()
6
+ })()