From 662cf7e7aaadfadc31b8ca1466986c3767b33609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Thu, 18 Apr 2024 22:31:23 +0800 Subject: [PATCH 1/4] Add support for default value in Module Interface --- demo/basic/generated/kotlin/IHtmlApi.kt | 9 +++++++++ demo/basic/generated/swift/IHtmlApi.swift | 14 ++++++++++++++ demo/basic/interfaces.ts | 12 ++++++++++++ documentation/interface-guide.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/parser/ValueParser.ts | 17 ++++++++++++++++- src/renderer/renderer.ts | 2 ++ .../value-transformer/KotlinValueTransformer.ts | 4 ++++ .../value-transformer/SwiftValueTransformer.ts | 4 ++++ .../value-transformer/ValueTransformer.ts | 1 + src/renderer/views/MethodView.ts | 14 ++++++++++++-- src/types.ts | 1 + 13 files changed, 91 insertions(+), 6 deletions(-) diff --git a/demo/basic/generated/kotlin/IHtmlApi.kt b/demo/basic/generated/kotlin/IHtmlApi.kt index 261d309..c633d2b 100644 --- a/demo/basic/generated/kotlin/IHtmlApi.kt +++ b/demo/basic/generated/kotlin/IHtmlApi.kt @@ -33,6 +33,7 @@ interface IHtmlApiBridge { fun getName(callback: Callback) fun getAge(gender: IHtmlApiGetAgeGender, callback: Callback) fun testDictionaryWithAnyKey(dict: Map) + fun testDefaultValue(bool: Boolean? = null, bool2: Boolean? = null, num: Float = 1) } open class IHtmlApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson, "htmlApi"), IHtmlApiBridge { @@ -86,6 +87,14 @@ open class IHtmlApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson "dict" to dict )) } + + override fun testDefaultValue(bool: Boolean? = null, bool2: Boolean? = null, num: Float = 1) { + executeJs("testDefaultValue", mapOf( + "bool" to bool + "bool2" to bool2 + "num" to num + )) + } } data class JSBaseSize( diff --git a/demo/basic/generated/swift/IHtmlApi.swift b/demo/basic/generated/swift/IHtmlApi.swift index 3fd72f5..237c24b 100644 --- a/demo/basic/generated/swift/IHtmlApi.swift +++ b/demo/basic/generated/swift/IHtmlApi.swift @@ -85,6 +85,20 @@ public class IHtmlApi { ) jsExecutor.execute(with: "htmlApi", feature: "testDictionaryWithAnyKey", args: args, completion: completion) } + + public func testDefaultValue(bool: Bool? = nil, bool2: Bool? = nil, num: Double = 1, completion: BridgeJSExecutor.Completion? = nil) { + struct Args: Encodable { + let bool: Bool? + let bool2: Bool? + let num: Double + } + let args = Args( + bool: bool, + bool2: bool2, + num: num + ) + jsExecutor.execute(with: "htmlApi", feature: "testDefaultValue", args: args, completion: completion) + } } public struct BaseSize: Codable { diff --git a/demo/basic/interfaces.ts b/demo/basic/interfaces.ts index a053f57..2f44560 100644 --- a/demo/basic/interfaces.ts +++ b/demo/basic/interfaces.ts @@ -76,6 +76,18 @@ export interface IHtmlApi { getName(): 'A2' | 'B2'; getAge({ gender }: { gender: 'Male' | 'Female' }): 21 | 22; testDictionaryWithAnyKey({ dict }: { dict: DictionaryWithAnyKey }): void; + + testDefaultValue(options: { + /** + * @default null + */ + bool?: boolean; + bool2?: boolean; + /** + * @default 1 + */ + num: number; + }): void; } /** diff --git a/documentation/interface-guide.md b/documentation/interface-guide.md index 19e94b7..27b74c4 100644 --- a/documentation/interface-guide.md +++ b/documentation/interface-guide.md @@ -216,6 +216,7 @@ ts-gyb parses tags in [JSDoc](https://jsdoc.app) documentation. - `@shouldExport`: Specify whether an `interface` should be exported. Set it to `true` to export. - `@overrideModuleName`: Change the name of the interface for ts-gyb. This is helpful for dropping the `I` prefix in TypeScript interface name. - `@overrideTypeName`: Similar to `@overrideModuleName`, this is used to override the name of custom types used in method parameters or return values. +- `@default`: default value for Module Interface's function parameter, ```typescript /** @@ -225,6 +226,18 @@ ts-gyb parses tags in [JSDoc](https://jsdoc.app) documentation. interface InterfaceWithTags { // The name of the module would be `ProperNamedInterface` in generated code ... + + foo(bar: { + /** + * @default null + */ + bool?: boolean; + bool2?: boolean; + /** + * @default 1 + */ + num: number; + }): void; } ``` diff --git a/package-lock.json b/package-lock.json index 312dff0..bc9f332 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ts-gyb", - "version": "0.10.1", + "version": "0.11.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ts-gyb", - "version": "0.10.1", + "version": "0.11.0", "license": "MIT", "dependencies": { "chalk": "^4.1.1", diff --git a/package.json b/package.json index 63d7db9..2235552 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-gyb", - "version": "0.10.1", + "version": "0.11.0", "description": "Generate Native API based on TS interface", "repository": { "type": "git", diff --git a/src/parser/ValueParser.ts b/src/parser/ValueParser.ts index c9cc4d5..ee50f3e 100644 --- a/src/parser/ValueParser.ts +++ b/src/parser/ValueParser.ts @@ -145,8 +145,9 @@ export class ValueParser { const documentation = ts.displayPartsToString(symbol?.getDocumentationComment(this.checker)); const staticValue = this.parseLiteralNode(node.type); + if (staticValue !== null) { - return { name, type: staticValue.type, staticValue: staticValue.value, documentation }; + return { name, type: staticValue.type, staticValue: staticValue.value, documentation, defaultValue: "" }; } if (node.type.kind === ts.SyntaxKind.NeverKeyword) { @@ -155,10 +156,24 @@ export class ValueParser { const valueType = this.valueTypeFromNode(node); + const jsDocTags = symbol?.getJsDocTags(this.checker); + let defaultValue = ""; + jsDocTags?.forEach((tag) => { + if (tag.name === 'default' && tag.text?.length === 1) { + if (tag.text[0].kind === 'text') { + defaultValue = tag.text[0].text; + } + } + }); + if (defaultValue.length === 0 && valueType.kind === ValueTypeKind.optionalType) { + defaultValue = "null"; + } + return { name, type: valueType, documentation, + defaultValue }; } diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index d17f4c0..306e7c9 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -8,5 +8,7 @@ export function renderCode(templatePath: string, view: View): string { return Mustache.render(template, view, (partialName) => { const partialPath = path.join(directory, `${partialName}.mustache`); return fs.readFileSync(partialPath).toString(); + }, { + escape: (value: string) => value, }); } diff --git a/src/renderer/value-transformer/KotlinValueTransformer.ts b/src/renderer/value-transformer/KotlinValueTransformer.ts index bb2b904..e8214fc 100644 --- a/src/renderer/value-transformer/KotlinValueTransformer.ts +++ b/src/renderer/value-transformer/KotlinValueTransformer.ts @@ -133,4 +133,8 @@ export class KotlinValueTransformer implements ValueTransformer { convertTypeNameFromCustomMap(name: string): string { return this.typeNameMap[name] ?? name; } + + null(): string { + return 'null'; + } } diff --git a/src/renderer/value-transformer/SwiftValueTransformer.ts b/src/renderer/value-transformer/SwiftValueTransformer.ts index 891af84..941e8e6 100644 --- a/src/renderer/value-transformer/SwiftValueTransformer.ts +++ b/src/renderer/value-transformer/SwiftValueTransformer.ts @@ -144,4 +144,8 @@ export class SwiftValueTransformer implements ValueTransformer { convertTypeNameFromCustomMap(name: string): string { return this.typeNameMap[name] ?? name; } + + null(): string { + return 'nil'; + } } diff --git a/src/renderer/value-transformer/ValueTransformer.ts b/src/renderer/value-transformer/ValueTransformer.ts index 6ced0b6..eebefc2 100644 --- a/src/renderer/value-transformer/ValueTransformer.ts +++ b/src/renderer/value-transformer/ValueTransformer.ts @@ -6,4 +6,5 @@ export interface ValueTransformer { convertValue(value: Value, type: ValueType): string; convertEnumKey(text: string): string; convertTypeNameFromCustomMap(name: string): string; + null(): string; } diff --git a/src/renderer/views/MethodView.ts b/src/renderer/views/MethodView.ts index bea827e..389099e 100644 --- a/src/renderer/views/MethodView.ts +++ b/src/renderer/views/MethodView.ts @@ -10,13 +10,23 @@ export class MethodView { } get parametersDeclaration(): string { - return this.parameters.map((parameter) => `${parameter.name}: ${parameter.type}`).join(', '); + return this.parameters.map((parameter) => { + if (parameter.defaultValue.length === 0) { + return `${parameter.name}: ${parameter.type}`; + } + let { defaultValue } = parameter; + if (defaultValue === 'null') { + defaultValue = this.valueTransformer.null(); + } + return `${parameter.name}: ${parameter.type} = ${defaultValue}`; + }).join(', '); } - get parameters(): { name: string; type: string; last: boolean }[] { + get parameters(): { name: string; type: string; defaultValue: string; last: boolean }[] { return this.method.parameters.map((parameter, index) => ({ name: parameter.name, type: this.valueTransformer.convertValueType(parameter.type), + defaultValue: parameter.defaultValue, last: index === this.method.parameters.length - 1, })); } diff --git a/src/types.ts b/src/types.ts index 8fb5724..2330736 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,6 +20,7 @@ export interface Field { type: ValueType; staticValue?: Value; documentation: string; + defaultValue: string; } export type ValueType = NonEmptyType | OptionalType; From 04a36cc425f801757769336eaf68a81d72cf3a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Mon, 22 Apr 2024 12:30:00 +0800 Subject: [PATCH 2/4] make defaultValue optional --- demo/basic/generated/kotlin/IHtmlApi.kt | 6 ++++-- demo/basic/generated/swift/IHtmlApi.swift | 8 ++++++-- demo/basic/interfaces.ts | 8 ++++++++ src/parser/ValueParser.ts | 16 ++++++++++------ src/renderer/views/MethodView.ts | 6 +++--- src/types.ts | 2 +- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/demo/basic/generated/kotlin/IHtmlApi.kt b/demo/basic/generated/kotlin/IHtmlApi.kt index c633d2b..6557405 100644 --- a/demo/basic/generated/kotlin/IHtmlApi.kt +++ b/demo/basic/generated/kotlin/IHtmlApi.kt @@ -33,7 +33,7 @@ interface IHtmlApiBridge { fun getName(callback: Callback) fun getAge(gender: IHtmlApiGetAgeGender, callback: Callback) fun testDictionaryWithAnyKey(dict: Map) - fun testDefaultValue(bool: Boolean? = null, bool2: Boolean? = null, num: Float = 1) + fun testDefaultValue(bool: Boolean? = null, bool2: Boolean? = null, bool3: Boolean = true, num: Float = 1, string: String = "hello") } open class IHtmlApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson, "htmlApi"), IHtmlApiBridge { @@ -88,11 +88,13 @@ open class IHtmlApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson )) } - override fun testDefaultValue(bool: Boolean? = null, bool2: Boolean? = null, num: Float = 1) { + override fun testDefaultValue(bool: Boolean? = null, bool2: Boolean? = null, bool3: Boolean = true, num: Float = 1, string: String = "hello") { executeJs("testDefaultValue", mapOf( "bool" to bool "bool2" to bool2 + "bool3" to bool3 "num" to num + "string" to string )) } } diff --git a/demo/basic/generated/swift/IHtmlApi.swift b/demo/basic/generated/swift/IHtmlApi.swift index 237c24b..4b1371a 100644 --- a/demo/basic/generated/swift/IHtmlApi.swift +++ b/demo/basic/generated/swift/IHtmlApi.swift @@ -86,16 +86,20 @@ public class IHtmlApi { jsExecutor.execute(with: "htmlApi", feature: "testDictionaryWithAnyKey", args: args, completion: completion) } - public func testDefaultValue(bool: Bool? = nil, bool2: Bool? = nil, num: Double = 1, completion: BridgeJSExecutor.Completion? = nil) { + public func testDefaultValue(bool: Bool? = nil, bool2: Bool? = nil, bool3: Bool = true, num: Double = 1, string: String = "hello", completion: BridgeJSExecutor.Completion? = nil) { struct Args: Encodable { let bool: Bool? let bool2: Bool? + let bool3: Bool let num: Double + let string: String } let args = Args( bool: bool, bool2: bool2, - num: num + bool3: bool3, + num: num, + string: string ) jsExecutor.execute(with: "htmlApi", feature: "testDefaultValue", args: args, completion: completion) } diff --git a/demo/basic/interfaces.ts b/demo/basic/interfaces.ts index 2f44560..b3caf09 100644 --- a/demo/basic/interfaces.ts +++ b/demo/basic/interfaces.ts @@ -83,10 +83,18 @@ export interface IHtmlApi { */ bool?: boolean; bool2?: boolean; + /** + * @default true + */ + bool3: boolean; /** * @default 1 */ num: number; + /** + * @default "hello" + */ + string: string; }): void; } diff --git a/src/parser/ValueParser.ts b/src/parser/ValueParser.ts index ee50f3e..60cd52d 100644 --- a/src/parser/ValueParser.ts +++ b/src/parser/ValueParser.ts @@ -147,7 +147,7 @@ export class ValueParser { const staticValue = this.parseLiteralNode(node.type); if (staticValue !== null) { - return { name, type: staticValue.type, staticValue: staticValue.value, documentation, defaultValue: "" }; + return { name, type: staticValue.type, staticValue: staticValue.value, documentation }; } if (node.type.kind === ts.SyntaxKind.NeverKeyword) { @@ -157,15 +157,19 @@ export class ValueParser { const valueType = this.valueTypeFromNode(node); const jsDocTags = symbol?.getJsDocTags(this.checker); - let defaultValue = ""; + let defaultValue: string | undefined; jsDocTags?.forEach((tag) => { - if (tag.name === 'default' && tag.text?.length === 1) { - if (tag.text[0].kind === 'text') { - defaultValue = tag.text[0].text; + if (tag.name === 'default') { + if (tag.text?.length !== 1 || tag.text[0].kind !== 'text' || tag.text[0].text.length === 0) { + throw new ValueParserError( + `Invalid default value for ${name}`, + 'Default value must be a single value, like `@default 0` or `@default "hello"`' + ); } + defaultValue = tag.text[0].text; } }); - if (defaultValue.length === 0 && valueType.kind === ValueTypeKind.optionalType) { + if ((defaultValue ?? "").length === 0 && valueType.kind === ValueTypeKind.optionalType) { defaultValue = "null"; } diff --git a/src/renderer/views/MethodView.ts b/src/renderer/views/MethodView.ts index 389099e..2c8e164 100644 --- a/src/renderer/views/MethodView.ts +++ b/src/renderer/views/MethodView.ts @@ -11,10 +11,10 @@ export class MethodView { get parametersDeclaration(): string { return this.parameters.map((parameter) => { - if (parameter.defaultValue.length === 0) { + let { defaultValue } = parameter; + if (defaultValue == null) { return `${parameter.name}: ${parameter.type}`; } - let { defaultValue } = parameter; if (defaultValue === 'null') { defaultValue = this.valueTransformer.null(); } @@ -22,7 +22,7 @@ export class MethodView { }).join(', '); } - get parameters(): { name: string; type: string; defaultValue: string; last: boolean }[] { + get parameters(): { name: string; type: string; defaultValue?: string; last: boolean }[] { return this.method.parameters.map((parameter, index) => ({ name: parameter.name, type: this.valueTransformer.convertValueType(parameter.type), diff --git a/src/types.ts b/src/types.ts index 2330736..f535e67 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,7 +20,7 @@ export interface Field { type: ValueType; staticValue?: Value; documentation: string; - defaultValue: string; + defaultValue?: string; } export type ValueType = NonEmptyType | OptionalType; From 3d78620abab5529b2be0a82fd0e247b07d0ead5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Mon, 22 Apr 2024 13:48:02 +0800 Subject: [PATCH 3/4] remove optional null --- demo/basic/generated/kotlin/IHtmlApi.kt | 4 ++-- demo/basic/generated/swift/IHtmlApi.swift | 2 +- src/parser/ValueParser.ts | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/demo/basic/generated/kotlin/IHtmlApi.kt b/demo/basic/generated/kotlin/IHtmlApi.kt index 6557405..cb24b7a 100644 --- a/demo/basic/generated/kotlin/IHtmlApi.kt +++ b/demo/basic/generated/kotlin/IHtmlApi.kt @@ -33,7 +33,7 @@ interface IHtmlApiBridge { fun getName(callback: Callback) fun getAge(gender: IHtmlApiGetAgeGender, callback: Callback) fun testDictionaryWithAnyKey(dict: Map) - fun testDefaultValue(bool: Boolean? = null, bool2: Boolean? = null, bool3: Boolean = true, num: Float = 1, string: String = "hello") + fun testDefaultValue(bool: Boolean? = null, bool2: Boolean?, bool3: Boolean = true, num: Float = 1, string: String = "hello") } open class IHtmlApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson, "htmlApi"), IHtmlApiBridge { @@ -88,7 +88,7 @@ open class IHtmlApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson )) } - override fun testDefaultValue(bool: Boolean? = null, bool2: Boolean? = null, bool3: Boolean = true, num: Float = 1, string: String = "hello") { + override fun testDefaultValue(bool: Boolean? = null, bool2: Boolean?, bool3: Boolean = true, num: Float = 1, string: String = "hello") { executeJs("testDefaultValue", mapOf( "bool" to bool "bool2" to bool2 diff --git a/demo/basic/generated/swift/IHtmlApi.swift b/demo/basic/generated/swift/IHtmlApi.swift index 4b1371a..5005136 100644 --- a/demo/basic/generated/swift/IHtmlApi.swift +++ b/demo/basic/generated/swift/IHtmlApi.swift @@ -86,7 +86,7 @@ public class IHtmlApi { jsExecutor.execute(with: "htmlApi", feature: "testDictionaryWithAnyKey", args: args, completion: completion) } - public func testDefaultValue(bool: Bool? = nil, bool2: Bool? = nil, bool3: Bool = true, num: Double = 1, string: String = "hello", completion: BridgeJSExecutor.Completion? = nil) { + public func testDefaultValue(bool: Bool? = nil, bool2: Bool?, bool3: Bool = true, num: Double = 1, string: String = "hello", completion: BridgeJSExecutor.Completion? = nil) { struct Args: Encodable { let bool: Bool? let bool2: Bool? diff --git a/src/parser/ValueParser.ts b/src/parser/ValueParser.ts index 60cd52d..deadfbf 100644 --- a/src/parser/ValueParser.ts +++ b/src/parser/ValueParser.ts @@ -169,9 +169,6 @@ export class ValueParser { defaultValue = tag.text[0].text; } }); - if ((defaultValue ?? "").length === 0 && valueType.kind === ValueTypeKind.optionalType) { - defaultValue = "null"; - } return { name, From 15b3008622382a93f99b9a6f9527bb1a7c7b8f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Tue, 23 Apr 2024 10:21:43 +0800 Subject: [PATCH 4/4] fix test fail --- src/parser/ValueParser.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/parser/ValueParser.ts b/src/parser/ValueParser.ts index deadfbf..298b074 100644 --- a/src/parser/ValueParser.ts +++ b/src/parser/ValueParser.ts @@ -170,12 +170,18 @@ export class ValueParser { } }); - return { + const field: Field = { name, type: valueType, - documentation, - defaultValue + documentation }; + + if (defaultValue != null) { + // to fix test fail + field.defaultValue = defaultValue; + } + + return field; } private parseTypeLiteralNode(typeNode: ts.TypeNode): TupleType | DictionaryType | null {