1
0

Initial commit

This commit is contained in:
Hubert Van De Walle 2020-10-08 22:24:32 +02:00
commit 865694be30
10 changed files with 498 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{kt, kts}]
indent_size = 4
insert_final_newline = true
continuation_indent_size=4
max_line_length = 120
disabled_rules = no-wildcard-imports
kotlin_imports_layout = idea

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
target/
.idea/
*.iml
*.ipr
*.iws

13
Makefile Normal file
View File

@ -0,0 +1,13 @@
version = 1.0-SNAPSHOT
jar = target/ktormgenerator-$(version).jar
native-output = target/ktormgenerator-$(version)-linux-static
.PHONY = all
all: $(native-output)
$(native-output): $(jar)
native-image --static --no-fallback --no-server -jar $< $@
$(jar):
mvn package

178
pom.xml Normal file
View File

@ -0,0 +1,178 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>be.vandewalleh</groupId>
<artifactId>ktormgenerator</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>11</java.version>
<kotlin.version>1.4.10</kotlin.version>
<kotlin.code.style>official</kotlin.code.style>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.source>${java.version}</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.class>be.vandewalleh.KtormgeneratorKt</main.class>
<!-- versions -->
<assertj.version>3.17.2</assertj.version>
<junit.version>5.7.0</junit.version>
<clikt.version>3.0.1</clikt.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>com.github.ajalt.clikt</groupId>
<artifactId>clikt-jvm</artifactId>
<version>${clikt.version}</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-bom</artifactId>
<version>${kotlin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M4</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit-platform</artifactId>
<version>3.0.0-M4</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<jvmTarget>${java.version}</jvmTarget>
<args>
<arg>-Xinline-classes</arg>
</args>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<minimizeJar>true</minimizeJar>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>${main.class}</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>ktlint</id>
<phase>verify</phase>
<configuration>
<target name="ktlint">
<java classname="com.pinterest.ktlint.Main" classpathref="maven.plugin.classpath" dir="${basedir}" failonerror="true" fork="true" taskname="ktlint">
<arg value="src/**/*.kt"/>
</java>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
<execution>
<id>ktlint-format</id>
<configuration>
<target name="ktlint">
<java classname="com.pinterest.ktlint.Main" classpathref="maven.plugin.classpath" dir="${basedir}" failonerror="true" fork="true" taskname="ktlint">
<arg value="-F"/>
<arg value="src/**/*.kt"/>
</java>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.pinterest</groupId>
<artifactId>ktlint</artifactId>
<version>0.39.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,31 @@
package be.vandewalleh
/**
@see https://ktorm.liuwj.me/en/schema-definition.html
*/
@Suppress("unused")
enum class ColumnType(val ktormType: String, val kotlinType: String, val sqlType: String) {
BOOLEAN("boolean", "Boolean", "boolean"),
INT("int", "Int", "int"),
SHORT("short", "Short", "smallint"),
LONG("long", "Long", "bigint"),
FLOAT("float", "Float", "float"),
DOUBLE("double", "Double", "double"),
DECIMAL("decimal", "BigDecimal", "decimal"),
VARCHAR("varchar", "String", "varchar"),
TEXT("text", "String", "text"),
BLOB("blob", "ByteArray", "blob"),
BYTES("bytes", "ByteArray", "bytes"),
JDBC_TIMESTAMP("jdbcTimestamp", "Timestamp", "timestamp"),
JDBC_DATE("jdbcDate", "Date", "date"),
JDBC_TIME("jdbcTime", "Time", "time"),
TIMESTAMP("timestamp", "Instant", "timestamp"),
DATETIME("datetime", "LocalDateTime", "datetime"),
DATE("date", "LocalDate", "date"),
TIME("time", "Time", "time"),
MONTHDAY("monthDay", "MonthDay", "varchar"),
YEARMONTH("yearMonth", "YearMonth", "varchar"),
YEAR("year", "Year", "int"),
ENUM("enum", "Enum", "enum"),
UUID("uuid", "UUID", "uuid"),
}

View File

@ -0,0 +1,59 @@
package be.vandewalleh
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.UsageError
import java.io.File
class GeneratorCommand(private val generator: TableGenerator) : CliktCommand() {
private fun promptReference(): Reference? {
if (!(prompt("Reference", default = "false") { it.toBoolean() } ?: return null)) return null
val tableName = prompt("Table name") { TableName(it) } ?: return null
val referenceName = prompt("Reference Name") { ReferenceName(it) } ?: return null
val entityName = prompt("Entity Name") { EntityName(it) } ?: return null
return Reference(tableName, referenceName, entityName)
}
private fun promptColumn(): Column? {
val name = prompt("Column name") { ColumnName(it) } ?: return null
println(ColumnType.values().joinToString("|") { it.name.toLowerCase() })
val type = prompt("Column type") { input ->
enumValues<ColumnType>().find { it.name.equals(input, ignoreCase = true) }
} ?: return null
val nullable = prompt("Nullable", default = "false") { it.toBoolean() } ?: return null
val primaryKey = prompt("Primary key", default = "false") { it.toBoolean() } ?: return null
val reference = promptReference()
return Column(name, type, nullable, primaryKey, reference)
}
override fun run() {
val tableName = prompt("Table name") { TableName(it) } ?: throw UsageError("Table Name Required")
val defaultEntityName = when {
tableName.value.endsWith("s") -> tableName.value.removeSuffix("s") + "Entity"
tableName.value.endsWith("Table") -> tableName.value.removeSuffix("Table") + "Entity"
else -> null
}
val entityName = prompt("Entity name", default = defaultEntityName) {
EntityName(it)
} ?: throw UsageError("Entity Name Required")
val columns = generateSequence {
println()
promptColumn()
}.toList()
val generated = generator(Table(tableName, entityName, columns))
val fileName = tableName.value + ".kt"
File(fileName).writeText(generated)
println("\nGenerated output in $fileName")
}
}

View File

@ -0,0 +1,27 @@
package be.vandewalleh
data class Table(val name: TableName, val entityName: EntityName, val columns: List<Column>)
data class Column(
val name: ColumnName,
val type: ColumnType,
val nullable: Boolean,
val primaryKey: Boolean,
val reference: Reference? = null,
)
data class Reference(
val tableName: TableName,
val referenceName: ReferenceName,
val entityName: EntityName,
)
inline class EntityName(val value: String)
inline class ColumnName(val value: String)
inline class ReferenceName(val value: String)
inline class TableName(val value: String)
fun main(args: Array<String>) {
val generator = TableGenerator()
GeneratorCommand(generator).main(args)
}

View File

@ -0,0 +1,41 @@
package be.vandewalleh
class TableGenerator {
operator fun invoke(table: Table): String {
val (tableName, entityName, columns) = table
val columnTemplate: (Column) -> String = { (name, type, _, primaryKey, reference) ->
buildString {
append("val ${name.value} = ${type.ktormType}(\"${name.value}\")")
if (primaryKey) append(".primaryKey()")
if (reference != null)
append(".references(${reference.tableName.value}) { it.${reference.referenceName.value} }")
else
append(".bindTo { it.${name.value} }")
}
}
val entityColumnTemplate: (Column) -> String = { (name, type, nullable, _, reference) ->
buildString {
if (reference != null) append("var ${reference.referenceName.value}: ${reference.entityName.value}")
else append("var ${name.value}: ${type.kotlinType}")
if (nullable) append("?")
}
}
return buildString {
append("internal open class ${tableName.value}(alias: String?) ")
append(": Table<${entityName.value}>(\"${tableName.value}\", alias) {\n")
append(" companion object : ${tableName.value}(null)\n\n")
append(" override fun aliased(alias: String) = ${tableName.value}(alias)\n\n")
append(" ${columns.joinToString("\n ") { columnTemplate(it) }}\n")
append("}\n\n")
append("internal interface ${entityName.value} : Entity<${entityName.value}> {\n")
append(" companion object : Entity.Factory<${entityName.value}>()\n\n")
append(" ${columns.joinToString("\n ") { entityColumnTemplate(it) }}\n")
append("}\n")
}
}
}

View File

@ -0,0 +1,122 @@
package be.vandewalleh
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
internal class TableGeneratorTest {
private val generator = TableGenerator()
@Test
fun generateUsers() {
val res = generator(
Table(
TableName("Users"), EntityName("UserEntity"),
listOf(
Column(name = ColumnName("id"), type = ColumnType.INT, nullable = false, primaryKey = true),
Column(
name = ColumnName("username"), type = ColumnType.VARCHAR,
nullable = false, primaryKey = false
),
Column(
name = ColumnName("password"), type = ColumnType.VARCHAR,
nullable = true, primaryKey = false
),
)
)
)
val expected = """
internal open class Users(alias: String?) : Table<UserEntity>("Users", alias) {
companion object : Users(null)
override fun aliased(alias: String) = Users(alias)
val id = int("id").primaryKey().bindTo { it.id }
val username = varchar("username").bindTo { it.username }
val password = varchar("password").bindTo { it.password }
}
internal interface UserEntity : Entity<UserEntity> {
companion object : Entity.Factory<UserEntity>()
var id: Int
var username: String
var password: String?
}
""".trimIndent()
assertThat(res).isEqualTo(expected)
}
@Test
fun generateNotes() {
val res = generator(
Table(
TableName("Notes"), EntityName("NoteEntity"),
listOf(
Column(name = ColumnName("uuid"), type = ColumnType.UUID, nullable = false, primaryKey = true),
Column(name = ColumnName("title"), type = ColumnType.VARCHAR, nullable = false, primaryKey = false),
Column(name = ColumnName("markdown"), type = ColumnType.TEXT, nullable = false, primaryKey = false),
Column(name = ColumnName("html"), type = ColumnType.TEXT, nullable = false, primaryKey = false),
Column(
name = ColumnName("user_id"), type = ColumnType.INT, nullable = false, primaryKey = false,
reference = Reference(TableName("Users"), ReferenceName("user"), EntityName("User"))
),
Column(
name = ColumnName("updated_at"), type = ColumnType.DATETIME,
nullable = false, primaryKey = false
),
Column(
name = ColumnName("updated_at"), type = ColumnType.DATETIME,
nullable = false, primaryKey = false
),
Column(
name = ColumnName("deleted"), type = ColumnType.BOOLEAN,
nullable = false, primaryKey = false
),
Column(
name = ColumnName("public"), type = ColumnType.BOOLEAN,
nullable = false, primaryKey = false
),
)
)
)
val expected = """
internal open class Notes(alias: String?) : Table<NoteEntity>("Notes", alias) {
companion object : Notes(null)
override fun aliased(alias: String) = Notes(alias)
val uuid = uuid("uuid").primaryKey().bindTo { it.uuid }
val title = varchar("title").bindTo { it.title }
val markdown = text("markdown").bindTo { it.markdown }
val html = text("html").bindTo { it.html }
val user_id = int("user_id").references(Users) { it.user }
val updated_at = datetime("updated_at").bindTo { it.updated_at }
val updated_at = datetime("updated_at").bindTo { it.updated_at }
val deleted = boolean("deleted").bindTo { it.deleted }
val public = boolean("public").bindTo { it.public }
}
internal interface NoteEntity : Entity<NoteEntity> {
companion object : Entity.Factory<NoteEntity>()
var uuid: UUID
var title: String
var markdown: String
var html: String
var user: User
var updated_at: LocalDateTime
var updated_at: LocalDateTime
var deleted: Boolean
var public: Boolean
}
""".trimIndent()
assertThat(res).isEqualTo(expected)
}
}

View File

@ -0,0 +1,4 @@
junit.jupiter.testinstance.lifecycle.default=per_class
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent