From cfdf7b4e998b93923c66c53d05d3b4a8b8fe3063 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, 9 Jan 2024 12:46:28 +0800 Subject: [PATCH] Add support for union to enum (#117) * Add support for union to enum * rename * reuse path for unique name & fix shared types is not in shared file * lint * fix: kotlin shared types is missing * fix: align usage with TupleType * style: rename members * fix: remove unused content * feature: add nullable check * fix: revert unused content * doc: update document * fix: remove kind from LiteralType * fix: remove type cast of namedType * fix: revert LiteralType from parseLiteralNode * fix: Add support for strings that look like numbers --- demo/basic/generated/kotlin/BridgeTypes.kt | 119 ++++++++++++++++ demo/basic/generated/kotlin/IHtmlApi.kt | 66 ++++----- .../basic/generated/kotlin/IImageOptionApi.kt | 5 + demo/basic/generated/swift/IHtmlApi.swift | 64 ++++----- .../generated/swift/IImageOptionApi.swift | 4 + demo/basic/generated/swift/SharedTypes.swift | 89 ++++++++++++ demo/basic/interfaces.ts | 12 +- .../kotlin-named-types.mustache | 5 + documentation/interface-guide.md | 8 +- example-templates/kotlin-named-types.mustache | 5 + src/generator/named-types.ts | 131 ++++++++++++------ src/parser/ValueParser.ts | 55 +++++++- .../KotlinValueTransformer.ts | 12 +- src/types.ts | 16 ++- src/utils.ts | 34 +++++ 15 files changed, 498 insertions(+), 127 deletions(-) create mode 100644 demo/basic/generated/kotlin/BridgeTypes.kt create mode 100644 demo/basic/generated/swift/SharedTypes.swift diff --git a/demo/basic/generated/kotlin/BridgeTypes.kt b/demo/basic/generated/kotlin/BridgeTypes.kt new file mode 100644 index 0000000..58d0c2d --- /dev/null +++ b/demo/basic/generated/kotlin/BridgeTypes.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2021. + * Microsoft Corporation. All rights reserved. + * + * + * This file is automatically generated + * Please DO NOT modify +*/ + +package com.microsoft.office.outlook.rooster.web.bridge + +import java.lang.reflect.Type +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import com.google.gson.annotations.SerializedName + + +data class OverriddenFullSize( + @JvmField val size: Float, + @JvmField val count: Int, + @JvmField val stringEnum: StringEnum, + @JvmField val numEnum: NumEnum, + @JvmField val defEnum: DefaultEnum, + @JvmField val stringUnion: OverriddenFullSizeMembersStringUnionType, + @JvmField val numberStringUnion: OverriddenFullSizeMembersNumberStringUnionType, + @JvmField val nullableStringUnion: OverriddenFullSizeMembersNullableStringUnionType?, + @JvmField val numUnion1: OverriddenFullSizeMembersNumUnion1Type, + @JvmField val foo: OverriddenFullSizeMembersFooType, + @JvmField val width: Float, + @JvmField val height: Float, + @JvmField val scale: Float, + @JvmField val member: NumEnum = NumEnum.ONE, +) + +enum class NumEnum(val value: Int) { + ONE(1), + TWO(2); + + companion object { + fun find(value: Int) = values().find { it.value == value } + } +} + +class NumEnumTypeAdapter : JsonSerializer, JsonDeserializer { + override fun serialize(obj: NumEnum, type: Type, context: JsonSerializationContext): JsonElement { + return JsonPrimitive(obj.value) + } + + override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): NumEnum? { + return NumEnum.find(json.asInt) + } +} + +enum class StringEnum { + @SerializedName("a") A, + @SerializedName("b") B +} + +enum class DefaultEnum(val value: Int) { + DEFAULT_VALUE_C(0), + DEFAULT_VALUE_D(1); + + companion object { + fun find(value: Int) = values().find { it.value == value } + } +} + +class DefaultEnumTypeAdapter : JsonSerializer, JsonDeserializer { + override fun serialize(obj: DefaultEnum, type: Type, context: JsonSerializationContext): JsonElement { + return JsonPrimitive(obj.value) + } + + override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): DefaultEnum? { + return DefaultEnum.find(json.asInt) + } +} + +enum class OverriddenFullSizeMembersStringUnionType { + @SerializedName("A1") A1, + @SerializedName("B1") B1 +} + +enum class OverriddenFullSizeMembersNumberStringUnionType { + @SerializedName("11") _11, + @SerializedName("21") _21 +} + +enum class OverriddenFullSizeMembersNullableStringUnionType { + @SerializedName("A1") A1, + @SerializedName("B1") B1 +} + +enum class OverriddenFullSizeMembersNumUnion1Type(val value: Int) { + _11(11), + _21(21); + + companion object { + fun find(value: Int) = values().find { it.value == value } + } +} + +class OverriddenFullSizeMembersNumUnion1TypeTypeAdapter : JsonSerializer, JsonDeserializer { + override fun serialize(obj: OverriddenFullSizeMembersNumUnion1Type, type: Type, context: JsonSerializationContext): JsonElement { + return JsonPrimitive(obj.value) + } + + override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): OverriddenFullSizeMembersNumUnion1Type? { + return OverriddenFullSizeMembersNumUnion1Type.find(json.asInt) + } +} + +data class OverriddenFullSizeMembersFooType( + @JvmField val stringField: String, + @JvmField val numberField: Float, +) diff --git a/demo/basic/generated/kotlin/IHtmlApi.kt b/demo/basic/generated/kotlin/IHtmlApi.kt index 72efcf1..261d309 100644 --- a/demo/basic/generated/kotlin/IHtmlApi.kt +++ b/demo/basic/generated/kotlin/IHtmlApi.kt @@ -30,6 +30,8 @@ interface IHtmlApiBridge { fun requestRenderingResult() fun getSize(callback: Callback) fun getAliasSize(callback: Callback) + fun getName(callback: Callback) + fun getAge(gender: IHtmlApiGetAgeGender, callback: Callback) fun testDictionaryWithAnyKey(dict: Map) } @@ -69,6 +71,16 @@ open class IHtmlApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson executeJsForResponse(JSBaseSize::class.java, "getAliasSize", callback) } + override fun getName(callback: Callback) { + executeJsForResponse(IHtmlApiGetNameReturnType::class.java, "getName", callback) + } + + override fun getAge(gender: IHtmlApiGetAgeGender, callback: Callback) { + executeJsForResponse(IHtmlApiGetAgeReturnType::class.java, "getAge", callback, mapOf( + "gender" to gender + )) + } + override fun testDictionaryWithAnyKey(dict: Map) { executeJs("testDictionaryWithAnyKey", mapOf( "dict" to dict @@ -76,62 +88,36 @@ open class IHtmlApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson } } -data class OverriddenFullSize( - @JvmField val size: Float, - @JvmField val count: Int, - @JvmField val stringEnum: StringEnum, - @JvmField val numEnum: NumEnum, - @JvmField val defEnum: DefaultEnum, +data class JSBaseSize( @JvmField val width: Float, @JvmField val height: Float, - @JvmField val scale: Float, - @JvmField val member: NumEnum = NumEnum.ONE, ) -enum class NumEnum(val value: Int) { - ONE(1), - TWO(2); - - companion object { - fun find(value: Int) = values().find { it.value == value } - } +enum class IHtmlApiGetNameReturnType { + @SerializedName("A2") A2, + @SerializedName("B2") B2 } -class NumEnumTypeAdapter : JsonSerializer, JsonDeserializer { - override fun serialize(obj: NumEnum, type: Type, context: JsonSerializationContext): JsonElement { - return JsonPrimitive(obj.value) - } - - override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): NumEnum? { - return NumEnum.find(json.asInt) - } -} - -enum class StringEnum { - @SerializedName("a") A, - @SerializedName("b") B +enum class IHtmlApiGetAgeGender { + @SerializedName("Male") MALE, + @SerializedName("Female") FEMALE } -enum class DefaultEnum(val value: Int) { - DEFAULT_VALUE_C(0), - DEFAULT_VALUE_D(1); +enum class IHtmlApiGetAgeReturnType(val value: Int) { + _21(21), + _22(22); companion object { fun find(value: Int) = values().find { it.value == value } } } -class DefaultEnumTypeAdapter : JsonSerializer, JsonDeserializer { - override fun serialize(obj: DefaultEnum, type: Type, context: JsonSerializationContext): JsonElement { +class IHtmlApiGetAgeReturnTypeTypeAdapter : JsonSerializer, JsonDeserializer { + override fun serialize(obj: IHtmlApiGetAgeReturnType, type: Type, context: JsonSerializationContext): JsonElement { return JsonPrimitive(obj.value) } - override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): DefaultEnum? { - return DefaultEnum.find(json.asInt) + override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): IHtmlApiGetAgeReturnType? { + return IHtmlApiGetAgeReturnType.find(json.asInt) } } - -data class JSBaseSize( - @JvmField val width: Float, - @JvmField val height: Float, -) diff --git a/demo/basic/generated/kotlin/IImageOptionApi.kt b/demo/basic/generated/kotlin/IImageOptionApi.kt index d34e12c..5eeb896 100644 --- a/demo/basic/generated/kotlin/IImageOptionApi.kt +++ b/demo/basic/generated/kotlin/IImageOptionApi.kt @@ -20,6 +20,7 @@ interface IImageOptionApiBridge { fun getSourceOfImageWithID(id: String, callback: Callback) fun getImageDataList(callback: Callback) fun getContentBoundsOfElementWithID(id: String, callback: Callback) + fun getSize(callback: Callback) } open class IImageOptionApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson, "imageOption"), IImageOptionApiBridge { @@ -51,4 +52,8 @@ open class IImageOptionApiBridge(editor: WebEditor, gson: Gson) : JsBridge(edito "id" to id )) } + + override fun getSize(callback: Callback) { + executeJsForResponse(OverriddenFullSize::class.java, "getSize", callback) + } } diff --git a/demo/basic/generated/swift/IHtmlApi.swift b/demo/basic/generated/swift/IHtmlApi.swift index 5a10aee..3fd72f5 100644 --- a/demo/basic/generated/swift/IHtmlApi.swift +++ b/demo/basic/generated/swift/IHtmlApi.swift @@ -62,6 +62,20 @@ public class IHtmlApi { jsExecutor.execute(with: "htmlApi", feature: "getAliasSize", args: nil, completion: completion) } + public func getName(completion: @escaping BridgeCompletion) { + jsExecutor.execute(with: "htmlApi", feature: "getName", args: nil, completion: completion) + } + + public func getAge(gender: IHtmlApiGetAgeGender, completion: @escaping BridgeCompletion) { + struct Args: Encodable { + let gender: IHtmlApiGetAgeGender + } + let args = Args( + gender: gender + ) + jsExecutor.execute(with: "htmlApi", feature: "getAge", args: args, completion: completion) + } + public func testDictionaryWithAnyKey(dict: [String: String], completion: BridgeJSExecutor.Completion? = nil) { struct Args: Encodable { let dict: [String: String] @@ -73,53 +87,27 @@ public class IHtmlApi { } } -/// Example documentation for interface -public struct OverriddenFullSize: Codable { - public var size: Double - public var count: Int - public var stringEnum: StringEnum - public var numEnum: NumEnum - public var defEnum: DefaultEnum +public struct BaseSize: Codable { public var width: Double public var height: Double - public var scale: Double - /// Example documentation for member - private var member: NumEnum = .one - - public init(size: Double, count: Int, stringEnum: StringEnum, numEnum: NumEnum, defEnum: DefaultEnum, width: Double, height: Double, scale: Double) { - self.size = size - self.count = count - self.stringEnum = stringEnum - self.numEnum = numEnum - self.defEnum = defEnum + + public init(width: Double, height: Double) { self.width = width self.height = height - self.scale = scale } } -public enum NumEnum: Int, Codable { - case one = 1 - case two = 2 +public enum IHtmlApiGetNameReturnType: String, Codable { + case a2 = "A2" + case b2 = "B2" } -public enum StringEnum: String, Codable { - /// Description for enum member a - case a = "a" - case b = "b" +public enum IHtmlApiGetAgeGender: String, Codable { + case male = "Male" + case female = "Female" } -public enum DefaultEnum: Int, Codable { - case defaultValueC = 0 - case defaultValueD = 1 -} - -public struct BaseSize: Codable { - public var width: Double - public var height: Double - - public init(width: Double, height: Double) { - self.width = width - self.height = height - } +public enum IHtmlApiGetAgeReturnType: Int, Codable { + case _21 = 21 + case _22 = 22 } diff --git a/demo/basic/generated/swift/IImageOptionApi.swift b/demo/basic/generated/swift/IImageOptionApi.swift index a662cb7..4ee2551 100644 --- a/demo/basic/generated/swift/IImageOptionApi.swift +++ b/demo/basic/generated/swift/IImageOptionApi.swift @@ -55,4 +55,8 @@ public class IImageOptionApi { ) jsExecutor.execute(with: "imageOption", feature: "getContentBoundsOfElementWithID", args: args, completion: completion) } + + public func getSize(completion: @escaping BridgeCompletion) { + jsExecutor.execute(with: "imageOption", feature: "getSize", args: nil, completion: completion) + } } diff --git a/demo/basic/generated/swift/SharedTypes.swift b/demo/basic/generated/swift/SharedTypes.swift new file mode 100644 index 0000000..1c329a0 --- /dev/null +++ b/demo/basic/generated/swift/SharedTypes.swift @@ -0,0 +1,89 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// + +// swiftformat:disable redundantRawValues +// Don't modify this file manually, it's auto generated. + +import UIKit + +/// Example documentation for interface +public struct OverriddenFullSize: Codable { + public var size: Double + public var count: Int + public var stringEnum: StringEnum + public var numEnum: NumEnum + public var defEnum: DefaultEnum + public var stringUnion: OverriddenFullSizeMembersStringUnionType + public var numberStringUnion: OverriddenFullSizeMembersNumberStringUnionType + public var nullableStringUnion: OverriddenFullSizeMembersNullableStringUnionType? + public var numUnion1: OverriddenFullSizeMembersNumUnion1Type + public var foo: OverriddenFullSizeMembersFooType + public var width: Double + public var height: Double + public var scale: Double + /// Example documentation for member + private var member: NumEnum = .one + + public init(size: Double, count: Int, stringEnum: StringEnum, numEnum: NumEnum, defEnum: DefaultEnum, stringUnion: OverriddenFullSizeMembersStringUnionType, numberStringUnion: OverriddenFullSizeMembersNumberStringUnionType, nullableStringUnion: OverriddenFullSizeMembersNullableStringUnionType?, numUnion1: OverriddenFullSizeMembersNumUnion1Type, foo: OverriddenFullSizeMembersFooType, width: Double, height: Double, scale: Double) { + self.size = size + self.count = count + self.stringEnum = stringEnum + self.numEnum = numEnum + self.defEnum = defEnum + self.stringUnion = stringUnion + self.numberStringUnion = numberStringUnion + self.nullableStringUnion = nullableStringUnion + self.numUnion1 = numUnion1 + self.foo = foo + self.width = width + self.height = height + self.scale = scale + } +} + +public enum NumEnum: Int, Codable { + case one = 1 + case two = 2 +} + +public enum StringEnum: String, Codable { + /// Description for enum member a + case a = "a" + case b = "b" +} + +public enum DefaultEnum: Int, Codable { + case defaultValueC = 0 + case defaultValueD = 1 +} + +public enum OverriddenFullSizeMembersStringUnionType: String, Codable { + case a1 = "A1" + case b1 = "B1" +} + +public enum OverriddenFullSizeMembersNumberStringUnionType: String, Codable { + case _11 = "11" + case _21 = "21" +} + +public enum OverriddenFullSizeMembersNullableStringUnionType: String, Codable { + case a1 = "A1" + case b1 = "B1" +} + +public enum OverriddenFullSizeMembersNumUnion1Type: Int, Codable { + case _11 = 11 + case _21 = 21 +} + +public struct OverriddenFullSizeMembersFooType: Codable { + public var stringField: String + public var numberField: Double + + public init(stringField: String, numberField: Double) { + self.stringField = stringField + self.numberField = numberField + } +} diff --git a/demo/basic/interfaces.ts b/demo/basic/interfaces.ts index b08689a..a053f57 100644 --- a/demo/basic/interfaces.ts +++ b/demo/basic/interfaces.ts @@ -36,14 +36,19 @@ enum DefaultEnum { */ interface FullSize extends BaseSize, CustomSize { /** - * Example documentation for member - */ + * Example documentation for member + */ member: NumEnum.one; size: number; count: CodeGen_Int; stringEnum: StringEnum; numEnum: NumEnum; defEnum: DefaultEnum; + stringUnion: 'A1' | 'B1'; + numberStringUnion: '11' | '21'; + nullableStringUnion: 'A1' | 'B1' | null; + numUnion1: 11 | 21; + foo: { stringField: string } | { numberField: number }; } interface DictionaryWithAnyKey { @@ -68,6 +73,8 @@ export interface IHtmlApi { requestRenderingResult(): void; getSize(): FullSize; getAliasSize(): AliasSize; + getName(): 'A2' | 'B2'; + getAge({ gender }: { gender: 'Male' | 'Female' }): 21 | 22; testDictionaryWithAnyKey({ dict }: { dict: DictionaryWithAnyKey }): void; } @@ -81,4 +88,5 @@ export interface IImageOptionApi { getSourceOfImageWithID({ id }: { id: string }): string | null; getImageDataList(): string; getContentBoundsOfElementWithID({ id }: { id: string }): string | null; + getSize(): FullSize; } diff --git a/demo/mini-editor/web/code-templates/kotlin-named-types.mustache b/demo/mini-editor/web/code-templates/kotlin-named-types.mustache index 0ed6e2e..c516c9c 100644 --- a/demo/mini-editor/web/code-templates/kotlin-named-types.mustache +++ b/demo/mini-editor/web/code-templates/kotlin-named-types.mustache @@ -17,3 +17,8 @@ import com.google.gson.JsonPrimitive import com.google.gson.JsonSerializationContext import com.google.gson.JsonSerializer import com.google.gson.annotations.SerializedName + +{{#.}} + +{{> kotlin-named-type}} +{{/.}} diff --git a/documentation/interface-guide.md b/documentation/interface-guide.md index 044234d..19e94b7 100644 --- a/documentation/interface-guide.md +++ b/documentation/interface-guide.md @@ -146,7 +146,7 @@ Arries defined like `string[]` and `Array` are supported. The element ca The support for union types is limited. Only these scenrios are supported: - Any supported value type union with `null` or/and `undefined` to specify optional type -- Union two interfaces or object literals to a new object literal +- Union two interfaces or object literals to a new object literal or union values of the same literal type to enum in the target language, e.g. string, number. - Combination of the above two cases Any other union types would result in an error. @@ -156,6 +156,12 @@ Any other union types would result in an error. string | null string | undefined string | null | undefined +'1' | '2' | null; + +// allowed: literal values +'1' | '2'; +1 | 2; + interface StringFieldInterface { stringField: string; diff --git a/example-templates/kotlin-named-types.mustache b/example-templates/kotlin-named-types.mustache index 0ed6e2e..c516c9c 100644 --- a/example-templates/kotlin-named-types.mustache +++ b/example-templates/kotlin-named-types.mustache @@ -17,3 +17,8 @@ import com.google.gson.JsonPrimitive import com.google.gson.JsonSerializationContext import com.google.gson.JsonSerializer import com.google.gson.annotations.SerializedName + +{{#.}} + +{{> kotlin-named-type}} +{{/.}} diff --git a/src/generator/named-types.ts b/src/generator/named-types.ts index 61b8ced..8c383cf 100644 --- a/src/generator/named-types.ts +++ b/src/generator/named-types.ts @@ -1,4 +1,10 @@ -import { capitalize } from '../utils'; +import { + basicTypeOfUnion, + membersOfUnion, + uniquePathWithMember, + uniquePathWithMethodParameter, + uniquePathWithMethodReturnType, +} from '../utils'; import { isArraryType, isInterfaceType, @@ -12,6 +18,9 @@ import { TupleType, isTupleType, ValueTypeKind, + isUnionType, + EnumSubType, + UnionType, } from '../types'; export const enum ValueTypeSource { @@ -40,14 +49,18 @@ export type NamedTypesResult = { associatedTypes: Record fetchRootTypes(module)) - .forEach(([valueType]) => { - recursiveVisitMembersType(valueType, (namedType) => { - if (!isInterfaceType(namedType)) { - return; - } - - namedType.name = namedType.name?.replace(/^I/, ''); - }); + .forEach(({ valueType, uniquePath }) => { + recursiveVisitMembersType( + valueType, + (namedType) => { + if (!isInterfaceType(namedType)) { + return; + } + + namedType.name = namedType.name?.replace(/^I/, ''); + }, + uniquePath + ); }); } @@ -97,25 +110,40 @@ function fetchNamedTypes(modules: Module[]): NamedTypesResult { const typeMap: Record }> = {}; modules.forEach((module) => { - fetchRootTypes(module).forEach(([valueType, source]) => { - recursiveVisitMembersType(valueType, (membersType, path) => { - let namedType = membersType; - if (isTupleType(namedType)) { - namedType = membersType as unknown as InterfaceType; - namedType.kind = ValueTypeKind.interfaceType; - namedType.name = path; - namedType.documentation = ''; - namedType.customTags = {}; - } - - if (typeMap[namedType.name] === undefined) { - typeMap[namedType.name] = { namedType, source, associatedModules: new Set() }; - } - - const existingResult = typeMap[namedType.name]; - existingResult.associatedModules.add(module.name); - existingResult.source |= source; - }); + fetchRootTypes(module).forEach(({ valueType, source, uniquePath }) => { + recursiveVisitMembersType( + valueType, + (membersType, path) => { + let namedType = membersType; + if (isTupleType(namedType)) { + namedType = membersType as unknown as InterfaceType; + namedType.kind = ValueTypeKind.interfaceType; + namedType.name = path; + namedType.documentation = ''; + namedType.customTags = {}; + } else if (isUnionType(namedType)) { + const subType = basicTypeOfUnion(namedType); + const members = membersOfUnion(namedType); + + namedType = membersType as unknown as EnumType; + namedType.kind = ValueTypeKind.enumType; + namedType.name = path; + namedType.subType = subType === 'number' ? EnumSubType.number : EnumSubType.string; + namedType.members = members; + namedType.documentation = ''; + namedType.customTags = {}; + } + + if (typeMap[namedType.name] === undefined) { + typeMap[namedType.name] = { namedType, source, associatedModules: new Set() }; + } + + const existingResult = typeMap[namedType.name]; + existingResult.associatedModules.add(module.name); + existingResult.source |= source; + }, + uniquePath + ); }); }); @@ -138,15 +166,30 @@ function fetchNamedTypes(modules: Module[]): NamedTypesResult { return { associatedTypes, sharedTypes }; } -function fetchRootTypes(module: Module): [ValueType, ValueTypeSource][] { - const typesInMembers: [ValueType, ValueTypeSource][] = module.members.map((field) => [ - field.type, - ValueTypeSource.Field, - ]); - const typesInMethods: [ValueType, ValueTypeSource][] = module.methods.flatMap((method) => +function fetchRootTypes(module: Module): { valueType: ValueType; source: ValueTypeSource; uniquePath: string }[] { + const typesInMembers: ReturnType = module.members.map((field) => ({ + valueType: field.type, + source: ValueTypeSource.Field, + uniquePath: uniquePathWithMember(module.name, field.name), + })); + const typesInMethods: ReturnType = module.methods.flatMap((method) => method.parameters - .map((parameter): [ValueType, ValueTypeSource] => [parameter.type, ValueTypeSource.Parameter]) - .concat(method.returnType ? [[method.returnType, ValueTypeSource.Return]] : []) + .map((parameter) => ({ + valueType: parameter.type, + source: ValueTypeSource.Parameter, + uniquePath: uniquePathWithMethodParameter(module.name, method.name, parameter.name), + })) + .concat( + method.returnType + ? [ + { + valueType: method.returnType, + source: ValueTypeSource.Return, + uniquePath: uniquePathWithMethodReturnType(module.name, method.name), + }, + ] + : [] + ) ); return typesInMembers.concat(typesInMethods); @@ -154,14 +197,14 @@ function fetchRootTypes(module: Module): [ValueType, ValueTypeSource][] { function recursiveVisitMembersType( valueType: ValueType, - visit: (membersType: NamedType | TupleType, path: string) => void, - path = '' + visit: (membersType: NamedType | TupleType | UnionType, path: string) => void, + path: string ): void { if (isInterfaceType(valueType)) { visit(valueType, path); valueType.members.forEach((member) => { - recursiveVisitMembersType(member.type, visit, `${path}${valueType.name}Members${capitalize(member.name)}Type`); + recursiveVisitMembersType(member.type, visit, uniquePathWithMember(valueType.name, member.name)); }); return; @@ -171,7 +214,7 @@ function recursiveVisitMembersType( visit(valueType, path); valueType.members.forEach((member) => { - recursiveVisitMembersType(member.type, visit, `${path}Members${capitalize(member.name)}Type`); + recursiveVisitMembersType(member.type, visit, uniquePathWithMember(path, member.name)); }); return; @@ -194,5 +237,13 @@ function recursiveVisitMembersType( if (isOptionalType(valueType)) { recursiveVisitMembersType(valueType.wrappedType, visit, `${path}`); + return; } + + if (isUnionType(valueType)) { + visit(valueType, path); + return; + } + + console.log(`Unhandled value type ${JSON.stringify(valueType)}`); } diff --git a/src/parser/ValueParser.ts b/src/parser/ValueParser.ts index 682c325..c9cc4d5 100644 --- a/src/parser/ValueParser.ts +++ b/src/parser/ValueParser.ts @@ -16,11 +16,14 @@ import { DictionaryKeyType, isInterfaceType, PredefinedType, - Value, TupleType, isTupleType, EnumField, isBasicType, + UnionType, + OptionalType, + Value, + UnionLiteralType, } from '../types'; import { isUndefinedOrNull, parseTypeJSDocTags } from './utils'; import { ParserLogger } from '../logger/ParserLogger'; @@ -243,12 +246,30 @@ export class ValueParser { let nullable = false; let valueType: ValueType | undefined; + const literalValues: { + type: BasicTypeValue.string | BasicTypeValue.number; + value: Value; + }[] = []; node.types.forEach((typeNode) => { if (isUndefinedOrNull(typeNode)) { nullable = true; return; } + const literalKind = this.parseLiteralNode(typeNode); + if (literalKind !== null) { + if ( + literalKind.type.kind === ValueTypeKind.basicType && + (literalKind.type.value === BasicTypeValue.string || literalKind.type.value === BasicTypeValue.number) + ) { + literalValues.push({ + type: literalKind.type.value, + value: literalKind.value, + }); + return; + } + return; + } const newValueType = this.valueTypeFromTypeNode(typeNode); @@ -276,6 +297,38 @@ export class ValueParser { }; }); + if (literalValues.length > 0) { + const kindSet = new Set(literalValues.map((obj) => obj.type)); + + if (kindSet.size !== 1) { + throw new ValueParserError( + `union type ${node.getText()} has multiple literal type`, + 'Union type must contain only one supported literal type' + ); + } + const members: UnionLiteralType[] = []; + literalValues.forEach((obj) => { + if (typeof obj.value === 'string') { + members.push(obj.value); + } + if (typeof obj.value === 'number') { + members.push(obj.value); + } + }); + const unionKind: UnionType = { + kind: ValueTypeKind.unionType, + memberType: literalValues[0].type, + members, + }; + if (nullable) { + const optionalType: OptionalType = { + kind: ValueTypeKind.optionalType, + wrappedType: unionKind, + }; + return optionalType; + } + return unionKind; + } if (!valueType) { throw new ValueParserError( `union type ${node.getText()} is invalid`, diff --git a/src/renderer/value-transformer/KotlinValueTransformer.ts b/src/renderer/value-transformer/KotlinValueTransformer.ts index f32f47f..bb2b904 100644 --- a/src/renderer/value-transformer/KotlinValueTransformer.ts +++ b/src/renderer/value-transformer/KotlinValueTransformer.ts @@ -120,10 +120,14 @@ export class KotlinValueTransformer implements ValueTransformer { } convertEnumKey(text: string): string { - return text - .replace(/\.?([A-Z]+)/g, (_, p1: string) => `_${p1}`) - .replace(/^_/, '') - .toUpperCase(); + let result = text.replace(/\.?([A-Z]+)/g, (_, p1: string) => `_${p1}`); + + const testText = result.replace(/^_/, ''); + if (Number.isNaN(Number(testText))) { + result = testText; + } + + return result.toUpperCase(); } convertTypeNameFromCustomMap(name: string): string { diff --git a/src/types.ts b/src/types.ts index 2db2fdc..8fb5724 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,7 +30,8 @@ export type NonEmptyType = | EnumType | ArrayType | DictionaryType - | PredefinedType; + | PredefinedType + | UnionType; export enum ValueTypeKind { basicType = 'basicType', @@ -41,6 +42,7 @@ export enum ValueTypeKind { dictionaryType = 'dictionaryType', optionalType = 'optionalType', predefinedType = 'predefinedType', + unionType = 'unionType', } interface BaseValueType { @@ -113,6 +115,14 @@ export interface PredefinedType extends BaseValueType { name: string; } +export type UnionLiteralType = string | number; + +export interface UnionType extends BaseValueType { + kind: ValueTypeKind.unionType; + memberType: BasicTypeValue.string | BasicTypeValue.number; + members: UnionLiteralType[]; +} + export function isBasicType(valueType: ValueType): valueType is BasicType { return valueType.kind === ValueTypeKind.basicType; } @@ -145,6 +155,10 @@ export function isPredefinedType(valueType: ValueType): valueType is PredefinedT return valueType.kind === ValueTypeKind.predefinedType; } +export function isUnionType(valueType: ValueType): valueType is UnionType { + return valueType.kind === ValueTypeKind.unionType; +} + // TODO: Define these types to support recursive definition type BaseValue = string | number | boolean | Record | null; export type Value = BaseValue | BaseValue[] | Record; diff --git a/src/utils.ts b/src/utils.ts index 4377ab6..bda2a3d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import path from 'path'; +import { BasicTypeValue, EnumField, UnionType } from './types'; export function capitalize(text: string): string { if (text.length === 0) { @@ -23,3 +24,36 @@ export function normalizePath(currentPath: string, basePath: string): string { const result = path.join(basePath, currentPath); return result; } + +export function uniquePathWithMember(ownerName: string, memberName: string): string { + return `${capitalize(ownerName)}Members${capitalize(memberName)}Type`; +} + +export function uniquePathWithMethodParameter(ownerName: string, methodName: string, parameterName: string): string { + return `${capitalize(ownerName)}${capitalize(methodName)}${capitalize(parameterName)}`; +} + +export function uniquePathWithMethodReturnType(ownerName: string, methodName: string): string { + return `${capitalize(ownerName)}${capitalize(methodName)}ReturnType`; +} + +export function basicTypeOfUnion(union: UnionType): BasicTypeValue { + return union.memberType; +} + +export function membersOfUnion(union: UnionType): EnumField[] { + const result: EnumField[] = []; + union.members.forEach((value) => { + let key = `${value}`; + if (!Number.isNaN(Number(value))) { + key = `_${key}`; + } + const enumField: EnumField = { + key, + value, + documentation: '', + }; + result.push(enumField); + }); + return result; +}