forked from codeborne/klite
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathTSGenerator.kt
157 lines (140 loc) · 6.24 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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package klite.json
import klite.Converter
import klite.Email
import klite.Phone
import klite.publicProperties
import org.intellij.lang.annotations.Language
import java.io.File
import java.io.PrintStream
import java.lang.System.err
import java.net.URI
import java.net.URL
import java.nio.charset.StandardCharsets.UTF_8
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
/** Converts project data/enum/inline classes to TypeScript for front-end type-safety */
open class TSGenerator(
customTypes: Map<String, String?> = emptyMap(),
private val typePrefix: String = "export ",
private val out: PrintStream = System.out
) {
private val customTypes = defaultCustomTypes + customTypes
private val usedCustomTypes = mutableSetOf<String>()
@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 printCustomTypes() {
if (customTypes.isNotEmpty()) out.println("")
customTypes.forEach {
if (it.value == null) printClass(it.key)
else if (it.key in usedCustomTypes) {
out.println("// ${it.key}")
out.println("${typePrefix}type ${it.key.substringAfterLast(".")} = ${it.value}")
}
}
}
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 customType = listOf(type?.toString(), type?.jvmErasure?.qualifiedName).find { it in customTypes }
val ts = customType?.also { usedCustomTypes += it }?.substringAfterLast(".") ?: 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 {
const val tsDate = "\${number}-\${number}-\${number}"
const val tsTime = "\${number}:\${number}:\${number}"
const val tsUrl = "`\${string}://\${string}`"
val defaultCustomTypes = mapOf(
LocalDate::class to "`${tsDate}`",
LocalTime::class to "`${tsTime}`",
LocalDateTime::class to "`${tsDate}T${tsTime}`",
OffsetDateTime::class to "`${tsDate}T${tsTime}+\${number}:\${number}`",
Instant::class to "`${tsDate}T${tsTime}Z`",
URL::class to tsUrl,
URI::class to tsUrl,
Email::class to "`\${string}@\${string}`",
Phone::class to "`+\${number}`",
).mapKeys { it.key.qualifiedName!! }
@JvmStatic fun main(args: Array<String>) {
if (args.isEmpty())
return err.println("Usage: <classes-dir> ...custom.Type=tsType ...package.IncludeThisType [-o <output-file>] [-p <prepend-text>]")
val dir = Path.of(args[0])
val argsLeft = args.toMutableList().apply { removeAt(0) }
val out = argsLeft.arg("-o")?.let { PrintStream(it, UTF_8) } ?: System.out
out.use {
argsLeft.arg("-p")?.let { out.println(it) }
val customTypes = argsLeft.associate { it.split("=").let { it[0] to it.getOrNull(1) } }
TSGenerator(customTypes, out = out).apply {
printFrom(dir)
printCustomTypes()
}
}
}
@JvmStatic private fun MutableList<String>.arg(prefix: String): String? {
val i = indexOf(prefix)
return if (i == -1) null else get(i + 1).also { removeAt(i); removeAt(i) }
}
}
}