diff --git a/annotation/.classpath b/annotation/.classpath new file mode 100644 index 0000000..3f00da7 --- /dev/null +++ b/annotation/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/annotation/.project b/annotation/.project new file mode 100644 index 0000000..50a66f9 --- /dev/null +++ b/annotation/.project @@ -0,0 +1,23 @@ + + + annotation + Project annotation created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/annotation/.settings/org.eclipse.buildship.core.prefs b/annotation/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..f778166 --- /dev/null +++ b/annotation/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir=../../../jsProjects/sourcerer +eclipse.preferences.version=1 diff --git a/annotation/bin/main/com/laidpack/typescript/annotation/TypeScript.kt b/annotation/bin/main/com/laidpack/typescript/annotation/TypeScript.kt new file mode 100644 index 0000000..762e69d --- /dev/null +++ b/annotation/bin/main/com/laidpack/typescript/annotation/TypeScript.kt @@ -0,0 +1,4 @@ +package com.laidpack.typescript.annotation + +@Retention(AnnotationRetention.SOURCE) +annotation class TypeScript diff --git a/codegen-impl/.classpath b/codegen-impl/.classpath new file mode 100644 index 0000000..8148573 --- /dev/null +++ b/codegen-impl/.classpath @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codegen-impl/.project b/codegen-impl/.project new file mode 100644 index 0000000..2246307 --- /dev/null +++ b/codegen-impl/.project @@ -0,0 +1,23 @@ + + + codegen-impl + Project codegen-impl created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/codegen-impl/.settings/org.eclipse.buildship.core.prefs b/codegen-impl/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..f778166 --- /dev/null +++ b/codegen-impl/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir=../../../jsProjects/sourcerer +eclipse.preferences.version=1 diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/BaseTypeScriptProcessor.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/BaseTypeScriptProcessor.kt new file mode 100644 index 0000000..ca0364d --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/BaseTypeScriptProcessor.kt @@ -0,0 +1,139 @@ +package com.laidpack.typescript.codegen + +import com.laidpack.typescript.annotation.TypeScript +import com.laidpack.typescript.codegen.moshi.ITargetType +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.asTypeName +import me.eugeniomarletti.kotlin.metadata.* +import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import javax.annotation.processing.Messager +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.RoundEnvironment +import javax.lang.model.SourceVersion +import javax.lang.model.element.TypeElement +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import javax.tools.Diagnostic + +typealias DefinitionProcessor = (targetType: ITargetType) -> String? +typealias FileProcessor = ( + targetTypes: HashMap, + rootPackageNames: Set, + packageNames: Set +) -> String? +typealias SuperTypeProcessor = (superClassName: ClassName, currentModuleName: String) -> String +abstract class BaseTypeScriptProcessor( + private val customTransformers: List = listOf(), + private val constrainToCurrentModulePackage: Boolean = false, + private val filePreProcessors: List = listOf(), + private val filePostProcessors: List = listOf(), + private val definitionPreProcessors: List = listOf(), + private val definitionPostProcessors: List = listOf(), + private val superTypeTransformer: SuperTypeProcessor = {c, _ -> c.simpleName} +) : KotlinAbstractProcessor(), KotlinMetadataUtils { + private val annotation = TypeScript::class.java + private var moduleName: String = "NativeTypes" + private var indent: String = " " + private var customOutputDir: String? = null + private var fileName = "types.d.ts" + + override fun getSupportedAnnotationTypes() = setOf(annotation.canonicalName) + + override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest() + + override fun getSupportedOptions() = setOf(OPTION_MODULE, OPTION_OUTPUTDIR, OPTION_INDENT, OPTION_FILENAME, kaptGeneratedOption) + + override fun init(processingEnv: ProcessingEnvironment) { + super.init(processingEnv) + moduleName = processingEnv.options[OPTION_MODULE] ?: moduleName + indent = processingEnv.options[OPTION_INDENT] ?: indent + customOutputDir = processingEnv.options[OPTION_OUTPUTDIR] + fileName = processingEnv.options[OPTION_FILENAME] ?: fileName + } + + override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { + + val context = createContext() + val targetedTypes = hashMapOf() + val rootPackageNames = mutableSetOf() + val packageNames = mutableSetOf() + for (element in roundEnv.getElementsAnnotatedWith(annotation)) { + val typeName = element.asType().asTypeName() + if (typeName is ClassName) { + rootPackageNames.add(typeName.packageName) + } + val foundTypes = TargetResolver.resolve(element, context) + foundTypes.forEach { + targetedTypes[it.name.simpleName] = it + packageNames.add(it.name.packageName) + } + } + + if (targetedTypes.isNotEmpty()) { + val content = TypeScriptGenerator.generate( + moduleName, + targetedTypes, + indent, + customTransformers, + constrainToCurrentModulePackage, + rootPackageNames, + packageNames, + filePreProcessors, + filePostProcessors, + definitionPreProcessors, + definitionPostProcessors, + superTypeTransformer + ) + var outputDir : String = customOutputDir ?: options[kaptGeneratedOption] ?: System.getProperty("user.dir") + if (!outputDir.endsWith(File.separator)) + outputDir += File.separator + + val path = Paths.get(outputDir) + if (!Files.exists(path) || !Files.isDirectory(path)) { + messager.printMessage(Diagnostic.Kind.ERROR, "Path '$outputDir' doesn't exist or is not a directory") + return false + } + + val file = File(outputDir, fileName) + file.createNewFile() // overwrite any existing file + file.writeText(content) + + messager.printMessage(Diagnostic.Kind.OTHER, "TypeScript definitions saved at $outputDir$fileName") + } + + return true + } + + private fun createContext(): TargetContext { + return TargetContext( + messager, + elementUtils, + typeUtils, + typesWithinScope = mutableSetOf(), + typesWithTypeScriptAnnotation = mutableSetOf(), + typesToBeAddedToScope = hashMapOf(), + abortOnError = true + ) + } + companion object { + const val OPTION_MODULE = "typescript.module" + const val OPTION_OUTPUTDIR = "typescript.outputDir" + const val OPTION_INDENT = "typescript.indent" + const val OPTION_FILENAME = "typescript.filename" + } +} + +internal class TargetContext ( + val messager: Messager, + val elementUtils: Elements, + val typeUtils: Types, + val typesWithinScope: MutableSet, + val typesWithTypeScriptAnnotation: MutableSet, + var typesToBeAddedToScope: MutableMap, + var abortOnError: Boolean +) { + var targetingTypscriptAnnotatedType = true // vs targeting a base bodyType +} \ No newline at end of file diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/IWrappedBodyType.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/IWrappedBodyType.kt new file mode 100644 index 0000000..109c176 --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/IWrappedBodyType.kt @@ -0,0 +1,36 @@ +package com.laidpack.typescript.codegen + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.TypeName +import javax.lang.model.element.VariableElement + +interface IWrappedBodyType { + val typeName: TypeName + val variableElement: VariableElement? + val isPropertyValue: Boolean + val isTypeVariable: Boolean + val isEnumValue: Boolean + val isBound: Boolean + val parameters: Map + val annotationNames: Set + val hasRawType: Boolean + val isInstantiable: Boolean + val nullable: Boolean + val isWildCard: Boolean + val rawType: ClassName? + val hasParameters: Boolean + val collectionType: CollectionType + val canonicalName : String? + var javaCanonicalName: String? + val name : String? + var isReturningTypeVariable: Boolean + val isPrimitiveOrStringType: Boolean + val bounds: Map + val isMap: Boolean + val isIterable: Boolean + val isSet: Boolean + val isPair: Boolean + val isArray: Boolean + val firstParameterType: IWrappedBodyType + val secondParameterType: IWrappedBodyType +} \ No newline at end of file diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/TargetEnumValue.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/TargetEnumValue.kt new file mode 100644 index 0000000..8324a1c --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/TargetEnumValue.kt @@ -0,0 +1,69 @@ +package com.laidpack.typescript.codegen + +import com.squareup.moshi.Json +import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf +import javax.lang.model.element.* + +/** A enum value in user code that maps to enum bodyType. */ +internal data class TargetEnumValue( + override val name: String, + override val bodyType: WrappedBodyType, + val ordinal: Int, + val proto: ProtoBuf.EnumEntry, + private val field: VariableElement? +) : TargetPropertyOrEnumValue { + + + private val element get() = field!! + + /** Returns the @Json name of this property, or this property's name if none is provided. */ + override fun jsonName(): String { + val fieldJsonName = element.jsonName + + return when { + fieldJsonName != null -> fieldJsonName + else -> name + } + } + + private val Element?.jsonName: String? + get() { + if (this == null) return null + return getAnnotation(Json::class.java)?.name?.replace("$", "\\$") + } + + override fun toString() = name + + /** Returns the JsonQualifiers on the field and parameter of this property. */ + /* + private fun jsonQualifiers(): Set { + val elementQualifiers = element.qualifiers + + return when { + elementQualifiers.isNotEmpty() -> elementQualifiers + else -> setOf() + } + } + private val Element?.qualifiers: Set + get() { + if (this == null) return setOf() + return AnnotationMirrors.getAnnotatedAnnotations(this, JsonQualifier::class.java) + } + /** Returns the JsonQualifiers on the field and parameter of this property. */ + /* + private fun jsonQualifiers(): Set { + val elementQualifiers = element.qualifiers + + return when { + elementQualifiers.isNotEmpty() -> elementQualifiers + else -> setOf() + } + } + private val Element?.qualifiers: Set + get() { + if (this == null) return setOf() + return AnnotationMirrors.getAnnotatedAnnotations(this, JsonQualifier::class.java) + } + */ + */ +} diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/TargetPropertyOrEnumValue.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/TargetPropertyOrEnumValue.kt new file mode 100644 index 0000000..3629efb --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/TargetPropertyOrEnumValue.kt @@ -0,0 +1,9 @@ +package com.laidpack.typescript.codegen + +interface TargetPropertyOrEnumValue { + val name: String + val bodyType: IWrappedBodyType + + /** Returns the @Json name of this property, or this property's name if none is provided. */ + fun jsonName(): String +} \ No newline at end of file diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/TargetResolver.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/TargetResolver.kt new file mode 100644 index 0000000..0da8606 --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/TargetResolver.kt @@ -0,0 +1,68 @@ +package com.laidpack.typescript.codegen + +import com.laidpack.typescript.codegen.moshi.TargetType +import javax.lang.model.element.Element +import javax.tools.Diagnostic + +internal object TargetResolver { + fun resolve(element: Element, context: TargetContext): List { + context.abortOnError = true + context.targetingTypscriptAnnotatedType = true + + val targetTypes = mutableListOf() + val rootType = processTargetType(element, context) + if (rootType != null) + targetTypes.add(rootType) + + /** + * extra bodyType can stem from 3 sources: + // 1. declared types in properties - e.g., bodyType Y in class X { val test = List() } -> see WrappedBodyType.resolvePropertyType + // 2. super classes --> see TargetType.resolveSuperTypes + // 3. bounds in bodyType variables - e.g., bodyType Y in class X { val test = T } --> see WrappedBodyType.resolveGenericClassDeclaration + // TODO: don't capture target types in the context instance (see typesToBeAddedToScope) + **/ + context.targetingTypscriptAnnotatedType = false + context.abortOnError = false + var extraTypes = context.typesToBeAddedToScope.toMap() + while (extraTypes.isNotEmpty()) { + extraTypes.forEach { + if (!context.typesWithinScope.contains(it.key)) { + val derivedType = processTargetType(it.value, context) + if (derivedType != null) + targetTypes.add(derivedType) + } + context.typesToBeAddedToScope.remove(it.key) + } + extraTypes = context.typesToBeAddedToScope.toMap() + } + + return targetTypes + } + + private fun processTargetType(element: Element, context: TargetContext): TargetType? { + if (isDuplicateType(element, context)) return null + + val type = TargetType.get(element, context) + if (type != null) { + context.typesWithinScope.add(type.name.simpleName) + if (context.targetingTypscriptAnnotatedType) context.typesWithTypeScriptAnnotation.add(type.name.simpleName) + } + + return type + } + + + private fun isDuplicateType(element: Element, context: TargetContext): Boolean { + val name = element.simpleName.toString() + if (context.typesWithinScope.contains(name)) { + // error on duplicated annotated types + if (context.typesWithTypeScriptAnnotation.contains(name) && context.targetingTypscriptAnnotatedType) { + context.messager.printMessage(Diagnostic.Kind.ERROR, "Multiple types with a duplicate name: '${element.simpleName}'. Please rename or remove the @TypeScript annotation?") + } + return true// ignore duplicate base types + } + + return false + } + +} \ No newline at end of file diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/TypeScriptGenerator.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/TypeScriptGenerator.kt new file mode 100644 index 0000000..8788cc5 --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/TypeScriptGenerator.kt @@ -0,0 +1,173 @@ +package com.laidpack.typescript.codegen + + +import com.laidpack.typescript.codegen.moshi.ITargetType +import com.squareup.kotlinpoet.ClassName +import java.time.LocalDateTime +import java.util.* + + +/** Generates a JSON adapter for a target bodyType. */ + +internal class TypeScriptGenerator private constructor ( + target: ITargetType, + private val typesWithinScope: Set, + customTransformers: List = listOf(), + private val currentModuleName: String, + private val superTypeTransformer: SuperTypeProcessor + ) { + private val className = target.name + private val typeVariables = target.typeVariables + private val isEnum = target.isEnum + private val superTypes = target.superTypes + val output by lazy {generateDefinition()} + private val propertiesOrEnumValues = target.propertiesOrEnumValues.values + private val transformer = TypeScriptTypeTransformer(customTransformers) + + override fun toString(): String { + return output + } + + private fun generateInterface(): String { + val extendsString = generateExtends() + val templateParameters = generateTypeVariables() + + val properties= generateProperties() + return "${indent}interface ${className.simpleName}$templateParameters$extendsString {\n" + + properties + + "$indent}\n" + } + + private fun generateProperties(): String { + return propertiesOrEnumValues + .joinToString ("") { property -> + val propertyName = property.jsonName() + val propertyType = transformer.transformType(property.bodyType, typesWithinScope, typeVariables) + val isNullable = if (transformer.isNullable(property.bodyType)) "?" else "" + "$indent$indent$propertyName$isNullable: $propertyType;\n" + } + } + + private fun generateEnum(): String { + val enumValues= propertiesOrEnumValues.joinToString(", ") { enumValue -> + "'${enumValue.jsonName()}'" + } + return "${indent}enum ${className.simpleName} { $enumValues }\n" + } + + private fun generateExtends(): String { + return if (superTypes.isNotEmpty()) { + " extends " + superTypes.joinToString(", ") { superTypeTransformer(it.className, currentModuleName) } + } else "" + } + + private fun generateTypeVariables(): String { + return if (typeVariables.isNotEmpty()) { + "<" + typeVariables.values.joinToString(", ") { typeVariable -> + "${typeVariable.name}${getTypeVariableBoundIfAny(typeVariable)}" + } + ">" + } else { + "" + } + } + + private fun getTypeVariableBoundIfAny(bodyType: IWrappedBodyType): String { + val bounds = bodyType.bounds.values.filter { !(it nameEquals Any::class) } + if (bounds.isNotEmpty()) { + val joinedString = bounds.joinToString(" & ") { + transformer.transformType(it, typesWithinScope, typeVariables) + } + return " extends $joinedString" + } + return "" + } + + + private fun generateDefinition(): String { + return if (isEnum) { + generateEnum() + } else { + generateInterface() + } + } + + companion object { + private var indent = " " + fun generate( + moduleName: String, + targetTypes: HashMap, + indent: String, + customTransformers: List, + constrainToCurrentModulePackage: Boolean, + rootPackageNames: Set, + packageNames: Set, + filePreProcessors: List, + filePostProcessors: List, + definitionPreProcessors: List, + definitionPostProcessors: List, + superTypeTransformer: SuperTypeProcessor + ): String { + this.indent = indent + val targetTypeNames = targetTypes.keys + val definitions = mutableListOf() + targetTypeNames.sorted().forEach { key -> + val targetType = targetTypes[key] as ITargetType + if (isValidTargetType(targetType, constrainToCurrentModulePackage, rootPackageNames)) { + val generatedTypeScript = TypeScriptGenerator( + targetType, + targetTypeNames, + customTransformers, + moduleName, + superTypeTransformer + ) + addAnyProcessedDefinitions(targetType, definitionPreProcessors, definitions) + definitions.add(generatedTypeScript.output) + addAnyProcessedDefinitions(targetType, definitionPostProcessors, definitions) + } + } + val timestamp = "/* generated @ ${LocalDateTime.now()} */\n" + val customBeginStatements = getAnyProcessedFileStatements(targetTypes, rootPackageNames, packageNames, filePreProcessors) + val moduleStart = "declare module \"$moduleName\" {\n" + val moduleContent = definitions.joinToString("\n") + val moduleEnd = "}\n" + val customEndStatements = getAnyProcessedFileStatements(targetTypes, rootPackageNames, packageNames, filePostProcessors) + + return "$timestamp$customBeginStatements$moduleStart$moduleContent$moduleEnd$customEndStatements" + } + + private fun isValidTargetType( + targetType: ITargetType, + constrainToCurrentModulePackage: Boolean, + rootPackageNames: Set + ): Boolean { + return !constrainToCurrentModulePackage + || rootPackageNames.contains(targetType.name.packageName) + || rootPackageNames.any { targetType.name.packageName.startsWith(it) } + } + private fun addAnyProcessedDefinitions( + targetType: ITargetType, + processors: List, + definitions: MutableList + ) { + for (processor in processors) { + val result = processor(targetType) + result?.let { definitions.add(it) } + } + } + + private fun getAnyProcessedFileStatements( + targetTypes: HashMap, + rootPackageNames: Set, + packageNames: Set, + processors: List + ): String { + var result = "" + for (processor in processors) { + processor(targetTypes, rootPackageNames, packageNames)?.let { + result += it + } + } + return result + } + } +} diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/TypeScriptTypeTransformer.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/TypeScriptTypeTransformer.kt new file mode 100644 index 0000000..38161f9 --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/TypeScriptTypeTransformer.kt @@ -0,0 +1,131 @@ +package com.laidpack.typescript.codegen + +enum class Nullability { + Null, + NonNull, + NoTransform +} +class TypeTransformer ( + val predicate: (bodyType: IWrappedBodyType) -> Boolean, + val type: String, + val nullable: Nullability +) + +internal class TypeScriptTypeTransformer( + private val customTransformers: List = listOf() +) { + + fun isNullable(bodyType: IWrappedBodyType): Boolean { + val customTransformer = customTransformers.find { t -> t.predicate(bodyType) } + return if (customTransformer != null) { + when (customTransformer.nullable) { + Nullability.Null -> true + Nullability.NonNull -> false + Nullability.NoTransform -> bodyType.nullable + } + } else bodyType.nullable + } + + fun transformType(bodyType: IWrappedBodyType, typesWithinScope: Set, bodyTypeVariables: Map): String { + val customTransformer = customTransformers.find { t -> t.predicate(bodyType) } + return when { + customTransformer != null -> customTransformer.type + bodyType.isWildCard -> "any" + bodyType.name != null && typesWithinScope.contains(bodyType.name as String) -> "${bodyType.name}${transformTypeParameters(bodyType, typesWithinScope, bodyTypeVariables)}" + bodyType.isReturningTypeVariable -> "${bodyType.name}" + bodyType.isTypeVariable && bodyTypeVariables.containsKey(bodyType.name) -> "${bodyType.name}${transformTypeParameters(bodyType, typesWithinScope, bodyTypeVariables)}" + else -> { + val transformer = if (!bodyType.hasParameters) valueTransformers.find { t -> t.predicate(bodyType) } else null + transformer?.type + ?: transformCollectionType(bodyType, typesWithinScope, bodyTypeVariables) + ?: "any /* unknown bodyType */" + } + } + } + + private fun transformCollectionType(bodyType: IWrappedBodyType, typesWithinScope: Set, bodyTypeVariables: Map): String? { + when { + bodyType.isMap && bodyType.hasParameters -> { + // check if first parameter is string or number.. then we can just use object notation + val firstParam = bodyType.firstParameterType + val secondParam = bodyType.secondParameterType + return when { + firstParam nameEquals String::class -> "{ [key: string]: ${transformType(secondParam, typesWithinScope, bodyTypeVariables)} }" + numericClasses.any { c -> firstParam nameEquals c } -> "{ [key: number]: ${transformType(secondParam, typesWithinScope, bodyTypeVariables)} }" + else -> "Map${transformTypeParameters(bodyType, typesWithinScope, bodyTypeVariables)}" + } + } + bodyType.isMap -> { + return "Map" + } + (bodyType.isIterable || bodyType.isArray) && bodyType.hasParameters -> { + if (bodyType.parameters.size == 1) { // can it be something else? + return "Array${transformTypeParameters(bodyType, typesWithinScope, bodyTypeVariables)}" + } + } + bodyType.isIterable -> { + return "Array" + } + bodyType.isArray -> { + return "any:[]" + } + bodyType.isPair && bodyType.hasParameters -> { + val firstParam = bodyType.firstParameterType + val secondParam = bodyType.secondParameterType + return "[${transformType(firstParam, typesWithinScope, bodyTypeVariables)}, ${transformType(secondParam, typesWithinScope, bodyTypeVariables)}]" + } + bodyType.isPair -> { + return "[any, any]" + } + bodyType.isSet && bodyType.hasParameters -> { + return "Set${transformTypeParameters(bodyType, typesWithinScope, bodyTypeVariables)}" + } + bodyType.isSet -> { + return "Set" + } + } + + return null + } + + private fun transformTypeParameters(bodyType: IWrappedBodyType, typesWithinScope: Set, bodyTypeVariables: Map): String { + if (bodyType.hasParameters) { + return "<${bodyType.parameters.values.joinToString(", ") { + transformType(it, typesWithinScope, bodyTypeVariables) + } + }>" + } + return "" + } + + companion object { + private val numericClasses = listOf( + Int::class, + Long::class, + Short::class, + Float::class, + Double::class, + Byte::class) + private val valueTransformers = listOf( + TypeTransformer({ r -> r nameEquals String::class}, "string", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals Char::class}, "string", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals Int::class}, "number", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals Long::class}, "number", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals Short::class}, "number", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals Float::class}, "number", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals Double::class}, "number", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals Byte::class}, "number", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals Boolean::class}, "boolean", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals IntArray::class }, "Array", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals ShortArray::class }, "Array", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals ByteArray::class }, "Array", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals LongArray::class }, "Array", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals DoubleArray::class }, "Array", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals FloatArray::class }, "Array", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals LongArray::class }, "Array", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals CharArray::class }, "Array", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals CharArray::class }, "Array", Nullability.NoTransform), + TypeTransformer({ r -> r nameEquals Any::class }, "any", Nullability.NoTransform) + ) + } +} \ No newline at end of file diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/WrappedBodyType.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/WrappedBodyType.kt new file mode 100644 index 0000000..be7a864 --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/WrappedBodyType.kt @@ -0,0 +1,347 @@ +package com.laidpack.typescript.codegen + +import com.laidpack.typescript.codegen.moshi.rawType +import com.squareup.kotlinpoet.* +import java.lang.IndexOutOfBoundsException +import javax.lang.model.element.VariableElement +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.TypeKind +import javax.lang.model.type.TypeMirror +import kotlin.reflect.KClass + +enum class CollectionType { + Map, + Set, + Iterable, + Pair, + Array, + None +} + +internal class WrappedBodyType private constructor ( + override val typeName: TypeName, + override val variableElement: VariableElement?, + override val isPropertyValue: Boolean, + override val isTypeVariable: Boolean, + override val isEnumValue: Boolean, + override val isBound: Boolean, + override val parameters: Map, + override val annotationNames: Set, + private val _bounds: Map +) : IWrappedBodyType { + override val hasRawType = typeName is ClassName || typeName is ParameterizedTypeName + override val isInstantiable = hasRawType && !isEnumValue + override val nullable = typeName.nullable + override val isWildCard = typeName is WildcardTypeName + override val rawType: ClassName? = if (hasRawType) typeName.rawType() else null + override val hasParameters: Boolean + get() = parameters.isNotEmpty() + override var collectionType: CollectionType = CollectionType.None + private set + override val canonicalName : String? = if (hasRawType) typeName.rawType().canonicalName else null + override val name : String? + get() = resolveName(typeName) + override var isReturningTypeVariable = false + override var javaCanonicalName: String? = null + override val isPrimitiveOrStringType by lazy {when { + this nameEquals String::class -> true + this nameEquals Int::class -> true + this nameEquals Boolean::class -> true + this nameEquals Float::class -> true + this nameEquals Double::class -> true + this nameEquals Long::class -> true + this nameEquals Char::class -> true + this nameEquals Short::class -> true + else -> false + }} + + override val bounds: Map + get() { + if (!isTypeVariable) throw IllegalStateException("Bounds are only available for bodyType variables. Type is $name") + return _bounds + } + + override val isMap: Boolean + get() = collectionType == CollectionType.Map + override val isIterable: Boolean + get() = collectionType == CollectionType.Iterable + override val isSet: Boolean + get() = collectionType == CollectionType.Set + override val isPair: Boolean + get() = collectionType == CollectionType.Pair + override val isArray: Boolean + get() = collectionType == CollectionType.Array + + override val firstParameterType: WrappedBodyType + get() = getParameterTypeAt(0) + override val secondParameterType: WrappedBodyType + get() = getParameterTypeAt(1) + + fun getParameterTypeAt(index: Int): WrappedBodyType { + if (!hasParameters || typeName !is ParameterizedTypeName) throw IllegalStateException("Type $name has no template parameters") + if (index < 0 || typeName.typeArguments.size < index) throw IndexOutOfBoundsException("Template parameter index $index is out of bounds") + val typeArgument = typeName.typeArguments[index] + val name = resolveName(typeArgument) ?: throw IllegalStateException("Parameter has no name") + if (!parameters.containsKey(name)) throw IllegalStateException("Parameter with name '$name' is not in parameters map") + return parameters[name] as WrappedBodyType + } + + companion object { + var getSuperTypeNames = { typeMirror: TypeMirror, context: TargetContext -> + context.typeUtils.directSupertypes(typeMirror).map { it.asTypeName() } + } + + var getMirror = { wrappedBodyType: WrappedBodyType, context: TargetContext -> getMirrorDefaultImpl(wrappedBodyType, context)} + + fun resolveName(typeName: TypeName): String? { + return when (typeName) { + is WildcardTypeName -> "any" + is TypeVariableName -> typeName.name + is ClassName, is ParameterizedTypeName -> typeName.rawType().simpleName + else -> null + } + } + + /** + wrap bodyType variable, + add bounds to wrapped bodyType variable + find current and nested bound types + for every bound, + -- check if there are any new declared types + -- resolve collection types + **/ + + fun resolveGenericClassDeclaration(typeVariableNames: Map, context: TargetContext): HashMap { + val typeVariables = HashMap() + if (typeVariableNames.isEmpty()) return typeVariables + + typeVariableNames.values.forEach { typeVariableName -> + val type = wrapTypeVariable(typeVariableName) + for (boundType in type.bounds.values) { + this.performActionsOnTypeAndItsNestedTypes(boundType, context) { foundType, c -> + resolveCollectionType(foundType, c) + addTypeToScopeIfNewDeclaredType(foundType, c) + } + } + typeVariables[type.name!!] = type + } + + return typeVariables + } + + /** + wrap property bodyType + check if value is a declared class bodyType variable + find nested bodyType variables.. + wrap nested types + check if there are any new declared types + resolve collection types + **/ + + fun resolvePropertyType(typeName: TypeName, variableElement: VariableElement?, bodyTypeVariables: Map, context: TargetContext): WrappedBodyType { + val type = wrapPropertyType(typeName, variableElement) + if (bodyTypeVariables.containsKey(type.name)) { + type.isReturningTypeVariable = true + } + this.performActionsOnTypeAndItsNestedTypes(type, context) { foundType, c -> + resolveCollectionType(foundType, c) + addTypeToScopeIfNewDeclaredType(foundType, c) + } + return type + } + + fun resolveEnumValueType(typeName: TypeName): WrappedBodyType { + return wrapEnumValueType(typeName) + } + + private fun get( + typeName: TypeName, + variableElement: VariableElement?, + isPropertyValue: Boolean, + isTypeVariable: Boolean, + isEnumValue: Boolean, + isBound: Boolean + ): WrappedBodyType { + val wrappedType = WrappedBodyType( + typeName, + variableElement, + isPropertyValue, + isTypeVariable, + isEnumValue, + isBound, + getParameterTypes(typeName), + getAnnotationNames(variableElement), + getBounds(isTypeVariable, typeName) + ) + injectJavaMirroredCanonicalName(wrappedType) + return wrappedType + } + + private fun getParameterTypes(typeName: TypeName): Map { + val parameters = mutableMapOf() + if (typeName is ParameterizedTypeName) { + typeName.typeArguments.forEach { + val name = WrappedBodyType.resolveName(it) + if (name != null && !parameters.containsKey(name)) { + parameters[name] = wrapTypeVariable(it) + } + } + } + return parameters + } + + private fun getAnnotationNames(variableElement: VariableElement?): Set { + val annotationNames = mutableSetOf() + if (variableElement != null) { + annotationNames.addAll( + variableElement.annotationMirrors.map { annotationMirror -> + annotationMirror.annotationType.asTypeName().toString() + } + ) + } + return annotationNames + } + + private fun getBounds(isTypeVariable: Boolean, typeName: TypeName): Map { + val bounds = mutableMapOf() + if (isTypeVariable && typeName is TypeVariableName) { + typeName.bounds.forEach { + val name = WrappedBodyType.resolveName(it) + if (name != null) { + val boundType = wrapBoundType(it) + bounds[name] = boundType + } + } + } + return bounds + } + + private fun injectJavaMirroredCanonicalName(bodyType: WrappedBodyType) { + if (bodyType.variableElement != null ) { + val typeMirror = bodyType.variableElement.asType() + injectJavaMirroredCanonicalName(bodyType, typeMirror) + } + } + + private fun injectJavaMirroredCanonicalName(bodyType: WrappedBodyType, typeMirror: TypeMirror) { + val javaTypeName = typeMirror.asTypeName() + if (javaTypeName is ClassName || javaTypeName is ParameterizedTypeName) { + bodyType.javaCanonicalName = javaTypeName.rawType().canonicalName + } + if (bodyType.typeName is ParameterizedTypeName && typeMirror is DeclaredType) { + val max = typeMirror.typeArguments.size -1 + for (i in 0..max) { + val parameterTypeMirror= typeMirror.typeArguments[i] + val parameterWrappedType = bodyType.getParameterTypeAt(i) + injectJavaMirroredCanonicalName(parameterWrappedType, parameterTypeMirror) + } + } + } + + private fun addTypeToScopeIfNewDeclaredType(bodyType: WrappedBodyType, context: TargetContext) { + if (!bodyType.isPrimitiveOrStringType && bodyType.isInstantiable && bodyType.collectionType == CollectionType.None + && !context.typesWithinScope.contains(bodyType.name) && !context.typesToBeAddedToScope.containsKey(bodyType.name)) { + val typeElement = context.elementUtils.getTypeElement(bodyType.canonicalName) + if (typeElement != null && typeElement.asType() is DeclaredType) { + context.typesToBeAddedToScope[bodyType.name!!] = typeElement + } + } + + } + + private fun resolveCollectionType(bodyType: WrappedBodyType, context: TargetContext) { + if (bodyType.isPrimitiveOrStringType) return + + val simpleMapping = when { + bodyType nameEquals Pair::class -> CollectionType.Pair + bodyType nameEquals List::class -> CollectionType.Iterable + bodyType nameEquals Set::class -> CollectionType.Set + bodyType nameEquals Map::class -> CollectionType.Map + bodyType nameEquals HashMap::class -> CollectionType.Map + else -> null + } + + if (simpleMapping != null) { + bodyType.collectionType = simpleMapping + return + } + + val typeMirror = getMirror(bodyType, context) ?: return + val kind = typeMirror.kind + if (kind == TypeKind.ARRAY) { + bodyType.collectionType = CollectionType.Array + return + } + // add recursive check? + loop@ for (superTypeName in getSuperTypeNames(typeMirror, context)) { + var exit = true + when { + superTypeName nameEquals Map::class -> bodyType.collectionType = CollectionType.Map + superTypeName nameEquals Set::class -> bodyType.collectionType = CollectionType.Set + superTypeName nameEquals Collection::class -> bodyType.collectionType = CollectionType.Iterable + else -> exit = false + } + if (exit) break@loop + } + } + + fun performActionsOnTypeAndItsNestedTypes(bodyType: WrappedBodyType, context: TargetContext, action: (foundBodyType: WrappedBodyType, context: TargetContext) -> Any) { + action(bodyType, context) + if (bodyType.hasParameters) { + bodyType.parameters.values.forEach { + performActionsOnTypeAndItsNestedTypes(it, context, action) + } + } + } + + private fun getMirrorDefaultImpl(wrappedBodyType: WrappedBodyType, context: TargetContext, preferJavaTypeMirror: Boolean = true): TypeMirror? { + var typeMirror : TypeMirror? = null + if (preferJavaTypeMirror) { + if (wrappedBodyType.variableElement != null) { + typeMirror = wrappedBodyType.variableElement.asType() + return typeMirror + } + // try to resovle bodyType from root.. we prefer java typess and the root bodyType contains the information + if (typeMirror == null && wrappedBodyType.javaCanonicalName != null) { + val typeElement = context.elementUtils.getTypeElement(wrappedBodyType.javaCanonicalName) + typeMirror = typeElement?.asType() + + } + } + if (typeMirror == null && wrappedBodyType.canonicalName != null) { + val typeElement = context.elementUtils.getTypeElement(wrappedBodyType.canonicalName) + typeMirror = typeElement?.asType() + } + return typeMirror + } + + private fun wrapPropertyType(typeName: TypeName, variableElement: VariableElement?): WrappedBodyType { + val type = WrappedBodyType.get(typeName, variableElement,true, false, false, false) + return type + } + private fun wrapTypeVariable(typeName: TypeName): WrappedBodyType { + val type = WrappedBodyType.get(typeName, null,false,true, false, false) + return type + } + private fun wrapEnumValueType(typeName: TypeName): WrappedBodyType { + return WrappedBodyType.get(typeName, null,false,false, true, false) + } + private fun wrapBoundType(typeName: TypeName): WrappedBodyType { + return WrappedBodyType.get(typeName, null,false,false, false, true) + } + } +} + + +internal infix fun WrappedBodyType.nameEquals(classType: KClass<*>): Boolean { + return this.canonicalName != null && + (this.canonicalName == classType.qualifiedName || this.canonicalName == classType.java.canonicalName) +} +internal infix fun IWrappedBodyType.nameEquals(classType: KClass<*>): Boolean { + return this.canonicalName != null && + (this.canonicalName == classType.qualifiedName || this.canonicalName == classType.java.canonicalName) +} +internal infix fun TypeName.nameEquals(classType: KClass<*>): Boolean { + return (this is ClassName || this is ParameterizedTypeName) && + (this.rawType().canonicalName == classType.qualifiedName || this.rawType().canonicalName == classType.java.canonicalName) +} diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/AppliedType.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/AppliedType.kt new file mode 100644 index 0000000..c5e6d14 --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/AppliedType.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.laidpack.typescript.codegen.moshi + +import com.squareup.kotlinpoet.* +import javax.lang.model.element.TypeElement +import javax.lang.model.type.DeclaredType +import javax.lang.model.util.Types + +/** + * A concrete bodyType like `List` with enough information to know how to resolve its bodyType + * variables. + */ +class AppliedType private constructor( + val element: TypeElement, + val resolver: TypeResolver, + val className: ClassName, + private val mirror: DeclaredType +) { + /** Returns super type. Includes both interface and class supertypes. */ + fun supertypes( + types: Types, + result: MutableSet = mutableSetOf() + ): Set { + //result.add(this) + for (supertype in types.directSupertypes(mirror)) { + val supertypeDeclaredType = supertype as DeclaredType + val supertypeElement = supertypeDeclaredType.asElement() as TypeElement + val appliedSupertype = AppliedType( + supertypeElement, + resolver(supertypeElement, supertypeDeclaredType), + supertypeElement.asClassName(), + supertypeDeclaredType + ) + result.add(appliedSupertype) + //appliedSupertype.supertypes(types, result) + } + return result + } + + /** Returns a resolver that uses `element` and `mirror` to resolve bodyType parameters. */ + private fun resolver(element: TypeElement, mirror: DeclaredType): TypeResolver { + return object : TypeResolver() { + override fun resolveTypeVariable(typeVariable: TypeVariableName): TypeName { + val index = element.typeParameters.indexOfFirst { + it.simpleName.toString() == typeVariable.name + } + check(index != -1) { "Unexpected bodyType variable $typeVariable in $mirror" } + val argument = mirror.typeArguments[index] + return argument.asTypeName() + } + } + } + + override fun toString() = mirror.toString() + + companion object { + fun get(typeElement: TypeElement): AppliedType { + return AppliedType(typeElement, TypeResolver(), typeElement.asClassName(), typeElement.asType() as DeclaredType) + } + } +} \ No newline at end of file diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/ITargetType.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/ITargetType.kt new file mode 100644 index 0000000..3c18e19 --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/ITargetType.kt @@ -0,0 +1,13 @@ +package com.laidpack.typescript.codegen.moshi + +import com.laidpack.typescript.codegen.IWrappedBodyType +import com.laidpack.typescript.codegen.TargetPropertyOrEnumValue +import com.squareup.kotlinpoet.ClassName + +interface ITargetType { + val name: ClassName + val propertiesOrEnumValues: Map + val typeVariables: Map + val isEnum: Boolean + val superTypes: Set +} \ No newline at end of file diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/TargetProperty.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/TargetProperty.kt new file mode 100644 index 0000000..65ab03b --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/TargetProperty.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.laidpack.typescript.codegen.moshi + +import com.laidpack.typescript.codegen.TargetPropertyOrEnumValue +import com.laidpack.typescript.codegen.WrappedBodyType +import com.squareup.moshi.Json +import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Property +import javax.lang.model.element.* + + +/** A property in user code that maps to JSON. */ +internal data class TargetProperty( + override val name: String, + override val bodyType: WrappedBodyType, + private val proto: Property, + private val annotationHolder: ExecutableElement?, + private val field: VariableElement?, + private val setter: ExecutableElement?, + private val getter: ExecutableElement? +) : TargetPropertyOrEnumValue { + + private val element get() = field ?: setter ?: getter!! + + /** Returns the @Json name of this property, or this property's name if none is provided. */ + override fun jsonName(): String { + val fieldJsonName = element.jsonName + val annotationHolderJsonName = annotationHolder.jsonName + + return when { + fieldJsonName != null -> fieldJsonName + annotationHolderJsonName != null -> annotationHolderJsonName + else -> name + } + } + + private val Element?.jsonName: String? + get() { + if (this == null) return null + return getAnnotation(Json::class.java)?.name?.replace("$", "\\$") + } + + override fun toString() = name + + + + /* + private val isTransient get() = field != null && Modifier.TRANSIENT in field.modifiers + + private val isSettable get() = proto.hasSetter || parameter != null + + private val isVisible: Boolean + get() { + return proto.visibility == INTERNAL + || proto.visibility == PROTECTED + || proto.visibility == PUBLIC + } + + + /** Returns the JsonQualifiers on the field and parameter of this property. */ + private fun jsonQualifiers(): Set { + val elementQualifiers = element.qualifiers + val annotationHolderQualifiers = annotationHolder.qualifiers + val parameterQualifiers = parameter?.element.qualifiers + + // TODO(jwilson): union the qualifiers somehow? + return when { + elementQualifiers.isNotEmpty() -> elementQualifiers + annotationHolderQualifiers.isNotEmpty() -> annotationHolderQualifiers + parameterQualifiers.isNotEmpty() -> parameterQualifiers + else -> setOf() + } + } + + + private val Element?.qualifiers: Set + get() { + if (this == null) return setOf() + return AnnotationMirrors.getAnnotatedAnnotations(this, JsonQualifier::class.java) + } + */ + +} diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/TargetType.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/TargetType.kt new file mode 100644 index 0000000..3a1b917 --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/TargetType.kt @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.laidpack.typescript.codegen.moshi + +import com.laidpack.typescript.codegen.* +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.asTypeName +import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata +import me.eugeniomarletti.kotlin.metadata.KotlinMetadata +import me.eugeniomarletti.kotlin.metadata.classKind +import me.eugeniomarletti.kotlin.metadata.getPropertyOrNull +import me.eugeniomarletti.kotlin.metadata.isInnerClass +import me.eugeniomarletti.kotlin.metadata.kotlinMetadata +import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf +import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Class +import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter +import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.LOCAL +import me.eugeniomarletti.kotlin.metadata.shadow.metadata.deserialization.NameResolver +import me.eugeniomarletti.kotlin.metadata.shadow.util.capitalizeDecapitalize.decapitalizeAsciiOnly +import me.eugeniomarletti.kotlin.metadata.visibility +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.tools.Diagnostic.Kind.ERROR +import javax.tools.Diagnostic.Kind.WARNING + +/** A user bodyType that should be decoded and encoded by generated code. */ +internal data class TargetType( + val proto: Class, + val element: TypeElement, + override val name: ClassName, + override val propertiesOrEnumValues: Map, + override val typeVariables: Map, + override val superTypes: Set, + val isTypeScriptAnnotated: Boolean +) : ITargetType { + + override val isEnum = proto.classKind == ProtoBuf.Class.Kind.ENUM_CLASS + companion object { + private val OBJECT_CLASS = ClassName("java.lang", "Object") + + /** Returns a target bodyType for `element`, or null if it cannot be used with code gen. */ + fun get(element: Element, context: TargetContext): TargetType? { + val typeMetadata: KotlinMetadata? = element.kotlinMetadata + if (element !is TypeElement || typeMetadata !is KotlinClassMetadata) { + if (context.abortOnError) + context.messager.printMessage(ERROR, "@TypeScript can't be applied to $element: must be a Kotlin class", element) + return null + } + + val proto = typeMetadata.data.classProto + if (proto.classKind == Class.Kind.ENUM_CLASS) { + return getEnumTargetType(element, proto, typeMetadata, context) + } + + return getDeclaredTargetType(element, proto, typeMetadata, context) + } + + private fun getEnumTargetType(element: TypeElement, proto: Class, typeMetadata: KotlinClassMetadata, context: TargetContext): TargetType? { + + val enumValues = declaredEnumValues(element, proto, typeMetadata) + + val typeName = element.asType().asTypeName() + val name = when (typeName) { + is ClassName -> typeName + is ParameterizedTypeName -> typeName.rawType + else -> throw IllegalStateException("unexpected TypeName: ${typeName::class}") + } + + return TargetType(proto, element, name, enumValues, mapOf(), setOf(), context.targetingTypscriptAnnotatedType) + } + + private fun getDeclaredTargetType(element: TypeElement, proto: Class, typeMetadata: KotlinClassMetadata, context: TargetContext): TargetType? { + when { + proto.classKind != Class.Kind.CLASS -> { + context.messager.printMessage( if (context.abortOnError) ERROR else WARNING, "@TypeScript can't be applied to $element: must be a Kotlin class", element) + return null + } + proto.isInnerClass -> { + context.messager.printMessage( if (context.abortOnError) ERROR else WARNING, "@TypeScript can't be applied to $element: must not be an inner class", element) + return null + } + proto.visibility == LOCAL -> { + context.messager.printMessage( if (context.abortOnError) ERROR else WARNING, "@TypeScript can't be applied to $element: must not be local", element) + return null + } + } + + val type = element.asType() + val typeName = type.asTypeName() + + if (typeName nameEquals Pair::class) { + // don't add a target bodyType for Pair that needs to be defined, use Typescript's [A,B] notation + return null + } + + val typeVariableNames = genericTypeNames(proto, typeMetadata.data.nameResolver) + val typeVariables = WrappedBodyType.resolveGenericClassDeclaration(typeVariableNames, context) + val appliedType = AppliedType.get(element) + + val properties = declaredProperties(element, appliedType.resolver, typeVariables, context) + val selectedSuperTypes = resolveSuperTypes(appliedType, context) ?: return null + + val name = when (typeName) { + is ClassName -> typeName + is ParameterizedTypeName -> typeName.rawType + else -> throw IllegalStateException("unexpected TypeName: ${typeName::class}") + } + return TargetType(proto, element, name, properties, typeVariables, selectedSuperTypes, context.targetingTypscriptAnnotatedType) + } + + private fun resolveSuperTypes(appliedType: AppliedType, context: TargetContext): Set? { + val selectedSuperTypes = mutableSetOf() + for (supertype in appliedType.supertypes(context.typeUtils)) { + if (supertype.element.asClassName() == OBJECT_CLASS) { + continue // Don't load propertiesOrEnumValues for java.lang.Object. + } + if (supertype.element.kind != ElementKind.CLASS) { + continue // Don't load propertiesOrEnumValues for interface types. + } + if (supertype.element.kotlinMetadata == null) { + context.messager.printMessage(ERROR, + "@TypeScript can't be applied to ${appliedType.element.simpleName}: supertype $supertype is not a Kotlin bodyType", + appliedType.element) + return null + } + if (supertype.element.asClassName() != appliedType.element.asClassName()) { + selectedSuperTypes.add(supertype) + context.typesToBeAddedToScope[supertype.element.simpleName.toString()] = supertype.element + } + } + return selectedSuperTypes + } + + /** Returns the propertiesOrEnumValues declared by `typeElement`. */ + private fun declaredProperties( + typeElement: TypeElement, + typeResolver: TypeResolver, + bodyTypeVariables: Map, + context: TargetContext + ): Map { + val typeMetadata: KotlinClassMetadata = typeElement.kotlinMetadata as KotlinClassMetadata + val nameResolver = typeMetadata.data.nameResolver + val classProto = typeMetadata.data.classProto + + val annotationHolders = mutableMapOf() + val fields = mutableMapOf() + val setters = mutableMapOf() + val getters = mutableMapOf() + for (element in typeElement.enclosedElements) { + if (element is VariableElement) { + fields[element.name] = element + } else if (element is ExecutableElement) { + when { + element.name.startsWith("get") -> { + val name = element.name.substring("get".length).decapitalizeAsciiOnly() + getters[name] = element + } + element.name.startsWith("is") -> { + val name = element.name.substring("is".length).decapitalizeAsciiOnly() + getters[name] = element + } + element.name.startsWith("set") -> { + val name = element.name.substring("set".length).decapitalizeAsciiOnly() + setters[name] = element + } + } + + val propertyProto = typeMetadata.data.getPropertyOrNull(element) + if (propertyProto != null) { + val name = nameResolver.getString(propertyProto.name) + annotationHolders[name] = element + } + } + } + + val result = mutableMapOf() + for (property in classProto.propertyList) { + val name = nameResolver.getString(property.name) + val typeName = typeResolver.resolve(property.returnType.asTypeName( + nameResolver, classProto::getTypeParameter, false + )) + + val wrappedType = WrappedBodyType.resolvePropertyType(typeName, fields[name], bodyTypeVariables, context) + result[name] = TargetProperty( + name, wrappedType, property, + annotationHolders[name], fields[name], setters[name], getters[name] + ) + + } + + return result + } + + /** Returns the propertiesOrEnumValues declared by `typeElement`. */ + private fun declaredEnumValues( + typeElement: TypeElement, + classProto: Class, + typeMetadata: KotlinClassMetadata + ): Map { + val nameResolver = typeMetadata.data.nameResolver + val fields = mutableMapOf() + for (element in typeElement.enclosedElements) { + if (element is VariableElement) { + fields[element.name] = element + } + } + + val result = mutableMapOf() + var ordinal = 0 + for (enumEntry in classProto.enumEntryList) { + val name = nameResolver.getString(enumEntry.name) + val wrappedType = WrappedBodyType.resolveEnumValueType(typeElement.asType().asTypeName()) + result[name] = TargetEnumValue( + name, + wrappedType, // enum value returns enum class (e.g., TestEnum.One returns --> TestEnum with value 1 in class enum TestEnum (val value; Int) { One(1), Two(2) } + ordinal, + enumEntry, + fields[name] + ) + ordinal += 1 + } + + return result + } + + private val Element.name get() = simpleName.toString() + + private fun genericTypeNames(proto: Class, nameResolver: NameResolver): Map { + return proto.typeParameterList.map { + val possibleBounds = it.upperBoundList + .map { it.asTypeName(nameResolver, proto::getTypeParameter, false) } + val typeVar = if (possibleBounds.isEmpty()) { + TypeVariableName( + name = nameResolver.getString(it.name), + variance = it.varianceModifier) + } else { + TypeVariableName( + name = nameResolver.getString(it.name), + bounds = *possibleBounds.toTypedArray(), + variance = it.varianceModifier) + } + return@map typeVar.reified(it.reified) + }.associateBy({ it.name }, { it }) + } + + private val TypeParameter.varianceModifier: KModifier? + get() { + return variance.asKModifier().let { + // We don't redeclare out variance here + if (it == KModifier.OUT) { + null + } else { + it + } + } + } + + } +} diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/TypeResolver.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/TypeResolver.kt new file mode 100644 index 0000000..8c193ad --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/TypeResolver.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.laidpack.typescript.codegen.moshi + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.WildcardTypeName +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy + +/** + * Resolves bodyType parameters against a bodyType declaration. Use this to fill in bodyType variables with + * their actual bodyType parameters. + */ +internal open class TypeResolver { + open fun resolveTypeVariable(typeVariable: TypeVariableName): TypeName = typeVariable + + fun resolve(typeName: TypeName): TypeName { + return when (typeName) { + is ClassName -> typeName + + is ParameterizedTypeName -> { + typeName.rawType.parameterizedBy(*(typeName.typeArguments.map { resolve(it) }.toTypedArray())) + .asNullableIf(typeName.nullable) + } + + is WildcardTypeName -> { + when { + typeName.lowerBounds.size == 1 -> { + WildcardTypeName.supertypeOf(resolve(typeName.lowerBounds[0])) + .asNullableIf(typeName.nullable) + } + typeName.upperBounds.size == 1 -> { + WildcardTypeName.subtypeOf(resolve(typeName.upperBounds[0])) + .asNullableIf(typeName.nullable) + } + else -> { + throw IllegalArgumentException( + "Unrepresentable wildcard bodyType. Cannot have more than one bound: $typeName") + } + } + } + + is TypeVariableName -> resolveTypeVariable(typeName) + + else -> throw IllegalArgumentException("Unrepresentable bodyType: $typeName") + } + } +} diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/kotlintypes.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/kotlintypes.kt new file mode 100644 index 0000000..de9c152 --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/kotlintypes.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.laidpack.typescript.codegen.moshi + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.TypeName + +internal fun TypeName.rawType(): ClassName { + return when (this) { + is ClassName -> this + is ParameterizedTypeName -> rawType + else -> throw IllegalArgumentException("Cannot get raw bodyType from $this") + } +} + +internal fun TypeName.asNullableIf(condition: Boolean): TypeName { + return if (condition) asNullable() else this +} diff --git a/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/metadata.kt b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/metadata.kt new file mode 100644 index 0000000..a3ffa5f --- /dev/null +++ b/codegen-impl/bin/main/com/laidpack/typescript/codegen/moshi/metadata.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.laidpack.typescript.codegen.moshi + +import com.squareup.kotlinpoet.ANY +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.WildcardTypeName +import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Type +import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter +import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter.Variance +import me.eugeniomarletti.kotlin.metadata.shadow.metadata.deserialization.NameResolver + +internal fun TypeParameter.asTypeName( + nameResolver: NameResolver, + getTypeParameter: (index: Int) -> TypeParameter, + resolveAliases: Boolean = false +): TypeVariableName { + val possibleBounds = upperBoundList.map { + it.asTypeName(nameResolver, getTypeParameter, resolveAliases) + } + return if (possibleBounds.isEmpty()) { + TypeVariableName( + name = nameResolver.getString(name), + variance = variance.asKModifier()) + } else { + TypeVariableName( + name = nameResolver.getString(name), + bounds = *possibleBounds.toTypedArray(), + variance = variance.asKModifier()) + } +} + +internal fun TypeParameter.Variance.asKModifier(): KModifier? { + return when (this) { + Variance.IN -> KModifier.IN + Variance.OUT -> KModifier.OUT + Variance.INV -> null + } +} + +/** + * Returns the TypeName of this bodyType as it would be seen in the source code, including nullability + * and generic bodyType parameters. + * + * @param [nameResolver] a [NameResolver] instance from the source proto + * @param [getTypeParameter] a function that returns the bodyType parameter for the given index. **Only + * called if [ProtoBuf.Type.hasTypeParameter] is true!** + */ +internal fun Type.asTypeName( + nameResolver: NameResolver, + getTypeParameter: (index: Int) -> TypeParameter, + useAbbreviatedType: Boolean = true +): TypeName { + + val argumentList = when { + useAbbreviatedType && hasAbbreviatedType() -> abbreviatedType.argumentList + else -> argumentList + } + + if (hasFlexibleUpperBound()) { + return WildcardTypeName.subtypeOf( + flexibleUpperBound.asTypeName(nameResolver, getTypeParameter, useAbbreviatedType)) + .asNullableIf(nullable) + } else if (hasOuterType()) { + return WildcardTypeName.supertypeOf( + outerType.asTypeName(nameResolver, getTypeParameter, useAbbreviatedType)) + .asNullableIf(nullable) + } + + val realType = when { + hasTypeParameter() -> return getTypeParameter(typeParameter) + .asTypeName(nameResolver, getTypeParameter, useAbbreviatedType) + .asNullableIf(nullable) + hasTypeParameterName() -> typeParameterName + useAbbreviatedType && hasAbbreviatedType() -> abbreviatedType.typeAliasName + else -> className + } + + var typeName: TypeName = + ClassName.bestGuess(nameResolver.getString(realType) + .replace("/", ".")) + + if (argumentList.isNotEmpty()) { + val remappedArgs: Array = argumentList.map { argumentType -> + val nullableProjection = if (argumentType.hasProjection()) { + argumentType.projection + } else null + if (argumentType.hasType()) { + argumentType.type.asTypeName(nameResolver, getTypeParameter, useAbbreviatedType) + .let { argumentTypeName -> + nullableProjection?.let { projection -> + when (projection) { + Type.Argument.Projection.IN -> WildcardTypeName.supertypeOf(argumentTypeName) + Type.Argument.Projection.OUT -> { + if (argumentTypeName == ANY) { + // This becomes a *, which we actually don't want here. + // List works with List<*>, but List<*> doesn't work with List + argumentTypeName + } else { + WildcardTypeName.subtypeOf(argumentTypeName) + } + } + Type.Argument.Projection.STAR -> WildcardTypeName.STAR + Type.Argument.Projection.INV -> TODO("INV projection is unsupported") + } + } ?: argumentTypeName + } + } else { + WildcardTypeName.STAR + } + }.toTypedArray() + typeName = (typeName as ClassName).parameterizedBy(*remappedArgs) + } + + return typeName.asNullableIf(nullable) +} diff --git a/codegen-impl/bin/test/com/laidpack/typescript/codegen/TypeScriptTypeTransformerTest.kt b/codegen-impl/bin/test/com/laidpack/typescript/codegen/TypeScriptTypeTransformerTest.kt new file mode 100644 index 0000000..02e0e8f --- /dev/null +++ b/codegen-impl/bin/test/com/laidpack/typescript/codegen/TypeScriptTypeTransformerTest.kt @@ -0,0 +1,113 @@ +package com.laidpack.typescript.codegen + +import org.amshove.kluent.`it returns` +import org.amshove.kluent.`should be equal to` +import org.amshove.kluent.shouldEqual +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.`when` + +internal class TypeScriptTypeTransformerTest { + private lateinit var mockedBodyType : WrappedBodyType + + @Before + fun setUp() { + mockedBodyType = Mockito.mock(WrappedBodyType::class.java) + } + @Test + fun `transformType - given type List'string, return Array'string in TS`() { + val mockedStringType = Mockito.mock(WrappedBodyType::class.java) + Mockito.`when`(mockedBodyType.isIterable).`it returns`(true) + Mockito.`when`(mockedBodyType.hasParameters).`it returns`(true) + Mockito.`when`(mockedBodyType.parameters).`it returns`(mapOf("String" to mockedStringType)) + Mockito.`when`(mockedStringType.hasParameters).`it returns`(false) + Mockito.`when`(mockedStringType.canonicalName).`it returns`(String::class.java.canonicalName) + + val transformer = TypeScriptTypeTransformer() + val result = transformer.transformType(mockedBodyType, setOf(), mapOf()) + result `should be equal to` "Array" + } + + @Test + fun `transformType - given type HashMap'String'T with declared type var T, return { |string-key| T}`() { + // Assemble + Mockito.`when`(mockedBodyType.isMap).`it returns`(true) + Mockito.`when`(mockedBodyType.hasParameters).`it returns`(true) + val mockedStringType = Mockito.mock(WrappedBodyType::class.java) + Mockito.`when`(mockedStringType.canonicalName).thenReturn(String::class.java.canonicalName) + val mockedTypeVariableType = Mockito.mock(WrappedBodyType::class.java) + Mockito.`when`(mockedTypeVariableType.isTypeVariable).thenReturn(true) + Mockito.`when`(mockedTypeVariableType.name).thenReturn("T") + Mockito.`when`(mockedTypeVariableType.hasParameters).`it returns`(false) + + Mockito.`when`(mockedBodyType.parameters).`it returns`(mapOf( + "String" to mockedStringType, + "T" to mockedTypeVariableType + )) + `when`(mockedBodyType.firstParameterType).thenReturn(mockedStringType) + `when`(mockedBodyType.secondParameterType).thenReturn(mockedTypeVariableType) + + // Act + val transformer = TypeScriptTypeTransformer() + val result = transformer.transformType(mockedBodyType, setOf(), mapOf("T" to mockedTypeVariableType)) + + // Assert + result shouldEqual "{ [key: string]: T }" + + } + + @Test + fun `transformType - given type MutableList'Int, return Array'number'`() { + // Assemble + Mockito.`when`(mockedBodyType.isIterable).`it returns`(true) + Mockito.`when`(mockedBodyType.hasParameters).`it returns`(true) + val mockedIntType = Mockito.mock(WrappedBodyType::class.java) + Mockito.`when`(mockedIntType.canonicalName).thenReturn(Int::class.java.canonicalName) + + Mockito.`when`(mockedBodyType.parameters).`it returns`(mapOf( + Int::class.java.simpleName to mockedIntType + )) + `when`(mockedBodyType.firstParameterType).thenReturn(mockedIntType) + + // Act + val transformer = TypeScriptTypeTransformer() + val result = transformer.transformType(mockedBodyType, setOf(), mapOf()) + + // Assert + result shouldEqual "Array" + } + + @Test + fun `transformType - given int annotated with Test, return custom value transformed string`() { + // Assemble + Mockito.`when`(mockedBodyType.canonicalName).`it returns`(Int::class.java.canonicalName) + Mockito.`when`(mockedBodyType.isPrimitiveOrStringType).`it returns`(true) + Mockito.`when`(mockedBodyType.annotationNames).`it returns`(setOf("Test")) + + // Act + val customValueTransformer = TypeTransformer({ t -> t.annotationNames.contains("Test")}, "string", Nullability.NoTransform) + val transformer = TypeScriptTypeTransformer(listOf(customValueTransformer)) + val result = transformer.transformType(mockedBodyType, setOf(), mapOf()) + + // Assert + result shouldEqual "string" + } + + @Test + fun `transformType - given non-nullable int annotated with Test, return transformed nullability as null`() { + // Assemble + Mockito.`when`(mockedBodyType.canonicalName).`it returns`(Int::class.java.canonicalName) + Mockito.`when`(mockedBodyType.isPrimitiveOrStringType).`it returns`(true) + Mockito.`when`(mockedBodyType.annotationNames).`it returns`(setOf("Test")) + + // Act + val customValueTransformer = TypeTransformer({ t -> t.annotationNames.contains("Test")}, "string", Nullability.Null) + val transformer = TypeScriptTypeTransformer(listOf(customValueTransformer)) + val result = transformer.isNullable(mockedBodyType) + + // Assert + result shouldEqual true + } + +} \ No newline at end of file diff --git a/codegen-impl/bin/test/com/laidpack/typescript/codegen/WrappedBodyTypeTest.kt b/codegen-impl/bin/test/com/laidpack/typescript/codegen/WrappedBodyTypeTest.kt new file mode 100644 index 0000000..1f753d7 --- /dev/null +++ b/codegen-impl/bin/test/com/laidpack/typescript/codegen/WrappedBodyTypeTest.kt @@ -0,0 +1,227 @@ +@file:Suppress("UNUSED_ANONYMOUS_PARAMETER") + +package com.laidpack.typescript.codegen + +import com.laidpack.typescript.codegen.moshi.rawType +import com.squareup.kotlinpoet.* +import org.amshove.kluent.shouldEqual +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import java.util.LinkedHashSet +import javax.lang.model.element.TypeElement +import javax.lang.model.type.TypeKind +import javax.lang.model.type.TypeMirror +import javax.lang.model.util.Elements +import javax.lang.model.util.Types + +internal class WrappedBodyTypeTest { + private val emptyTypeVariablesMap = HashMap() + private lateinit var mockedElements: Elements + private lateinit var mockedTypes: Types + private lateinit var mockedContext: TargetContext + private var mockedTypeElement = Mockito.mock(TypeElement::class.java) + + @Before + fun setUp() { + mockedElements = Mockito.mock(Elements::class.java) + mockedTypes = Mockito.mock(Types::class.java) + mockedContext = Mockito.mock(TargetContext::class.java) + val mockedTypeMirror = Mockito.mock(TypeMirror::class.java) + + Mockito.`when`(mockedTypeElement.asType()).thenReturn(mockedTypeMirror) + Mockito.`when`(mockedTypeMirror.kind).thenReturn(TypeKind.OTHER) + } + + @Test + fun `performActionsOnCurrentAndNestedTypes - given type C'A'List'String, then return C, A, List, and String,`() { + // given + val mockedType = Mockito.mock(WrappedBodyType::class.java) + val mockedSubTemplateType = Mockito.mock(WrappedBodyType::class.java) + val mockedSubListType = Mockito.mock(WrappedBodyType::class.java) + val mockedStringType = Mockito.mock(WrappedBodyType::class.java) + Mockito.`when`(mockedType.name).thenReturn("C") + Mockito.`when`(mockedType.hasParameters).thenReturn(true) + Mockito.`when`(mockedType.parameters).thenReturn(hashMapOf("A" to mockedSubTemplateType)) + + Mockito.`when`(mockedSubTemplateType.name).thenReturn("A") + Mockito.`when`(mockedSubTemplateType.hasParameters).thenReturn(true) + Mockito.`when`(mockedSubTemplateType.parameters).thenReturn(hashMapOf("List" to mockedSubListType)) + + Mockito.`when`(mockedSubListType.name).thenReturn("List") + Mockito.`when`(mockedSubListType.hasParameters).thenReturn(true) + Mockito.`when`(mockedSubListType.parameters).thenReturn(hashMapOf("String" to mockedStringType)) + + Mockito.`when`(mockedStringType.name).thenReturn("String") + Mockito.`when`(mockedStringType.hasParameters).thenReturn(false) + + val list = mutableListOf() + val action = { bodyType: WrappedBodyType, context: TargetContext-> list.add(bodyType.name!!)} + WrappedBodyType.performActionsOnTypeAndItsNestedTypes(mockedType, mockedContext, action) + + list[0] shouldEqual "C" + list[1] shouldEqual "A" + list[2] shouldEqual "List" + list[3] shouldEqual "String" + } + + @Test + fun `resolvePropertyType - given type List, return CollectionType'Iterable`() { + val mockedTypeName = Mockito.mock(ClassName::class.java) + Mockito.`when`(mockedTypeName.rawType().canonicalName).thenReturn(List::class.java.canonicalName) + Mockito.`when`(mockedTypeName.rawType().simpleName).thenReturn(List::class.java.simpleName) + + val wrappedType = WrappedBodyType.resolvePropertyType (mockedTypeName, null, emptyTypeVariablesMap, mockedContext) + + wrappedType.collectionType shouldEqual CollectionType.Iterable + } + + @Test + fun `resolvePropertyType - given type MutableList, return CollectionType'Iterable`() { + val mockedTypeName = Mockito.mock(ClassName::class.java) + Mockito.`when`(mockedTypeName.rawType().canonicalName).thenReturn(MutableList::class.java.canonicalName) + Mockito.`when`(mockedTypeName.rawType().simpleName).thenReturn(MutableList::class.java.simpleName) + + val wrappedType = WrappedBodyType.resolvePropertyType (mockedTypeName, null, emptyTypeVariablesMap, mockedContext) + + wrappedType.collectionType shouldEqual CollectionType.Iterable + } + + @Test + fun `resolvePropertyType - given type HashMap, return CollectionType'Map`() { + val mockedTypeName = Mockito.mock(ClassName::class.java) + Mockito.`when`(mockedTypeName.canonicalName).thenReturn(HashMap::class.java.canonicalName) + Mockito.`when`(mockedTypeName.rawType().simpleName).thenReturn(HashMap::class.java.simpleName) + Mockito.`when`(mockedElements.getTypeElement(HashMap::class.java.canonicalName)).thenReturn(mockedTypeElement) + + WrappedBodyType.getSuperTypeNames = { a, b -> + HashMap::class.supertypes.map { + it.asTypeName() + } + } + + val wrappedType = WrappedBodyType.resolvePropertyType (mockedTypeName, null, emptyTypeVariablesMap, mockedContext) + + wrappedType.collectionType shouldEqual CollectionType.Map + } + + @Test + fun `resolvePropertyType - given type LinkedHashMap, return CollectionType'Map`() { + val mockedTypeName = Mockito.mock(ClassName::class.java) + Mockito.`when`(mockedTypeName.canonicalName).thenReturn(LinkedHashMap::class.java.canonicalName) + Mockito.`when`(mockedTypeName.rawType().simpleName).thenReturn(LinkedHashMap::class.java.simpleName) + Mockito.`when`(mockedElements.getTypeElement(LinkedHashMap::class.java.canonicalName)).thenReturn(mockedTypeElement) + + WrappedBodyType.getSuperTypeNames = { a, b -> + LinkedHashMap::class.supertypes.map { + it.asTypeName() + } + } + + val wrappedType = WrappedBodyType.resolvePropertyType (mockedTypeName, null, emptyTypeVariablesMap, mockedContext) + + wrappedType.collectionType shouldEqual CollectionType.Map + } + + + @Test + fun `resolvePropertyType - given type LinkedHashSet, return CollectionType'Set`() { + val mockedTypeName = Mockito.mock(ClassName::class.java) + Mockito.`when`(mockedTypeName.canonicalName).thenReturn(LinkedHashSet::class.java.canonicalName) + Mockito.`when`(mockedTypeName.rawType().simpleName).thenReturn(LinkedHashSet::class.java.simpleName) + Mockito.`when`(mockedElements.getTypeElement(LinkedHashSet::class.java.canonicalName)).thenReturn(mockedTypeElement) + + WrappedBodyType.getSuperTypeNames = { a, b -> + LinkedHashSet::class.supertypes.map { + it.asTypeName() + } + } + WrappedBodyType.getMirror = { a, b -> + Mockito.mock(TypeMirror::class.java) + } + + val wrappedType = WrappedBodyType.resolvePropertyType (mockedTypeName, null, emptyTypeVariablesMap, mockedContext) + + wrappedType.collectionType shouldEqual CollectionType.Set + } + + + @Test + fun `resolvePropertyType - given type Pair, return CollectionType'Pair`() { + val mockedTypeName = Mockito.mock(ClassName::class.java) + Mockito.`when`(mockedTypeName.rawType().canonicalName).thenReturn(Pair::class.java.canonicalName) + Mockito.`when`(mockedTypeName.rawType().simpleName).thenReturn(Pair::class.java.simpleName) + + val wrappedType = WrappedBodyType.resolvePropertyType (mockedTypeName, null, emptyTypeVariablesMap, mockedContext) + + wrappedType.collectionType shouldEqual CollectionType.Pair + } + + + @Test + fun `resolvePropertyType - given type HashMap''String'T'', return wrapped type with two params + last var returns type variable`() { + // assemble + val mockedtypeArgument1 = Mockito.mock(ClassName::class.java) + Mockito.`when`(mockedtypeArgument1.rawType().simpleName).thenReturn(String::class.java.simpleName) + val mockedTypeArgument2 = Mockito.mock(TypeVariableName::class.java) + Mockito.`when`(mockedTypeArgument2.name).thenReturn("T") + val mockedClassName = Mockito.mock(ClassName::class.java) + Mockito.`when`(mockedClassName.canonicalName).thenReturn(HashMap::class.java.canonicalName) + Mockito.`when`(mockedClassName.simpleName).thenReturn(HashMap::class.java.simpleName) + val mockedTypeName = Mockito.mock(ParameterizedTypeName::class.java) + Mockito.`when`(mockedTypeName.rawType()).thenReturn(mockedClassName) + Mockito.`when`(mockedTypeName.typeArguments).thenReturn(listOf(mockedtypeArgument1, mockedTypeArgument2)) + + Mockito.`when`(mockedElements.getTypeElement(HashMap::class.java.canonicalName)).thenReturn(mockedTypeElement) + WrappedBodyType.getSuperTypeNames = { a, b -> + HashMap::class.supertypes.map { + it.asTypeName() + } + } + WrappedBodyType.getMirror = { a, b -> + Mockito.mock(TypeMirror::class.java) + } + + // act + val wrappedType = WrappedBodyType.resolvePropertyType (mockedTypeName, null, emptyTypeVariablesMap, mockedContext) + + // assert + wrappedType.collectionType shouldEqual CollectionType.Map + wrappedType.parameters.size shouldEqual 2 + wrappedType.parameters.containsKey(String::class.java.simpleName) shouldEqual true + wrappedType.parameters.containsKey("T") shouldEqual true + wrappedType.parameters["T"]?.isTypeVariable shouldEqual true + } + + @Test + fun `resolvePropertyType - given type MutableList''Int'', return CollectionType'Iterable + param = Int`() { + // assemble + val mockedTypeArgument1 = Mockito.mock(ClassName::class.java) + Mockito.`when`(mockedTypeArgument1.rawType().simpleName).thenReturn(Int::class.java.simpleName) + val mockedClassName = Mockito.mock(ClassName::class.java) + Mockito.`when`(mockedClassName.canonicalName).thenReturn(MutableList::class.java.canonicalName) + Mockito.`when`(mockedClassName.simpleName).thenReturn(MutableList::class.java.simpleName) + val mockedTypeName = Mockito.mock(ParameterizedTypeName::class.java) + Mockito.`when`(mockedTypeName.rawType()).thenReturn(mockedClassName) + Mockito.`when`(mockedTypeName.typeArguments).thenReturn(listOf(mockedTypeArgument1)) + + Mockito.`when`(mockedElements.getTypeElement(MutableList::class.java.canonicalName)).thenReturn(mockedTypeElement) + WrappedBodyType.getSuperTypeNames = { a, b -> + MutableList::class.supertypes.map { + it.asTypeName() + } + } + WrappedBodyType.getMirror = { a, b -> + Mockito.mock(TypeMirror::class.java) + } + + // act + val wrappedType = WrappedBodyType.resolvePropertyType (mockedTypeName, null, emptyTypeVariablesMap, mockedContext) + + // assert + wrappedType.collectionType shouldEqual CollectionType.Iterable + wrappedType.parameters.size shouldEqual 1 + wrappedType.parameters.containsKey(Int::class.java.simpleName) shouldEqual true + } + +} \ No newline at end of file diff --git a/codegen-impl/bin/test/mockito-extensions/org.mockito.plugins.MockMaker b/codegen-impl/bin/test/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..ca6ee9c --- /dev/null +++ b/codegen-impl/bin/test/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/BaseTypeScriptProcessor.kt b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/BaseTypeScriptProcessor.kt index 72dc45c..c889627 100644 --- a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/BaseTypeScriptProcessor.kt +++ b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/BaseTypeScriptProcessor.kt @@ -24,33 +24,59 @@ typealias FileProcessor = ( rootPackageNames: Set, packageNames: Set ) -> String? -abstract class BaseTypeScriptProcessor( - private val customTransformers: List = listOf(), - private val constrainToCurrentModulePackage: Boolean = false, - private val filePreProcessors: List = listOf(), - private val filePostProcessors: List = listOf(), - private val definitionPreProcessors: List = listOf(), - private val definitionPostProcessors: List = listOf() -) : KotlinAbstractProcessor(), KotlinMetadataUtils { +typealias SuperTypeTransformer = (superClassName: ClassName, currentModuleName: String) -> String +typealias DefinitionTypeTransformer = (className: ClassName) -> String + +abstract class BaseTypeScriptProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils { private val annotation = TypeScript::class.java private var moduleName: String = "NativeTypes" + private var namespace: String? = null private var indent: String = " " private var customOutputDir: String? = null private var fileName = "types.d.ts" - private var shouldAppendToFile = false + private lateinit var moduleOption: ModuleOption + private lateinit var name: String + protected open val customTransformers: List = listOf() + protected open val filePreProcessors: List = listOf() + protected open val filePostProcessors: List = listOf() + protected open val definitionPreProcessors: List = listOf() + protected open val definitionPostProcessors: List = listOf() + protected open val definitionTypeTransformer: DefinitionTypeTransformer = { c -> c.simpleName} + protected open val superTypeTransformer: SuperTypeTransformer = { c, _ -> c.simpleName} + protected open val constrainToCurrentModulePackage: Boolean = false + protected open val exportDefinitions: Boolean = false + protected open val inAmbientDefinitionFile: Boolean = true + override fun getSupportedAnnotationTypes() = setOf(annotation.canonicalName) override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest() - override fun getSupportedOptions() = setOf(OPTION_MODULE, OPTION_OUTPUTDIR, OPTION_INDENT, kaptGeneratedOption) + override fun getSupportedOptions() = setOf( + OPTION_MODULE, OPTION_NAMESPACE, OPTION_OUTPUTDIR, OPTION_INDENT, OPTION_FILENAME, kaptGeneratedOption + ) override fun init(processingEnv: ProcessingEnvironment) { super.init(processingEnv) moduleName = processingEnv.options[OPTION_MODULE] ?: moduleName + namespace = processingEnv.options[OPTION_NAMESPACE] indent = processingEnv.options[OPTION_INDENT] ?: indent customOutputDir = processingEnv.options[OPTION_OUTPUTDIR] fileName = processingEnv.options[OPTION_FILENAME] ?: fileName + when { + processingEnv.options[OPTION_MODULE] != null -> { + moduleOption = ModuleOption.Namespace + name = moduleName + } + processingEnv.options[OPTION_NAMESPACE] != null -> { + moduleOption = ModuleOption.Namespace + name = namespace as String + } + else -> { + ModuleOption.None + name = "" + } + } } override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { @@ -73,7 +99,8 @@ abstract class BaseTypeScriptProcessor( if (targetedTypes.isNotEmpty()) { val content = TypeScriptGenerator.generate( - moduleName, + name, + moduleOption, targetedTypes, indent, customTransformers, @@ -83,7 +110,11 @@ abstract class BaseTypeScriptProcessor( filePreProcessors, filePostProcessors, definitionPreProcessors, - definitionPostProcessors + definitionPostProcessors, + definitionTypeTransformer, + superTypeTransformer, + exportDefinitions, + inAmbientDefinitionFile ) var outputDir : String = customOutputDir ?: options[kaptGeneratedOption] ?: System.getProperty("user.dir") if (!outputDir.endsWith(File.separator)) @@ -95,21 +126,20 @@ abstract class BaseTypeScriptProcessor( return false } - val file = File(outputDir, fileName) - file.createNewFile() // overwrite any existing file - if (!shouldAppendToFile) { - file.writeText(content) - shouldAppendToFile = true - } else { - file.appendText(content) - } - - messager.printMessage(Diagnostic.Kind.OTHER, "TypeScript definitions saved at $outputDir$fileName") + writeFile(outputDir, fileName, content) } return true } + open fun writeFile(outputDir: String, fileName: String, content: String) { + val file = File(outputDir, fileName) + file.createNewFile() // overwrite any existing file + file.writeText(content) + + messager.printMessage(Diagnostic.Kind.OTHER, "TypeScript definitions saved at $file") + } + private fun createContext(): TargetContext { return TargetContext( messager, @@ -122,10 +152,11 @@ abstract class BaseTypeScriptProcessor( ) } companion object { - private const val OPTION_MODULE = "typescript.module" - private const val OPTION_OUTPUTDIR = "typescript.outputDir" - private const val OPTION_INDENT = "typescript.indent" - private const val OPTION_FILENAME = "typescript.filename" + const val OPTION_MODULE = "typescript.module" + const val OPTION_NAMESPACE= "typescript.namespace" + const val OPTION_OUTPUTDIR = "typescript.outputDir" + const val OPTION_INDENT = "typescript.indent" + const val OPTION_FILENAME = "typescript.filename" } } diff --git a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/IWrappedBodyType.kt b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/IWrappedBodyType.kt index 109c176..9447847 100644 --- a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/IWrappedBodyType.kt +++ b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/IWrappedBodyType.kt @@ -12,7 +12,7 @@ interface IWrappedBodyType { val isEnumValue: Boolean val isBound: Boolean val parameters: Map - val annotationNames: Set + val annotations: Map> val hasRawType: Boolean val isInstantiable: Boolean val nullable: Boolean diff --git a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TargetResolver.kt b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TargetResolver.kt index 0da8606..ff8c922 100644 --- a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TargetResolver.kt +++ b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TargetResolver.kt @@ -1,6 +1,8 @@ package com.laidpack.typescript.codegen import com.laidpack.typescript.codegen.moshi.TargetType +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.asTypeName import javax.lang.model.element.Element import javax.tools.Diagnostic @@ -20,6 +22,7 @@ internal object TargetResolver { // 2. super classes --> see TargetType.resolveSuperTypes // 3. bounds in bodyType variables - e.g., bodyType Y in class X { val test = T } --> see WrappedBodyType.resolveGenericClassDeclaration // TODO: don't capture target types in the context instance (see typesToBeAddedToScope) + // TODO: normalize types and clean up ;-) **/ context.targetingTypscriptAnnotatedType = false context.abortOnError = false @@ -44,8 +47,8 @@ internal object TargetResolver { val type = TargetType.get(element, context) if (type != null) { - context.typesWithinScope.add(type.name.simpleName) - if (context.targetingTypscriptAnnotatedType) context.typesWithTypeScriptAnnotation.add(type.name.simpleName) + context.typesWithinScope.add(type.name.canonicalName) + if (context.targetingTypscriptAnnotatedType) context.typesWithTypeScriptAnnotation.add(type.name.canonicalName) } return type @@ -53,15 +56,14 @@ internal object TargetResolver { private fun isDuplicateType(element: Element, context: TargetContext): Boolean { - val name = element.simpleName.toString() - if (context.typesWithinScope.contains(name)) { + val typeName = element.asType().asTypeName() + if (typeName is ClassName && context.typesWithinScope.contains(typeName.canonicalName)) { // error on duplicated annotated types - if (context.typesWithTypeScriptAnnotation.contains(name) && context.targetingTypscriptAnnotatedType) { - context.messager.printMessage(Diagnostic.Kind.ERROR, "Multiple types with a duplicate name: '${element.simpleName}'. Please rename or remove the @TypeScript annotation?") + if (context.typesWithTypeScriptAnnotation.contains(typeName.canonicalName) && context.targetingTypscriptAnnotatedType) { + context.messager.printMessage(Diagnostic.Kind.ERROR, "Multiple types with a duplicate name: '${typeName.canonicalName}'. Please rename or remove the @TypeScript annotation?") } return true// ignore duplicate base types } - return false } diff --git a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TypeScriptGenerator.kt b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TypeScriptGenerator.kt index 6dc1b6d..9f7ae17 100644 --- a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TypeScriptGenerator.kt +++ b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TypeScriptGenerator.kt @@ -7,11 +7,20 @@ import java.util.* /** Generates a JSON adapter for a target bodyType. */ +internal enum class ModuleOption { + Module, + Namespace, + None +} internal class TypeScriptGenerator private constructor ( target: ITargetType, private val typesWithinScope: Set, - customTransformers: List = listOf() + customTransformers: List = listOf(), + private val currentModuleName: String, + private val definitionTypeTransformer: DefinitionTypeTransformer, + private val superTypeTransformer: SuperTypeTransformer, + exportDefinitions: Boolean ) { private val className = target.name private val typeVariables = target.typeVariables @@ -20,41 +29,46 @@ internal class TypeScriptGenerator private constructor ( val output by lazy {generateDefinition()} private val propertiesOrEnumValues = target.propertiesOrEnumValues.values private val transformer = TypeScriptTypeTransformer(customTransformers) + private val export = if (exportDefinitions) "export " else "" override fun toString(): String { return output } + + private fun generateDefinition(): String { + return if (isEnum) { + generateEnum() + } else { + generateInterface() + } + } + private fun generateInterface(): String { val extendsString = generateExtends() val templateParameters = generateTypeVariables() - + val interfaceName = definitionTypeTransformer(className) val properties= generateProperties() - return "${indent}interface ${className.simpleName}$templateParameters$extendsString {\n" + + return "$indent${export}interface $interfaceName$templateParameters$extendsString {\n" + properties + "$indent}\n" } - private fun generateProperties(): String { - return propertiesOrEnumValues - .joinToString ("") { property -> - val propertyName = property.jsonName() - val propertyType = transformer.transformType(property.bodyType, typesWithinScope, typeVariables) - val isNullable = if (transformer.isNullable(property.bodyType)) "?" else "" - "$indent$indent$propertyName$isNullable: $propertyType;\n" - } - } - private fun generateEnum(): String { - val enumValues= propertiesOrEnumValues.joinToString(", ") { enumValue -> - "'${enumValue.jsonName()}'" - } - return "${indent}enum ${className.simpleName} { $enumValues }\n" + val enumValues= propertiesOrEnumValues + .sortedBy { it.jsonName() } + .joinToString(",\n") { enumValue -> + "${indent+indent}${enumValue.jsonName()} = '${enumValue.jsonName()}'" + } + val enumName = definitionTypeTransformer(className) + return "$indent${export}enum $enumName {\n" + + "$enumValues\n" + + "$indent}\n" } private fun generateExtends(): String { return if (superTypes.isNotEmpty()) { - " extends " + superTypes.joinToString(", ") { it.element.simpleName } + " extends " + superTypes.joinToString(", ") { superTypeTransformer(it.className, currentModuleName) } } else "" } @@ -79,19 +93,21 @@ internal class TypeScriptGenerator private constructor ( return "" } - - private fun generateDefinition(): String { - return if (isEnum) { - generateEnum() - } else { - generateInterface() - } + private fun generateProperties(): String { + return propertiesOrEnumValues + .joinToString ("") { property -> + val propertyName = property.jsonName() + val propertyType = transformer.transformType(property.bodyType, typesWithinScope, typeVariables) + val isNullable = if (transformer.isNullable(property.bodyType)) "?" else "" + "$indent$indent$propertyName$isNullable: $propertyType;\n" + } } companion object { private var indent = " " fun generate( moduleName: String, + moduleOption: ModuleOption, targetTypes: HashMap, indent: String, customTransformers: List, @@ -101,7 +117,11 @@ internal class TypeScriptGenerator private constructor ( filePreProcessors: List, filePostProcessors: List, definitionPreProcessors: List, - definitionPostProcessors: List + definitionPostProcessors: List, + definitionTypeTransformer: DefinitionTypeTransformer, + superTypeTransformer: SuperTypeTransformer, + exportDefinitions: Boolean, + inAmbientDefinitionFile: Boolean ): String { this.indent = indent val targetTypeNames = targetTypes.keys @@ -112,7 +132,11 @@ internal class TypeScriptGenerator private constructor ( val generatedTypeScript = TypeScriptGenerator( targetType, targetTypeNames, - customTransformers + customTransformers, + moduleName, + definitionTypeTransformer, + superTypeTransformer, + exportDefinitions ) addAnyProcessedDefinitions(targetType, definitionPreProcessors, definitions) definitions.add(generatedTypeScript.output) @@ -121,9 +145,17 @@ internal class TypeScriptGenerator private constructor ( } val timestamp = "/* generated @ ${LocalDateTime.now()} */\n" val customBeginStatements = getAnyProcessedFileStatements(targetTypes, rootPackageNames, packageNames, filePreProcessors) - val moduleStart = "declare module \"$moduleName\" {\n" - val moduleContent = definitions.joinToString("\n") - val moduleEnd = "}\n" + val declare = if (inAmbientDefinitionFile) { + "declare " + } else "" + val export = if (exportDefinitions) "export " else "" + val moduleStart = when(moduleOption) { + ModuleOption.Module -> "$export${declare}module \"$moduleName\" {\n" + ModuleOption.Namespace -> "$export${declare}namespace $moduleName {\n" + else -> "" + } + val moduleContent = definitions.joinToString("") + val moduleEnd = if (moduleOption == ModuleOption.None) "" else "}\n" val customEndStatements = getAnyProcessedFileStatements(targetTypes, rootPackageNames, packageNames, filePostProcessors) return "$timestamp$customBeginStatements$moduleStart$moduleContent$moduleEnd$customEndStatements" @@ -136,7 +168,7 @@ internal class TypeScriptGenerator private constructor ( ): Boolean { return !constrainToCurrentModulePackage || rootPackageNames.contains(targetType.name.packageName) - || rootPackageNames.any { targetType.name.packageName.startsWith(it) } + || rootPackageNames.any { targetType.name.packageName.startsWith("$it.") } } private fun addAnyProcessedDefinitions( targetType: ITargetType, diff --git a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TypeScriptTypeTransformer.kt b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TypeScriptTypeTransformer.kt index 38161f9..7f9c77d 100644 --- a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TypeScriptTypeTransformer.kt +++ b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/TypeScriptTypeTransformer.kt @@ -7,9 +7,15 @@ enum class Nullability { } class TypeTransformer ( val predicate: (bodyType: IWrappedBodyType) -> Boolean, - val type: String, + val typeProvider: (bodyType: IWrappedBodyType) -> String, val nullable: Nullability -) +) { + constructor( + predicate: (bodyType: IWrappedBodyType) -> Boolean, + type: String, + nullable: Nullability + ) : this(predicate, {type}, nullable) +} internal class TypeScriptTypeTransformer( private val customTransformers: List = listOf() @@ -29,16 +35,19 @@ internal class TypeScriptTypeTransformer( fun transformType(bodyType: IWrappedBodyType, typesWithinScope: Set, bodyTypeVariables: Map): String { val customTransformer = customTransformers.find { t -> t.predicate(bodyType) } return when { - customTransformer != null -> customTransformer.type + customTransformer != null -> customTransformer.typeProvider(bodyType) bodyType.isWildCard -> "any" bodyType.name != null && typesWithinScope.contains(bodyType.name as String) -> "${bodyType.name}${transformTypeParameters(bodyType, typesWithinScope, bodyTypeVariables)}" bodyType.isReturningTypeVariable -> "${bodyType.name}" bodyType.isTypeVariable && bodyTypeVariables.containsKey(bodyType.name) -> "${bodyType.name}${transformTypeParameters(bodyType, typesWithinScope, bodyTypeVariables)}" else -> { val transformer = if (!bodyType.hasParameters) valueTransformers.find { t -> t.predicate(bodyType) } else null - transformer?.type - ?: transformCollectionType(bodyType, typesWithinScope, bodyTypeVariables) - ?: "any /* unknown bodyType */" + if (transformer != null) { + transformer.typeProvider(bodyType) + } else { + transformCollectionType(bodyType, typesWithinScope, bodyTypeVariables) + ?: "any /* unknown bodyType */" + } } } } diff --git a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/WrappedBodyType.kt b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/WrappedBodyType.kt index be7a864..ff49abd 100644 --- a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/WrappedBodyType.kt +++ b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/WrappedBodyType.kt @@ -26,7 +26,7 @@ internal class WrappedBodyType private constructor ( override val isEnumValue: Boolean, override val isBound: Boolean, override val parameters: Map, - override val annotationNames: Set, + override val annotations: Map>, private val _bounds: Map ) : IWrappedBodyType { override val hasRawType = typeName is ClassName || typeName is ParameterizedTypeName @@ -170,7 +170,7 @@ internal class WrappedBodyType private constructor ( isEnumValue, isBound, getParameterTypes(typeName), - getAnnotationNames(variableElement), + getAnnotations(variableElement), getBounds(isTypeVariable, typeName) ) injectJavaMirroredCanonicalName(wrappedType) @@ -190,16 +190,28 @@ internal class WrappedBodyType private constructor ( return parameters } - private fun getAnnotationNames(variableElement: VariableElement?): Set { - val annotationNames = mutableSetOf() + private fun getAnnotations(variableElement: VariableElement?): Map> { + val annotations = mutableMapOf>() if (variableElement != null) { - annotationNames.addAll( - variableElement.annotationMirrors.map { annotationMirror -> - annotationMirror.annotationType.asTypeName().toString() + for (annotationMirror in variableElement.annotationMirrors) { + val annotationMembers = mutableMapOf() + for (elementValue in annotationMirror.elementValues) { + val value = elementValue.value + val memberValue = when (value) { + is DeclaredType -> { + val typeName = value.asTypeName() + if (typeName is ClassName) { + typeName.canonicalName + } else typeName.toString() + } + else -> value.toString() } - ) + annotationMembers[elementValue.key.simpleName.toString()] = memberValue + } + annotations[annotationMirror.annotationType.asTypeName().toString()] = annotationMembers + } } - return annotationNames + return annotations } private fun getBounds(isTypeVariable: Boolean, typeName: TypeName): Map { @@ -239,11 +251,15 @@ internal class WrappedBodyType private constructor ( } private fun addTypeToScopeIfNewDeclaredType(bodyType: WrappedBodyType, context: TargetContext) { - if (!bodyType.isPrimitiveOrStringType && bodyType.isInstantiable && bodyType.collectionType == CollectionType.None - && !context.typesWithinScope.contains(bodyType.name) && !context.typesToBeAddedToScope.containsKey(bodyType.name)) { - val typeElement = context.elementUtils.getTypeElement(bodyType.canonicalName) + val canonicalName = bodyType.canonicalName + if (canonicalName != null + && !bodyType.isPrimitiveOrStringType + && bodyType.isInstantiable && bodyType.collectionType == CollectionType.None + && !context.typesWithinScope.contains(canonicalName) + && !context.typesToBeAddedToScope.containsKey(canonicalName)) { + val typeElement = context.elementUtils.getTypeElement(canonicalName) if (typeElement != null && typeElement.asType() is DeclaredType) { - context.typesToBeAddedToScope[bodyType.name!!] = typeElement + context.typesToBeAddedToScope[canonicalName] = typeElement } } diff --git a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/moshi/TargetType.kt b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/moshi/TargetType.kt index 3a1b917..a864a13 100644 --- a/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/moshi/TargetType.kt +++ b/codegen-impl/src/main/kotlin/com/laidpack/typescript/codegen/moshi/TargetType.kt @@ -69,13 +69,18 @@ internal data class TargetType( val proto = typeMetadata.data.classProto if (proto.classKind == Class.Kind.ENUM_CLASS) { - return getEnumTargetType(element, proto, typeMetadata, context) + return getEnumTargetType(element, proto, typeMetadata, context.targetingTypscriptAnnotatedType) } return getDeclaredTargetType(element, proto, typeMetadata, context) } - private fun getEnumTargetType(element: TypeElement, proto: Class, typeMetadata: KotlinClassMetadata, context: TargetContext): TargetType? { + private fun getEnumTargetType( + element: TypeElement, + proto: Class, + typeMetadata: KotlinClassMetadata, + targetingTypscriptAnnotatedType: Boolean + ): TargetType { val enumValues = declaredEnumValues(element, proto, typeMetadata) @@ -86,7 +91,7 @@ internal data class TargetType( else -> throw IllegalStateException("unexpected TypeName: ${typeName::class}") } - return TargetType(proto, element, name, enumValues, mapOf(), setOf(), context.targetingTypscriptAnnotatedType) + return TargetType(proto, element, name, enumValues, mapOf(), setOf(), targetingTypscriptAnnotatedType) } private fun getDeclaredTargetType(element: TypeElement, proto: Class, typeMetadata: KotlinClassMetadata, context: TargetContext): TargetType? { @@ -139,13 +144,14 @@ internal data class TargetType( } if (supertype.element.kotlinMetadata == null) { context.messager.printMessage(ERROR, - "@TypeScript can't be applied to ${appliedType.element.simpleName}: supertype $supertype is not a Kotlin bodyType", + "@TypeScript can't be applied to ${appliedType.element.simpleName}: supertype $supertype is not a Kotlin type", appliedType.element) return null } - if (supertype.element.asClassName() != appliedType.element.asClassName()) { + val superClassName = supertype.element.asClassName() + if (superClassName != appliedType.element.asClassName()) { selectedSuperTypes.add(supertype) - context.typesToBeAddedToScope[supertype.element.simpleName.toString()] = supertype.element + context.typesToBeAddedToScope[superClassName.canonicalName] = supertype.element } } return selectedSuperTypes diff --git a/codegen-impl/src/test/kotlin/com/laidpack/typescript/codegen/TypeScriptTypeTransformerTest.kt b/codegen-impl/src/test/kotlin/com/laidpack/typescript/codegen/TypeScriptTypeTransformerTest.kt index 02e0e8f..eacb18f 100644 --- a/codegen-impl/src/test/kotlin/com/laidpack/typescript/codegen/TypeScriptTypeTransformerTest.kt +++ b/codegen-impl/src/test/kotlin/com/laidpack/typescript/codegen/TypeScriptTypeTransformerTest.kt @@ -83,10 +83,10 @@ internal class TypeScriptTypeTransformerTest { // Assemble Mockito.`when`(mockedBodyType.canonicalName).`it returns`(Int::class.java.canonicalName) Mockito.`when`(mockedBodyType.isPrimitiveOrStringType).`it returns`(true) - Mockito.`when`(mockedBodyType.annotationNames).`it returns`(setOf("Test")) + Mockito.`when`(mockedBodyType.annotations).`it returns`(mapOf("Test" to mapOf())) // Act - val customValueTransformer = TypeTransformer({ t -> t.annotationNames.contains("Test")}, "string", Nullability.NoTransform) + val customValueTransformer = TypeTransformer({ t -> t.annotations.contains("Test")}, "string", Nullability.NoTransform) val transformer = TypeScriptTypeTransformer(listOf(customValueTransformer)) val result = transformer.transformType(mockedBodyType, setOf(), mapOf()) @@ -99,10 +99,10 @@ internal class TypeScriptTypeTransformerTest { // Assemble Mockito.`when`(mockedBodyType.canonicalName).`it returns`(Int::class.java.canonicalName) Mockito.`when`(mockedBodyType.isPrimitiveOrStringType).`it returns`(true) - Mockito.`when`(mockedBodyType.annotationNames).`it returns`(setOf("Test")) + Mockito.`when`(mockedBodyType.annotations).`it returns`(mapOf("Test" to mapOf())) // Act - val customValueTransformer = TypeTransformer({ t -> t.annotationNames.contains("Test")}, "string", Nullability.Null) + val customValueTransformer = TypeTransformer({ t -> t.annotations.contains("Test")}, "string", Nullability.Null) val transformer = TypeScriptTypeTransformer(listOf(customValueTransformer)) val result = transformer.isNullable(mockedBodyType)