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)