-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathTSGenerator.kt
123 lines (110 loc) · 5.06 KB
/
TSGenerator.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package klite.json
import klite.Converter
import klite.publicProperties
import org.intellij.lang.annotations.Language
import java.io.File
import java.io.PrintStream
import java.lang.System.err
import java.nio.file.Path
import java.time.*
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.PathWalkOption.INCLUDE_DIRECTORIES
import kotlin.io.path.extension
import kotlin.io.path.walk
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.jvmErasure
internal const val tsDate = "\${number}-\${number}-\${number}"
internal const val tsTime = "\${number}:\${number}:\${number}"
/** Converts project data/enum/inline classes to TypeScript for front-end type-safety */
open class TSGenerator(
private val customTypes: Map<String, String?> = mapOf(
LocalDate::class.qualifiedName!! to "`${tsDate}`",
LocalTime::class.qualifiedName!! to "`${tsTime}`",
LocalDateTime::class.qualifiedName!! to "`${tsDate}T${tsTime}`",
OffsetDateTime::class.qualifiedName!! to "`${tsDate}T${tsTime}+\${number}:\${number}`",
Instant::class.qualifiedName!! to "`${tsDate}T${tsTime}Z`"
),
private val typePrefix: String = "export ",
private val out: PrintStream = System.out
) {
@OptIn(ExperimentalPathApi::class)
open fun printFrom(dir: Path) {
dir.walk(INCLUDE_DIRECTORIES).filter { it.extension == "class" }.sorted().forEach {
val className = dir.relativize(it).toString().removeSuffix(".class").replace(File.separatorChar, '.')
printClass(className)
}
}
protected open fun printUnmappedCustomTypes() = customTypes.forEach {
if (it.value == null) printClass(it.key)
}
protected open fun printClass(className: String) = try {
val cls = Class.forName(className).kotlin
render(cls)?.let {
out.println("// $cls")
out.println(typePrefix + it)
}
} catch (ignore: UnsupportedOperationException) {
} catch (e: Exception) {
err.println("// $className: $e")
}
@Language("TypeScript") open fun render(cls: KClass<*>) =
if (cls.isData || cls.java.isInterface && !cls.java.isAnnotation) renderInterface(cls)
else if (cls.isValue) renderInline(cls)
else if (cls.isSubclassOf(Enum::class)) renderEnum(cls)
else null
protected open fun renderEnum(cls: KClass<*>) = "enum " + tsName(cls) + " {" + cls.java.enumConstants.joinToString { "$it = '$it'" } + "}"
protected open fun renderInline(cls: KClass<*>) = "type " + tsName(cls) + typeParams(cls, noVariance = true) +
" = " + tsType(cls.primaryConstructor?.parameters?.first()?.type)
protected open fun typeParams(cls: KClass<*>, noVariance: Boolean = false) =
cls.typeParameters.takeIf { it.isNotEmpty() }?.joinToString(prefix = "<", postfix = ">") { if (noVariance) it.name else it.toString() } ?: ""
@Suppress("UNCHECKED_CAST")
protected open fun renderInterface(cls: KClass<*>): String? = StringBuilder().apply {
val props = (cls.publicProperties as Sequence<KProperty1<Any, *>>).notIgnored.iterator()
if (!props.hasNext()) return null
append("interface ").append(tsName(cls)).append(typeParams(cls)).append(" {")
props.iterator().forEach { p ->
append(p.jsonName)
if (p.returnType.isMarkedNullable) append("?")
append(": ").append(tsType(p.returnType)).append("; ")
}
if (endsWith("; ")) setLength(length - 2)
append("}")
}.toString()
protected open fun tsType(type: KType?): String {
val cls = type?.classifier as? KClass<*>
val ts = customTypes[type?.toString()] ?: customTypes[type?.jvmErasure?.qualifiedName] ?: when {
cls == null || cls == Any::class -> "any"
cls.isValue -> tsName(cls)
cls.isSubclassOf(Enum::class) -> tsName(cls)
cls.isSubclassOf(Boolean::class) -> "boolean"
cls.isSubclassOf(Number::class) -> "number"
cls.isSubclassOf(Iterable::class) -> "Array"
cls.java.isArray -> "Array" + (cls.java.componentType?.let { if (it.isPrimitive) "<" + tsType(it.kotlin.createType()) + ">" else "" } ?: "")
cls.isSubclassOf(Map::class) -> "Record"
cls == KProperty1::class -> "keyof " + tsType(type.arguments.first().type)
cls.isSubclassOf(CharSequence::class) || Converter.supports(cls) -> "string"
cls.isData || cls.java.isInterface -> tsName(cls)
else -> "any"
}
return if (ts[0].isLowerCase()) ts
else ts + (type?.arguments?.takeIf { it.isNotEmpty() }?.joinToString(prefix = "<", postfix = ">") { tsType(it.type) } ?: "")
}
protected open fun tsName(type: KClass<*>) = type.java.name.substringAfterLast(".").replace("$", "")
companion object {
@JvmStatic fun main(args: Array<String>) {
if (args.isEmpty())
return err.println("Usage: <classes-dir> ...custom.Type=tsType ...package.IncludeThisType")
val dir = Path.of(args[0])
val customTypes = args.drop(1).associate { it.split("=").let { it[0] to it.getOrNull(1) } }
TSGenerator(customTypes).apply {
printUnmappedCustomTypes()
printFrom(dir)
}
}
}
}