commit 865694be30a2bfb1dc59ba5b52fa92f4fcf28288 Author: Hubert Van De Walle Date: Thu Oct 8 22:24:32 2020 +0200 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..237ae17 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eff7b80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target/ +.idea/ +*.iml +*.ipr +*.iws diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..50aaaf2 --- /dev/null +++ b/Makefile @@ -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 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ce7a12f --- /dev/null +++ b/pom.xml @@ -0,0 +1,178 @@ + + 4.0.0 + be.vandewalleh + ktormgenerator + 1.0-SNAPSHOT + + 11 + 1.4.10 + official + ${java.version} + ${java.version} + UTF-8 + be.vandewalleh.KtormgeneratorKt + + 3.17.2 + 5.7.0 + 3.0.1 + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + com.github.ajalt.clikt + clikt-jvm + ${clikt.version} + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + + + + org.jetbrains.kotlin + kotlin-bom + ${kotlin.version} + pom + import + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.2 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M4 + + + org.apache.maven.surefire + surefire-junit-platform + 3.0.0-M4 + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + ${java.version} + + -Xinline-classes + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + false + true + + + ${main.class} + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + ktlint + verify + + + + + + + + + run + + + + ktlint-format + + + + + + + + + + run + + + + + + com.pinterest + ktlint + 0.39.0 + + + + + + diff --git a/src/main/kotlin/ColumnType.kt b/src/main/kotlin/ColumnType.kt new file mode 100644 index 0000000..0603501 --- /dev/null +++ b/src/main/kotlin/ColumnType.kt @@ -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"), +} diff --git a/src/main/kotlin/GeneratorCommand.kt b/src/main/kotlin/GeneratorCommand.kt new file mode 100644 index 0000000..6615c3c --- /dev/null +++ b/src/main/kotlin/GeneratorCommand.kt @@ -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().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") + } +} diff --git a/src/main/kotlin/Ktormgenerator.kt b/src/main/kotlin/Ktormgenerator.kt new file mode 100644 index 0000000..04733f6 --- /dev/null +++ b/src/main/kotlin/Ktormgenerator.kt @@ -0,0 +1,27 @@ +package be.vandewalleh + +data class Table(val name: TableName, val entityName: EntityName, val columns: List) + +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) { + val generator = TableGenerator() + GeneratorCommand(generator).main(args) +} diff --git a/src/main/kotlin/TableGenerator.kt b/src/main/kotlin/TableGenerator.kt new file mode 100644 index 0000000..0ea2ed6 --- /dev/null +++ b/src/main/kotlin/TableGenerator.kt @@ -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") + } + } +} diff --git a/src/test/kotlin/TableGeneratorTest.kt b/src/test/kotlin/TableGeneratorTest.kt new file mode 100644 index 0000000..cf33325 --- /dev/null +++ b/src/test/kotlin/TableGeneratorTest.kt @@ -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("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 { + companion object : Entity.Factory() + + 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("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 { + companion object : Entity.Factory() + + 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) + } +} diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..a8416a7 --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -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