Skip to content

Commit

Permalink
Issue ntrrgc#4 - Support for sealed classes
Browse files Browse the repository at this point in the history
  • Loading branch information
Derek Berner committed Mar 27, 2018
1 parent df9ee38 commit 7e2d0e7
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 66 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ repositories {
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile 'org.reflections:reflections:0.9.11'

testCompile "com.winterbe:expekt:0.5.0"
testCompile "org.jetbrains.spek:spek-api:$spek_version"
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
144 changes: 78 additions & 66 deletions src/main/kotlin/me/ntrrgc/tsGenerator/TypeScriptGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package me.ntrrgc.tsGenerator

import org.reflections.Reflections
import java.beans.Introspector
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
Expand Down Expand Up @@ -75,20 +76,20 @@ import kotlin.reflect.jvm.javaType
* version used supports it or the user wants to be extra explicit.
*/
class TypeScriptGenerator(
rootClasses: Iterable<KClass<*>>,
private val mappings: Map<KClass<*>, String> = mapOf(),
classTransformers: List<ClassTransformer> = listOf(),
ignoreSuperclasses: Set<KClass<*>> = setOf(),
private val intTypeName: String = "number",
private val voidType: VoidType = VoidType.NULL
rootClasses: Iterable<KClass<*>>,
private val mappings: Map<KClass<*>, String> = mapOf(),
classTransformers: List<ClassTransformer> = listOf(),
ignoreSuperclasses: Set<KClass<*>> = setOf(),
private val intTypeName: String = "number",
private val voidType: VoidType = VoidType.NULL
) {
private val visitedClasses: MutableSet<KClass<*>> = java.util.HashSet()
private val generatedDefinitions = mutableListOf<String>()
private val pipeline = ClassTransformerPipeline(classTransformers)
private val ignoredSuperclasses = setOf(
Any::class,
java.io.Serializable::class,
Comparable::class
Any::class,
java.io.Serializable::class,
Comparable::class
).plus(ignoreSuperclasses)

init {
Expand All @@ -101,7 +102,7 @@ class TypeScriptGenerator(
fun isJavaBeanProperty(kProperty: KProperty<*>, klass: KClass<*>): Boolean {
val beanInfo = Introspector.getBeanInfo(klass.java)
return beanInfo.propertyDescriptors
.any { bean -> bean.name == kProperty.name }
.any { bean -> bean.name == kProperty.name }
}
}

Expand Down Expand Up @@ -140,14 +141,13 @@ class TypeScriptGenerator(
@Suppress("IfThenToElvis")
if (classifier is KClass<*>) {
if (classifier.isSubclassOf(Iterable::class)
|| classifier.javaObjectType.isArray)
{
|| classifier.javaObjectType.isArray) {
// Use native JS array
// Parenthesis are needed to disambiguate complex cases,
// e.g. (Pair<string|null, int>|null)[]|null
val itemType = when (kType.classifier) {
// Native Java arrays... unfortunately simple array types like these
// are not mapped automatically into kotlin.Array<T> by kotlin-reflect :(
// Native Java arrays... unfortunately simple array types like these
// are not mapped automatically into kotlin.Array<T> by kotlin-reflect :(
IntArray::class -> Int::class.createType(nullable = false)
ShortArray::class -> Short::class.createType(nullable = false)
ByteArray::class -> Byte::class.createType(nullable = false)
Expand All @@ -156,7 +156,7 @@ class TypeScriptGenerator(
FloatArray::class -> Float::class.createType(nullable = false)
DoubleArray::class -> Double::class.createType(nullable = false)

// Class container types (they use generics)
// Class container types (they use generics)
else -> kType.arguments.single().type ?: KotlinAnyOrNull
}
"${formatKType(itemType).formatWithParenthesis()}[]"
Expand All @@ -169,8 +169,8 @@ class TypeScriptGenerator(
// Use class name, with or without template parameters
formatClassType(classifier) + if (kType.arguments.isNotEmpty()) {
"<" + kType.arguments
.map { arg -> formatKType(arg.type ?: KotlinAnyOrNull).formatWithoutParenthesis() }
.joinToString(", ") + ">"
.map { arg -> formatKType(arg.type ?: KotlinAnyOrNull).formatWithoutParenthesis() }
.joinToString(", ") + ">"
} else ""
}
} else if (classifier is KTypeParameter) {
Expand All @@ -186,71 +186,83 @@ class TypeScriptGenerator(

private fun generateEnum(klass: KClass<*>): String {
return "type ${klass.simpleName} = ${klass.java.enumConstants
.map { constant: Any ->
constant.toString().toJSString()
}
.joinToString(" | ")
.map { constant: Any ->
constant.toString().toJSString()
}
.joinToString(" | ")
};"
}

private fun generateInterface(klass: KClass<*>): String {
val superclasses = klass.superclasses
.filterNot { it in ignoredSuperclasses }
private val KClass<*>.templateParameters: String get() = if (typeParameters.isNotEmpty()) {
"<" + typeParameters.joinToString(", ") { typeParameter ->
val bounds = typeParameter.upperBounds
.filter { it.classifier != Any::class }
typeParameter.name + if (bounds.isNotEmpty()) {
" extends " + bounds
.map { bound ->
formatKType(bound).formatWithoutParenthesis()
}
.joinToString(" & ")
} else {
""
}
} + ">"
} else {
""
}

private fun generateUnionType(sealedClass: KClass<*>): String {
val subclasses
= Reflections(sealedClass.java.`package`.name).getSubTypesOf(sealedClass.java).map { it.kotlin }
subclasses.forEach{ t -> generateInterface(t, sealedClass) }

return "type ${sealedClass.simpleName}${sealedClass.templateParameters} = " +
subclasses.joinToString(" | ") { "${it.simpleName}" } +
";"
}

private fun generateInterface(klass: KClass<*>, sealedClass: KClass<*>? = null): String {
val superclasses = (klass.superclasses
.filterNot { it in ignoredSuperclasses || it == sealedClass }) +
(sealedClass?.superclasses
?.filterNot { it in ignoredSuperclasses || it == sealedClass }
?: emptyList())

val extendsString = if (superclasses.isNotEmpty()) {
" extends " + superclasses
.map { formatClassType(it) }
.joinToString(", ")
" extends " + superclasses.joinToString(", ") { formatClassType(it) }
} else ""

val templateParameters = if (klass.typeParameters.isNotEmpty()) {
"<" + klass.typeParameters
.map { typeParameter ->
val bounds = typeParameter.upperBounds
.filter { it.classifier != Any::class }
typeParameter.name + if (bounds.isNotEmpty()) {
" extends " + bounds
.map { bound ->
formatKType(bound).formatWithoutParenthesis()
}
.joinToString(" & ")
} else {
""
}
}
.joinToString(", ") + ">"
} else {
""
}

return "interface ${klass.simpleName}$templateParameters$extendsString {\n" +
klass.declaredMemberProperties
.filter { !isFunctionType(it.returnType.javaType) }
.filter {
it.visibility == KVisibility.PUBLIC || isJavaBeanProperty(it, klass)
}
.let { propertyList ->
pipeline.transformPropertyList(propertyList, klass)
}
.map { property ->
val propertyName = pipeline.transformPropertyName(property.name, property, klass)
val propertyType = pipeline.transformPropertyType(property.returnType, property, klass)
return "interface ${klass.simpleName}${klass.templateParameters}$extendsString {\n" +
klass.declaredMemberProperties
.filter { !isFunctionType(it.returnType.javaType) }
.filter {
it.visibility == KVisibility.PUBLIC || isJavaBeanProperty(it, klass)
}
.let { propertyList ->
pipeline.transformPropertyList(propertyList, klass)
}
.map { property ->
val propertyName = pipeline.transformPropertyName(property.name, property, klass)
val propertyType = pipeline.transformPropertyType(property.returnType, property, klass)

val formattedPropertyType = formatKType(propertyType).formatWithoutParenthesis()
" $propertyName: $formattedPropertyType;\n"
}
.joinToString("") +
"}"
val formattedPropertyType = formatKType(propertyType).formatWithoutParenthesis()
" $propertyName: $formattedPropertyType;\n"
}
.joinToString("") +
"}"
}

private fun isFunctionType(javaType: Type): Boolean {
return javaType is KCallable<*>
|| javaType.typeName.startsWith("kotlin.jvm.functions.")
|| (javaType is ParameterizedType && isFunctionType(javaType.rawType))
|| javaType.typeName.startsWith("kotlin.jvm.functions.")
|| (javaType is ParameterizedType && isFunctionType(javaType.rawType))
}

private fun generateDefinition(klass: KClass<*>): String {
return if (klass.java.isEnum) {
generateEnum(klass)
} else if (klass.isSealed) {
generateUnionType(klass)
} else {
generateInterface(klass)
}
Expand Down

0 comments on commit 7e2d0e7

Please sign in to comment.