diff --git a/demo/basic/generated/kotlin/BridgeTypes.kt b/demo/basic/generated/kotlin/BridgeTypes.kt index 58d0c2d..3d4fdbc 100644 --- a/demo/basic/generated/kotlin/BridgeTypes.kt +++ b/demo/basic/generated/kotlin/BridgeTypes.kt @@ -30,6 +30,7 @@ data class OverriddenFullSize( @JvmField val nullableStringUnion: OverriddenFullSizeMembersNullableStringUnionType?, @JvmField val numUnion1: OverriddenFullSizeMembersNumUnion1Type, @JvmField val foo: OverriddenFullSizeMembersFooType, + @JvmField val unionType: OverriddenFullSizeMembersUnionTypeType, @JvmField val width: Float, @JvmField val height: Float, @JvmField val scale: Float, @@ -117,3 +118,59 @@ data class OverriddenFullSizeMembersFooType( @JvmField val stringField: String, @JvmField val numberField: Float, ) + +sealed class OverriddenFullSizeMembersUnionTypeType(val value: Any) { + data class NumEnumValue(val value: NumEnum) : OverriddenFullSizeMembersUnionTypeType() + data class DefaultEnumValue(val value: DefaultEnum) : OverriddenFullSizeMembersUnionTypeType() + data class StringArrayValue(val value: Array) : OverriddenFullSizeMembersUnionTypeType() + data class StringForStringDictionaryValue(val value: Map) : OverriddenFullSizeMembersUnionTypeType() + data class BooleanValue(val value: Boolean) : OverriddenFullSizeMembersUnionTypeType() + data class FloatValue(val value: Float) : OverriddenFullSizeMembersUnionTypeType() + data class StringValue(val value: String) : OverriddenFullSizeMembersUnionTypeType() +} + +class OverriddenFullSizeMembersUnionTypeTypeAdapter : JsonSerializer, JsonDeserializer { + override fun serialize(src: OverriddenFullSizeMembersUnionTypeType, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { + return context.serialize(src.value) + } + + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): OverriddenFullSizeMembersUnionTypeType { + try { + return OverriddenFullSizeMembersUnionTypeType.NumEnumValue(context.deserialize(json, NumEnum::class.java)) + } catch (e: Exception) { + // Ignore the exception and try the next type + } + try { + return OverriddenFullSizeMembersUnionTypeType.DefaultEnumValue(context.deserialize(json, DefaultEnum::class.java)) + } catch (e: Exception) { + // Ignore the exception and try the next type + } + try { + return OverriddenFullSizeMembersUnionTypeType.StringArrayValue(context.deserialize(json, Array::class.java)) + } catch (e: Exception) { + // Ignore the exception and try the next type + } + try { + return OverriddenFullSizeMembersUnionTypeType.StringForStringDictionaryValue(context.deserialize(json, Map::class.java)) + } catch (e: Exception) { + // Ignore the exception and try the next type + } + try { + return OverriddenFullSizeMembersUnionTypeType.BooleanValue(context.deserialize(json, Boolean::class.java)) + } catch (e: Exception) { + // Ignore the exception and try the next type + } + try { + return OverriddenFullSizeMembersUnionTypeType.FloatValue(context.deserialize(json, Float::class.java)) + } catch (e: Exception) { + // Ignore the exception and try the next type + } + try { + return OverriddenFullSizeMembersUnionTypeType.StringValue(context.deserialize(json, String::class.java)) + } catch (e: Exception) { + // Ignore the exception and try the next type + } + + throw IllegalArgumentException("Unexpected JSON type: ${json.javaClass}") + } +} diff --git a/demo/basic/generated/kotlin/IHtmlApi.kt b/demo/basic/generated/kotlin/IHtmlApi.kt index 7ca0177..33d6544 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?, bool3: Boolean = true, num: Float = 1, string: String = "hello", callback: Callback) + fun testDefaultValue(bool: Boolean? = null, bool2: Boolean?, bool3: Boolean = true, num: Float = 1, string: String = "hello", callback: Callback) } open class IHtmlApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson, "htmlApi"), IHtmlApiBridge { @@ -88,8 +88,8 @@ open class IHtmlApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson )) } - override fun testDefaultValue(bool: Boolean? = null, bool2: Boolean?, bool3: Boolean = true, num: Float = 1, string: String = "hello", callback: Callback) { - executeJsForResponse(nterfaceWithDefeaultValue::class.java, "testDefaultValue", callback, mapOf( + override fun testDefaultValue(bool: Boolean? = null, bool2: Boolean?, bool3: Boolean = true, num: Float = 1, string: String = "hello", callback: Callback) { + executeJsForResponse(ObjectWithDefeaultValue::class.java, "testDefaultValue", callback, mapOf( "bool" to bool "bool2" to bool2 "bool3" to bool3 @@ -133,6 +133,6 @@ class IHtmlApiGetAgeReturnTypeTypeAdapter : JsonSerializer) { + public func testDefaultValue(bool: Bool? = nil, bool2: Bool?, bool3: Bool = true, num: Double = 1, string: String = "hello", completion: @escaping BridgeCompletion) { struct Args: Encodable { let bool: Bool? let bool2: Bool? @@ -130,7 +130,7 @@ public enum IHtmlApiGetAgeReturnType: Int, Codable { case _22 = 22 } -public struct nterfaceWithDefeaultValue: Codable { +public struct ObjectWithDefeaultValue: Codable { public var defaultValue: Bool? public init(defaultValue: Bool? = true) { diff --git a/demo/basic/generated/swift/SharedTypes.swift b/demo/basic/generated/swift/SharedTypes.swift index 1c329a0..ebdbc22 100644 --- a/demo/basic/generated/swift/SharedTypes.swift +++ b/demo/basic/generated/swift/SharedTypes.swift @@ -19,13 +19,14 @@ public struct OverriddenFullSize: Codable { public var nullableStringUnion: OverriddenFullSizeMembersNullableStringUnionType? public var numUnion1: OverriddenFullSizeMembersNumUnion1Type public var foo: OverriddenFullSizeMembersFooType + public var unionType: OverriddenFullSizeMembersUnionTypeType 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) { + public init(size: Double, count: Int, stringEnum: StringEnum, numEnum: NumEnum, defEnum: DefaultEnum, stringUnion: OverriddenFullSizeMembersStringUnionType, numberStringUnion: OverriddenFullSizeMembersNumberStringUnionType, nullableStringUnion: OverriddenFullSizeMembersNullableStringUnionType?, numUnion1: OverriddenFullSizeMembersNumUnion1Type, foo: OverriddenFullSizeMembersFooType, unionType: OverriddenFullSizeMembersUnionTypeType, width: Double, height: Double, scale: Double) { self.size = size self.count = count self.stringEnum = stringEnum @@ -36,6 +37,7 @@ public struct OverriddenFullSize: Codable { self.nullableStringUnion = nullableStringUnion self.numUnion1 = numUnion1 self.foo = foo + self.unionType = unionType self.width = width self.height = height self.scale = scale @@ -87,3 +89,59 @@ public struct OverriddenFullSizeMembersFooType: Codable { self.numberField = numberField } } + +public enum OverriddenFullSizeMembersUnionTypeType: Codable { + case numEnum(_ value: NumEnum) + case defaultEnum(_ value: DefaultEnum) + case stringArray(_ value: [String]) + case stringForStringDictionary(_ value: [String: String]) + case bool(_ value: Bool) + case double(_ value: Double) + case string(_ value: String) + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + if let value = try? container.decode(NumEnum.self) { + self = .numEnum(value) + } + else if let value = try? container.decode(DefaultEnum.self) { + self = .defaultEnum(value) + } + else if let value = try? container.decode([String].self) { + self = .stringArray(value) + } + else if let value = try? container.decode([String: String].self) { + self = .stringForStringDictionary(value) + } + else if let value = try? container.decode(Bool.self) { + self = .bool(value) + } + else if let value = try? container.decode(Double.self) { + self = .double(value) + } + else { + let value = try container.decode(String.self) + self = .string(value) + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .numEnum(let value): + try container.encode(value) + case .defaultEnum(let value): + try container.encode(value) + case .stringArray(let value): + try container.encode(value) + case .stringForStringDictionary(let value): + try container.encode(value) + case .bool(let value): + try container.encode(value) + case .double(let value): + try container.encode(value) + case .string(let value): + try container.encode(value) + } + } +} diff --git a/demo/basic/interfaces.ts b/demo/basic/interfaces.ts index ba66c75..630fc08 100644 --- a/demo/basic/interfaces.ts +++ b/demo/basic/interfaces.ts @@ -49,6 +49,7 @@ interface FullSize extends BaseSize, CustomSize { nullableStringUnion: 'A1' | 'B1' | null; numUnion1: 11 | 21; foo: { stringField: string } | { numberField: number }; + unionType: string | number | boolean | NumEnum | DefaultEnum | string[] | DictionaryWithAnyKey; } interface DictionaryWithAnyKey { diff --git a/documentation/interface-guide.md b/documentation/interface-guide.md index 27b74c4..537f1ba 100644 --- a/documentation/interface-guide.md +++ b/documentation/interface-guide.md @@ -176,8 +176,10 @@ interface NumberFieldInterface { StringFieldInterface | { numberField: number } StringFieldInterface | NumberFieldInterface -// not allowed: unsupported union +// allowed: types union string | number + +// not allowed: mixing type and tuple { stringField: string } | number ``` @@ -216,7 +218,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, +- `@default`: default value for Module Interface's function parameter, ```typescript /** diff --git a/example-templates/kotlin-named-type.mustache b/example-templates/kotlin-named-type.mustache index 96606bb..47cea2d 100644 --- a/example-templates/kotlin-named-type.mustache +++ b/example-templates/kotlin-named-type.mustache @@ -38,3 +38,28 @@ enum class {{typeName}} { } {{/isStringType}} {{/enum}} +{{#unionType}} +sealed class {{unionTypeName}}(val value: Any) { + {{#members}} + data class {{capitalizeName}}Value(val value: {{{type}}}) : {{unionTypeName}}() + {{/members}} +} + +class {{unionTypeName}}Adapter : JsonSerializer<{{unionTypeName}}>, JsonDeserializer<{{unionTypeName}}> { + override fun serialize(src: {{unionTypeName}}, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { + return context.serialize(src.value) + } + + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): {{unionTypeName}} { + {{#members}} + try { + return {{unionTypeName}}.{{capitalizeName}}Value(context.deserialize(json, {{type}}::class.java)) + } catch (e: Exception) { + // Ignore the exception and try the next type + } + {{/members}} + + throw IllegalArgumentException("Unexpected JSON type: ${json.javaClass}") + } +} +{{/unionType}} \ No newline at end of file diff --git a/example-templates/swift-named-type.mustache b/example-templates/swift-named-type.mustache index be2507b..ab08641 100644 --- a/example-templates/swift-named-type.mustache +++ b/example-templates/swift-named-type.mustache @@ -36,3 +36,37 @@ public enum {{typeName}}: {{valueType}}, Codable { {{/members}} } {{/enum}} +{{#unionType}} +public enum {{unionTypeName}}: Codable { + {{#members}} + case {{uncapitalizeName}}(_ value: {{type}}) + {{/members}} + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + {{#members}} + {{^last}} + {{^first}}else {{/first}}if let value = try? container.decode({{type}}.self) { + self = .{{uncapitalizeName}}(value) + } + {{/last}} + {{#last}} + else { + let value = try container.decode({{type}}.self) + self = .{{uncapitalizeName}}(value) + } + {{/last}} + {{/members}} + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + {{#members}} + case .{{uncapitalizeName}}(let value): + try container.encode(value) + {{/members}} + } + } +} +{{/unionType}} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6090296..9ece0e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ts-gyb", - "version": "0.11.1", + "version": "0.12.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ts-gyb", - "version": "0.11.1", + "version": "0.12.0", "license": "MIT", "dependencies": { "chalk": "^4.1.1", diff --git a/package.json b/package.json index c2327fd..225306b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-gyb", - "version": "0.11.1", + "version": "0.12.0", "description": "Generate Native API based on TS interface", "repository": { "type": "git", diff --git a/src/generator/CodeGenerator.ts b/src/generator/CodeGenerator.ts index 0e8c937..cdf9a53 100644 --- a/src/generator/CodeGenerator.ts +++ b/src/generator/CodeGenerator.ts @@ -10,9 +10,9 @@ import { } from './named-types'; import { Parser } from '../parser/Parser'; import { renderCode } from '../renderer/renderer'; -import { NamedTypeView, ModuleView, InterfaceTypeView, EnumTypeView } from '../renderer/views'; +import { NamedTypeView, ModuleView, InterfaceTypeView, EnumTypeView, UnionTypeView } from '../renderer/views'; import { serializeModule, serializeNamedType } from '../serializers'; -import { isInterfaceType } from '../types'; +import { isEnumType, isInterfaceType } from '../types'; import { applyDefaultCustomTags } from './utils'; import { ValueTransformer, SwiftValueTransformer, KotlinValueTransformer } from '../renderer/value-transformer'; @@ -128,9 +128,12 @@ export class CodeGenerator { if (isInterfaceType(namedType.type)) { namedTypeView = new InterfaceTypeView(namedType.type, namedType.source, valueTransformer); namedTypeView.custom = true; - } else { + } else if (isEnumType(namedType.type)) { namedTypeView = new EnumTypeView(namedType.type, namedType.source, valueTransformer); namedTypeView.enum = true; + } else { + namedTypeView = new UnionTypeView(namedType.type, valueTransformer); + namedTypeView.unionType = true; } return namedTypeView; diff --git a/src/generator/named-types.ts b/src/generator/named-types.ts index df9e82b..5d437f9 100644 --- a/src/generator/named-types.ts +++ b/src/generator/named-types.ts @@ -1,5 +1,6 @@ import { basicTypeOfUnion, + capitalize, membersOfUnion, uniquePathWithMember, uniquePathWithMethodParameter, @@ -18,8 +19,12 @@ import { TupleType, isTupleType, ValueTypeKind, - isUnionType, + isLiteralType, EnumSubType, + LiteralType, + isUnionType, + isBasicType, + isPredefinedType, UnionType, } from '../types'; @@ -29,7 +34,7 @@ export const enum ValueTypeSource { Return = 1 << 2, } -export type NamedType = InterfaceType | EnumType; +export type NamedType = InterfaceType | EnumType | UnionType; export interface NamedTypeInfo { type: NamedType; source: ValueTypeSource; @@ -121,7 +126,7 @@ function fetchNamedTypes(modules: Module[]): NamedTypesResult { namedType.name = path; namedType.documentation = ''; namedType.customTags = {}; - } else if (isUnionType(namedType)) { + } else if (isLiteralType(namedType)) { const subType = basicTypeOfUnion(namedType); const members = membersOfUnion(namedType); @@ -132,6 +137,8 @@ function fetchNamedTypes(modules: Module[]): NamedTypesResult { namedType.members = members; namedType.documentation = ''; namedType.customTags = {}; + } else if (isUnionType(namedType)) { + namedType.name = path; } if (typeMap[namedType.name] === undefined) { @@ -197,7 +204,7 @@ function fetchRootTypes(module: Module): { valueType: ValueType; source: ValueTy function recursiveVisitMembersType( valueType: ValueType, - visit: (membersType: NamedType | TupleType | UnionType, path: string) => void, + visit: (membersType: NamedType | TupleType | LiteralType | UnionType, path: string) => void, path: string ): void { if (isInterfaceType(valueType)) { @@ -240,18 +247,36 @@ function recursiveVisitMembersType( return; } + if (isLiteralType(valueType)) { + visit(valueType, path); + return; + } + if (isUnionType(valueType)) { visit(valueType, path); + valueType.members.forEach((member) => { + let subType: string; + if (isBasicType(member)) { + subType = member.value; + } else if ((member as NamedType).name !== undefined) { + subType = (member as NamedType).name; + } else { + subType = member.kind; + } + recursiveVisitMembersType(member, visit, `${path}${capitalize(subType)}`); + }); return; } - if (valueType.kind === ValueTypeKind.basicType) { + if (isBasicType(valueType)) { // string, boolean, etc. return; } - if (valueType.kind === ValueTypeKind.predefinedType) { + + if (isPredefinedType(valueType)) { // CodeGen_Int, etc. return; } + throw Error(`Unhandled value type ${JSON.stringify(valueType)}`); } diff --git a/src/parser/ValueParser.ts b/src/parser/ValueParser.ts index 298b074..e3058e0 100644 --- a/src/parser/ValueParser.ts +++ b/src/parser/ValueParser.ts @@ -20,7 +20,7 @@ import { isTupleType, EnumField, isBasicType, - UnionType, + LiteralType, OptionalType, Value, UnionLiteralType, @@ -226,9 +226,9 @@ export class ValueParser { } private valueTypeFromTypeNode(typeNode: ts.TypeNode): ValueType { - const unionType = this.parseUnionTypeNode(typeNode); - if (unionType !== null) { - return unionType; + const literalType = this.parseLiteralOrUnionTypeNode(typeNode); + if (literalType !== null) { + return literalType; } const referenceType = this.parseReferenceTypeNode(typeNode); @@ -261,13 +261,15 @@ export class ValueParser { ); } - private parseUnionTypeNode(node: ts.TypeNode): ValueType | null { + private parseLiteralOrUnionTypeNode(node: ts.TypeNode): ValueType | null { if (!ts.isUnionTypeNode(node)) { return null; } let nullable = false; - let valueType: ValueType | undefined; + let isTuple = false; + const valueTypes: ValueType[] = []; + const tupleMembers: Field[] = []; const literalValues: { type: BasicTypeValue.string | BasicTypeValue.number; value: Value; @@ -295,28 +297,22 @@ export class ValueParser { const newValueType = this.valueTypeFromTypeNode(typeNode); - if (!valueType) { - valueType = newValueType; - return; + if (valueTypes.length === 0) { + isTuple = isInterfaceType(newValueType) || isTupleType(newValueType); } - if ( - (!isInterfaceType(valueType) && !isTupleType(valueType)) || - (!isInterfaceType(newValueType) && !isTupleType(newValueType)) - ) { - throw new ValueParserError( - `union type ${node.getText()} is invalid`, - 'Do not support multiple union types except for interface or literal type' - ); + if (isTuple) { + if (isInterfaceType(newValueType) || isTupleType(newValueType)) { + tupleMembers.push(...newValueType.members); + } else { + throw new ValueParserError( + `type ${node.getText()} is invalid`, + 'Use `multiple tuple types` or `union value types` or `type union`' + ); + } + } else { + valueTypes.push(newValueType); } - - const existingMemberNames = new Set(valueType.members.map((member) => member.name)); - valueType = { - kind: ValueTypeKind.tupleType, - members: valueType.members.concat( - newValueType.members.filter((member) => !existingMemberNames.has(member.name)) - ), - }; }); if (literalValues.length > 0) { @@ -337,35 +333,66 @@ export class ValueParser { members.push(obj.value); } }); - const unionKind: UnionType = { - kind: ValueTypeKind.unionType, + const literalKind: LiteralType = { + kind: ValueTypeKind.literalType, memberType: literalValues[0].type, members, }; if (nullable) { const optionalType: OptionalType = { kind: ValueTypeKind.optionalType, - wrappedType: unionKind, + wrappedType: literalKind, }; return optionalType; } - return unionKind; + return literalKind; } - if (!valueType) { + + if (valueTypes.length === 0 && tupleMembers.length === 0) { throw new ValueParserError( - `union type ${node.getText()} is invalid`, - 'Union type must contain one supported non empty type' + `type ${node.getText()} is invalid`, + 'Type must contain one supported non empty type' ); } - if (!isOptionalType(valueType) && nullable) { - return { - kind: ValueTypeKind.optionalType, - wrappedType: valueType, + if (valueTypes.length > 0 && tupleMembers.length > 0) { + throw new ValueParserError( + `mixing ${node.getText()} is invalid`, + 'Type must contain types or tuples' + ); + } + + if (isTuple) { + const value: TupleType = { + kind: ValueTypeKind.tupleType, + members: tupleMembers, }; + if (nullable) { + const optionalType: OptionalType = { + kind: ValueTypeKind.optionalType, + wrappedType: value, + }; + return optionalType; + } + return value; } - return valueType; + if (valueTypes.length === 1) { + if (!isOptionalType(valueTypes[0]) && nullable) { + return { + kind: ValueTypeKind.optionalType, + wrappedType: valueTypes[0], + }; + } + return valueTypes[0]; + } + + return { + name: '', + kind: ValueTypeKind.unionType, + members: valueTypes, + customTags: {}, + }; } private basicTypeKindFromTypeNode(node: ts.TypeNode): BasicType | null { diff --git a/src/renderer/value-transformer/KotlinValueTransformer.ts b/src/renderer/value-transformer/KotlinValueTransformer.ts index e8214fc..1202c1d 100644 --- a/src/renderer/value-transformer/KotlinValueTransformer.ts +++ b/src/renderer/value-transformer/KotlinValueTransformer.ts @@ -10,6 +10,7 @@ import { isPredefinedType, ValueType, Value, + isUnionType, } from '../../types'; import { ValueTransformer } from './ValueTransformer'; @@ -66,6 +67,10 @@ export class KotlinValueTransformer implements ValueTransformer { return this.typeNameMap[valueType.name] ?? valueType.name; } + if (isUnionType(valueType)) { + return this.convertTypeNameFromCustomMap(valueType.name); + } + throw Error('Type not handled'); } diff --git a/src/renderer/value-transformer/SwiftValueTransformer.ts b/src/renderer/value-transformer/SwiftValueTransformer.ts index 941e8e6..f72df28 100644 --- a/src/renderer/value-transformer/SwiftValueTransformer.ts +++ b/src/renderer/value-transformer/SwiftValueTransformer.ts @@ -10,6 +10,7 @@ import { isPredefinedType, ValueType, Value, + isUnionType, } from '../../types'; import { ValueTransformer } from './ValueTransformer'; @@ -66,6 +67,10 @@ export class SwiftValueTransformer implements ValueTransformer { return this.typeNameMap[valueType.name] ?? valueType.name; } + if (isUnionType(valueType)) { + return this.convertTypeNameFromCustomMap(valueType.name); + } + throw Error('Type not handled'); } diff --git a/src/renderer/views/UnionTypeView.ts b/src/renderer/views/UnionTypeView.ts new file mode 100644 index 0000000..76eedf9 --- /dev/null +++ b/src/renderer/views/UnionTypeView.ts @@ -0,0 +1,89 @@ +import { capitalize, uncapitalize } from "../../utils"; +import { BasicType, DictionaryKeyType, DictionaryType, UnionType, ValueType, isArraryType, isBasicType, isDictionaryType } from '../../types'; +import { ValueTransformer } from '../value-transformer'; + +export class UnionTypeView { + constructor( + private readonly value: UnionType, + private readonly valueTransformer: ValueTransformer + ) { } + + get unionTypeName(): string { + return this.valueTransformer.convertTypeNameFromCustomMap(this.value.name); + } + + convertValueTypeToUnionMemberName(valueType: ValueType): string { + if (isArraryType(valueType)) { + return `${this.valueTransformer.convertValueType(valueType.elementType)}Array`; + } + + if (isDictionaryType(valueType)) { + let keyType: string; + switch (valueType.keyType) { + case DictionaryKeyType.string: + keyType = 'String'; + break; + case DictionaryKeyType.number: + keyType = 'Int'; + break; + default: + throw Error('Type not exists'); + } + + return `${this.valueTransformer.convertValueType(valueType.valueType)}For${keyType}Dictionary`; + } + + return this.valueTransformer.convertValueType(valueType); + } + + get members(): { + capitalizeName: string, + uncapitalizeName: string, + type: string; + first: boolean; + last: boolean; + }[] { + const { members } = this.value; + + const dictionaryTypeMembers: DictionaryType[] = []; + let basicTypeMembers: BasicType[] = []; + const otherMembers: ValueType[] = []; + + members.forEach((member) => { + if (isDictionaryType(member)) { + dictionaryTypeMembers.push(member); + } else if (isBasicType(member)) { + basicTypeMembers.push(member); + } else { + otherMembers.push(member); + } + }); + + basicTypeMembers = basicTypeMembers.sort((a, b) => { + // put string to last + if (a.value === 'string' && b.value === 'string') { + return 0; + } + + if (a.value === 'string') { + return 1; + } + + return -1; + }); + + const sortedMembers: ValueType[] = [...otherMembers, ...dictionaryTypeMembers, ...basicTypeMembers]; + return sortedMembers + .map((member, index) => { + const typeName = this.valueTransformer.convertValueType(member); + const memberName = this.convertValueTypeToUnionMemberName(member); + return { + capitalizeName: capitalize(memberName), + uncapitalizeName: uncapitalize(memberName), + type: typeName, + first: index === 0, + last: index === members.length - 1, + }; + }); + } +} diff --git a/src/renderer/views/index.ts b/src/renderer/views/index.ts index be266ec..c1611f5 100644 --- a/src/renderer/views/index.ts +++ b/src/renderer/views/index.ts @@ -1,9 +1,11 @@ import { InterfaceTypeView } from './InterfaceTypeView'; import { EnumTypeView } from './EnumTypeView'; +import { UnionTypeView } from './UnionTypeView'; export * from './EnumTypeView'; export * from './InterfaceTypeView'; export * from './MethodView'; export * from './ModuleView'; +export * from './UnionTypeView'; -export type NamedTypeView = (InterfaceTypeView | EnumTypeView) & { custom?: boolean; enum?: boolean }; +export type NamedTypeView = (InterfaceTypeView | EnumTypeView | UnionTypeView) & { custom?: boolean; enum?: boolean; unionType?: boolean; }; diff --git a/src/serializers.ts b/src/serializers.ts index 7ee1dea..e43679b 100644 --- a/src/serializers.ts +++ b/src/serializers.ts @@ -13,6 +13,7 @@ import { Module, ValueType, Value, + isUnionType, } from './types'; const keywordColor = chalk.green; @@ -71,20 +72,31 @@ ${namedType.members .join('\n')} }`; } + if (isEnumType(namedType)) { + return `${serializeDocumentation(namedType.documentation)}${documentationColor(customTags)}${keywordColor('Enum')} ${namedType.name + } { + ${namedType.members + .map( + (member) => + `${serializeDocumentation(member.documentation)}${identifierColor(member.key)} = ${valueColor(member.value)}` + ) + .join('\n') + .split('\n') + .map((line) => ` ${line}`) + .join('\n')} + }`; + } + if (isUnionType(namedType)) { + return `${documentationColor(customTags)} + ${namedType.members + .map( + (member) => + serializeValueType(member) + ) + .join(' | ')}`; + } - return `${serializeDocumentation(namedType.documentation)}${documentationColor(customTags)}${keywordColor('Enum')} ${ - namedType.name - } { -${namedType.members - .map( - (member) => - `${serializeDocumentation(member.documentation)}${identifierColor(member.key)} = ${valueColor(member.value)}` - ) - .join('\n') - .split('\n') - .map((line) => ` ${line}`) - .join('\n')} -}`; + throw Error(`Unhandled value type ${JSON.stringify(namedType)}`); } function serializeMethod(method: Method): string { @@ -126,6 +138,10 @@ function serializeValueType(valueType: ValueType): string { return valueType.name; } + if (isUnionType(valueType)) { + return valueType.name; + } + throw Error(`Unhandled value type ${JSON.stringify(valueType)}`); } diff --git a/src/types.ts b/src/types.ts index f535e67..71e96e8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,6 +32,7 @@ export type NonEmptyType = | ArrayType | DictionaryType | PredefinedType + | LiteralType | UnionType; export enum ValueTypeKind { @@ -43,6 +44,7 @@ export enum ValueTypeKind { dictionaryType = 'dictionaryType', optionalType = 'optionalType', predefinedType = 'predefinedType', + literalType = 'literalType', unionType = 'unionType', } @@ -60,7 +62,6 @@ export interface BasicType extends BaseValueType { kind: ValueTypeKind.basicType; value: BasicTypeValue; } - export interface InterfaceType extends BaseValueType, Omit { kind: ValueTypeKind.interfaceType; } @@ -118,12 +119,19 @@ export interface PredefinedType extends BaseValueType { export type UnionLiteralType = string | number; -export interface UnionType extends BaseValueType { - kind: ValueTypeKind.unionType; +export interface LiteralType extends BaseValueType { + kind: ValueTypeKind.literalType; memberType: BasicTypeValue.string | BasicTypeValue.number; members: UnionLiteralType[]; } +export interface UnionType extends BaseValueType { + name: string; + kind: ValueTypeKind.unionType; + members: ValueType[]; + customTags: Record; +} + export function isBasicType(valueType: ValueType): valueType is BasicType { return valueType.kind === ValueTypeKind.basicType; } @@ -156,6 +164,10 @@ export function isPredefinedType(valueType: ValueType): valueType is PredefinedT return valueType.kind === ValueTypeKind.predefinedType; } +export function isLiteralType(valueType: ValueType): valueType is LiteralType { + return valueType.kind === ValueTypeKind.literalType; +} + export function isUnionType(valueType: ValueType): valueType is UnionType { return valueType.kind === ValueTypeKind.unionType; } diff --git a/src/utils.ts b/src/utils.ts index bda2a3d..9917522 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import path from 'path'; -import { BasicTypeValue, EnumField, UnionType } from './types'; +import { BasicTypeValue, EnumField, LiteralType } from './types'; export function capitalize(text: string): string { if (text.length === 0) { @@ -37,11 +37,11 @@ export function uniquePathWithMethodReturnType(ownerName: string, methodName: st return `${capitalize(ownerName)}${capitalize(methodName)}ReturnType`; } -export function basicTypeOfUnion(union: UnionType): BasicTypeValue { +export function basicTypeOfUnion(union: LiteralType): BasicTypeValue { return union.memberType; } -export function membersOfUnion(union: UnionType): EnumField[] { +export function membersOfUnion(union: LiteralType): EnumField[] { const result: EnumField[] = []; union.members.forEach((value) => { let key = `${value}`; diff --git a/test/value-parser-test.ts b/test/value-parser-test.ts index b8c6988..9ba1ce5 100644 --- a/test/value-parser-test.ts +++ b/test/value-parser-test.ts @@ -2,7 +2,9 @@ import { describe, it } from 'mocha'; import { expect } from 'chai'; import { withTempMethodParser, withTempValueParser } from './utils'; import { ValueParserError } from '../src/parser/ValueParserError'; -import { BasicType, BasicTypeValue, DictionaryKeyType, DictionaryType, EnumSubType, EnumType, InterfaceType, OptionalType, PredefinedType, TupleType, ValueType, ValueTypeKind } from '../src/types'; +import { BasicType, BasicTypeValue, DictionaryKeyType, DictionaryType, EnumSubType, EnumType, InterfaceType, OptionalType, PredefinedType, TupleType, ValueType, ValueTypeKind, UnionType } from '../src/types'; +import { UnionTypeView } from '../src/renderer/views'; +import { SwiftValueTransformer } from '../src/renderer/value-transformer'; const stringType: BasicType = { kind: ValueTypeKind.basicType, value: BasicTypeValue.string }; const numberType: BasicType = { kind: ValueTypeKind.basicType, value: BasicTypeValue.number }; @@ -289,30 +291,102 @@ describe('ValueParser', () => { it('Empty types union', () => { const valueTypeCode = 'null | undefined'; withTempValueParser(valueTypeCode, parseFunc => { - expect(parseFunc).to.throw('union type null | undefined is invalid'); + expect(parseFunc).to.throw('type null | undefined is invalid'); }); }); + const customCode = ` + interface Person { + name: string; + } + `; - it('Multiple types union', () => { - const valueTypeCode = 'string | number'; - withTempValueParser(valueTypeCode, parseFunc => { - expect(parseFunc).to.throw('union type string | number is invalid'); - }); - }); - - const optionalStringType: OptionalType = { kind: ValueTypeKind.optionalType, wrappedType: stringType }; - - testValueType('null union', 'string | null', optionalStringType); - testValueType('undefined union', 'string | undefined', optionalStringType); - testValueType('null and undefined union', 'string | null | undefined', optionalStringType); - - const tupleType: TupleType = { - kind: ValueTypeKind.tupleType, - members: [{ name: 'stringField', type: stringType, documentation: '' }, { name: 'numberField', type: numberType, documentation: '' }], + let unionType: UnionType = { + customTags: {}, + name: '', + kind: ValueTypeKind.unionType, + members: [ + { + kind: ValueTypeKind.basicType, + value: BasicTypeValue.string, + }, + { + kind: ValueTypeKind.basicType, + value: BasicTypeValue.number, + }, + { + keyType: DictionaryKeyType.string, + kind: ValueTypeKind.dictionaryType, + valueType: { + kind: ValueTypeKind.basicType, + value: BasicTypeValue.string, + } + }, + { + elementType: { + kind: ValueTypeKind.basicType, + value: BasicTypeValue.string, + }, + kind: ValueTypeKind.arrayType + }, + { + customTags: {}, + documentation: "", + kind: ValueTypeKind.interfaceType, + members: [ + { + documentation: "", + name: "name", + type: { + "kind": ValueTypeKind.basicType, + "value": BasicTypeValue.string, + } + } + ], + methods: [], + name: "Person", + }, + ], }; - const optionalTupleType: OptionalType = { kind: ValueTypeKind.optionalType, wrappedType: tupleType }; - - testValueType('merged optional tuple union', '{ stringField: string } | { numberField: number } | null', optionalTupleType); + testValueType('Multiple types union', 'string | number | Record | string[] | Person', unionType, new Set(), customCode); + + const sortedMembers = new UnionTypeView(unionType, new SwiftValueTransformer({})).members; + expect(sortedMembers).to.deep.equal([ + { + capitalizeName: 'StringArray', + uncapitalizeName: 'stringArray', + type: '[String]', + first: true, + last: false + }, + { + capitalizeName: 'Person', + uncapitalizeName: 'person', + type: 'Person', + first: false, + last: false + }, + { + capitalizeName: 'StringForStringDictionary', + uncapitalizeName: 'stringForStringDictionary', + type: '[String: String]', + first: false, + last: false + }, + { + capitalizeName: 'Double', + uncapitalizeName: 'double', + type: 'Double', + first: false, + last: false + }, + { + capitalizeName: 'String', + uncapitalizeName: 'string', + type: 'String', + first: false, + last: true + }, + ]); }); describe('Parse alias type', () => {