From ba243bc151b34791fcd030d361edb224297e3439 Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Fri, 28 Dec 2018 00:24:25 -0800 Subject: [PATCH] version 2.0.0, lots of things --- README.md | 42 +- build.gradle.kts | 35 +- src/main/kotlin/com/rnett/daogen/Main.kt | 30 +- .../kotlin/com/rnett/daogen/app/DaogenApp.kt | 309 ++++++++++++- .../kotlin/com/rnett/daogen/app/TreeItems.kt | 40 +- .../kotlin/com/rnett/daogen/database/DB.kt | 3 +- .../kotlin/com/rnett/daogen/ddl/Column.kt | 96 ++++- .../kotlin/com/rnett/daogen/ddl/Database.kt | 203 ++++++++- src/main/kotlin/com/rnett/daogen/ddl/Table.kt | 408 ++++++++++++++---- src/main/kotlin/com/rnett/daogen/ddl/Type.kt | 9 +- .../com/rnett/daogen/app/AppView.fxml | 91 +++- 11 files changed, 1092 insertions(+), 174 deletions(-) diff --git a/README.md b/README.md index feb11d8..4bda701 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,35 @@ look at the Type class and its uses, it is the only vendor dependant part. Error reports / issues are welcome, but there is no guarantee I will get to it. This isn't something I'm supporting, just something I use that might be useful. +# Features + + * KotlinX Serializer generation (saved fields on serialization, loads from database on deserializiation) + * Multiplatform support (with cross-platform capable serialization for easy data transfer) + * Foreign and Referencing Keys + * Optional assume nullable unless explcit `not null` + * Imports and package statement + * Export to class/package structure + * `new` pesudo-Constructor that takes fields as arguments + * Customizable names, mutability + +# Limitations + + * Some data types + * Anything Non-postgres + * Must have one primary key that is an int or long type if you want DAO (DSL will still be generated) + # Usage ## GUI (ExposedDaoGenerator.jar) Download the ExposedDaoGenerator.jar. Run it. -##### It is still in Beta, the UI is somewhat buggy. +Supports package names, save/load, import/export, and export to files (a file per table). + +Also supports Kotlin multiplatform projects +(generates a common class, a JS class that will work on other platforms, and the JVM class with the exposed backend). + +##### UI is somewhat buggy, but as long as you don't try to break it you should be allright. Issues and feedback are appreciated. To start, hit File -> New or `CTRL-N` to create a new database from a connection string. @@ -32,7 +54,9 @@ You can change the names of the columns and keys, and make the class properties I plan to add the ability to change the type, and to exclude columns. -### Saving +### Usage + +To change settings, use `CTRL-SHIFT-O`. You can save your (edited) database to a .daogen file using File -> Save or `CTRL-S`. @@ -46,6 +70,8 @@ To change the export file, use `CTRL-SHIFT-E`. To import the database from an exported file, use `CTRL-I`. +To export to files, use `CTRL-ALT-E`. + If Autosave is checked, the database will automatically be saved to the save file when any changes are made (if the save file is set). If Auto Export is checked, the database will automatically be exported to the export file when any changes are made (if the export file is set). @@ -58,11 +84,17 @@ Download daogen.jar or build with gradle. The jar is /libs/daogen.jarif you build with gradle. -Run with the command arguments: ` [-f outFile] [-s schema] [-nodao] [-cc] [-q]` +Run with the command arguments: ` [-p package] [-f outFile] [-s schema] [-tables tablesCSVList] [-nodao] [-noserialize] [-multiplatform JVM/JS/Common] [-cc] [-q]` * `connectionString` is the JDBC connection string. **[MANDATORY]** - * `outFile` file to output to. Default is not to output the generated code to a file. Optional, **must be preceded by -f** - * `schema` schema to look at. Default is all of them. Optional, **must be preceded by -s** + * `package` is the package to put in the package statement. Optional, **must be preceded by -p** + * `outFile` is the file to output to. Default is not to output the generated code to a file. Optional, **must be preceded by -f** + * `schema` is schema to look at. Default is all of them. Optional, **must be preceded by -s** + * `tablesCSVList` is the list of tables, separated by commas, to look at. Default is all of them. Optional, **must be preceded by -tables** * `-nodao` means not to generate DAO (classes), just DSL (objects). Optional. + * `-noserialize` means not to generate KotlinX Serializers. Optional. + * `-multiplatform` optionally generates the DAO for a multiplatform project's platform. + `-multiplatform` can be followed by `JVM`, `JS`, or `Common`, which causes daogen to output for that platform. + Note that specifying `JVM` here is different than no multiplatform at all; with `JVM`, `actual` statements will be included. * `-cc` means to copy the generated code to the clipboard. Optional. * `-q` means to run in quiet mode without outputting the generated code. Optional. diff --git a/build.gradle.kts b/build.gradle.kts index 5202e04..53d0c50 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,23 +1,42 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jetbrains.kotlin.gradle.dsl.Coroutines import org.apache.tools.ant.filters.* -import org.jetbrains.kotlin.contracts.model.structure.UNKNOWN_COMPUTATION.type +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import sun.tools.jar.resources.jar -val kotlin_version = "1.2.71" +val kotlin_version = "1.3.11" + +buildscript { + val kotlin_version = "1.3.11" + repositories { + jcenter() + mavenCentral() + maven("https://dl.bintray.com/kotlin/kotlin-eap") + maven("https://kotlin.bintray.com/kotlinx") + } + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlin_version") + } +} group = "com.rnett.daogen" -version = "2.0.0-beta" +version = "2.0.0" plugins { java - kotlin("jvm") version "1.2.71" + kotlin("jvm") version "1.3.11" } +apply { + plugin("kotlinx-serialization") +} + +val serializiation_version = "0.9.1" + repositories { mavenCentral() jcenter() maven("https://jitpack.io") + maven("https://kotlin.bintray.com/kotlinx") } dependencies { @@ -26,6 +45,9 @@ dependencies { implementation("org.postgresql:postgresql:42.2.5") implementation("no.tornado:tornadofx:1.7.16") implementation("com.github.salomonbrys.kotson:kotson:2.5.0") + + implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serializiation_version") + implementation("com.github.rnett:core:1.3.8") } configure { @@ -35,6 +57,7 @@ tasks.withType { kotlinOptions.jvmTarget = "1.8" } + val jars = mapOf( "daogen" to "com.rnett.daogen.MainKt", "ExposedDaoGenerator" to "com.rnett.daogen.app.DaogenApp" diff --git a/src/main/kotlin/com/rnett/daogen/Main.kt b/src/main/kotlin/com/rnett/daogen/Main.kt index ea5c7f5..ea1f0e1 100644 --- a/src/main/kotlin/com/rnett/daogen/Main.kt +++ b/src/main/kotlin/com/rnett/daogen/Main.kt @@ -1,21 +1,20 @@ package com.rnett.daogen import com.rnett.daogen.database.DB -import com.rnett.daogen.ddl.generateDB -import com.rnett.daogen.ddl.generateKotlin +import com.rnett.daogen.ddl.* import java.awt.Toolkit import java.awt.datatransfer.StringSelection import java.io.File - -var doDAO = true +import kotlin.contracts.ExperimentalContracts /** - * args: [-f outFile] [-s schema] [-nodao] [-cc] [-q] + * args: [-p package] [-f outFile] [-s schema] [-tables ] [-nodao] [-noserialize] [-multiplatform ] [-cc] [-q] */ +@ExperimentalContracts fun main(args: Array) { if (args.contains("--version")) { - println("Daogen version 1.0.0") + println("Daogen version 2.0.0") return } @@ -28,10 +27,23 @@ fun main(args: Array) { val schema = args["-s"] - if ("-nodao" in args) - doDAO = false + val pack = args["-p"] ?: "unknown" + + val tables = args["-tables"] + + val doDao = "-nodao" !in args + val doSerialize = "-noserialize" !in args + val multiplatform = args["-multiplatform"] - val out = DB.connect(dbString).generateDB(dbString, schema).generateKotlin() + val options = GenerationOptions(pack, doSerialize, true, doDao, multiplatform != null) + + val db = DB.connect(dbString).generateDB(dbString, schema, tables?.split(",")?.map { it.trim() }) + + val out = when (multiplatform) { + "JS" -> db.generateKotlinJS(options) + "Common" -> db.generateKotlinCommon(options) + else -> db.generateKotlin(options) + } if ("-cc" in args) { val sel = StringSelection(out) diff --git a/src/main/kotlin/com/rnett/daogen/app/DaogenApp.kt b/src/main/kotlin/com/rnett/daogen/app/DaogenApp.kt index fad5495..84adfbd 100644 --- a/src/main/kotlin/com/rnett/daogen/app/DaogenApp.kt +++ b/src/main/kotlin/com/rnett/daogen/app/DaogenApp.kt @@ -2,9 +2,7 @@ package com.rnett.daogen.app import com.github.salomonbrys.kotson.fromJson import com.google.gson.Gson -import com.rnett.daogen.ddl.Database -import com.rnett.daogen.ddl.Table -import com.rnett.daogen.ddl.generateKotlin +import com.rnett.daogen.ddl.* import javafx.event.Event import javafx.event.EventType import javafx.geometry.Pos @@ -18,6 +16,7 @@ import javafx.scene.text.FontWeight import javafx.stage.FileChooser import tornadofx.* import java.io.File +import kotlin.contracts.ExperimentalContracts import kotlin.properties.Delegates //TODO checkboxes for nullable (only doable if server is nullable, may want to set un-nullable) Do I want this? @@ -35,6 +34,7 @@ val DB_CHANGED = EventType("DB_CHANGE") class DBChangeEvent : Event(DB_CHANGED) +@ExperimentalContracts class DaogenApp : App(AppView::class) { companion object { @JvmStatic @@ -44,6 +44,7 @@ class DaogenApp : App(AppView::class) { } } +@ExperimentalContracts class AppView : View() { val tablesList: ListView by fxid() @@ -51,7 +52,10 @@ class AppView : View() { val classTree: TreeView<*> by fxid() val editorPane: AnchorPane by fxid() val editorLabel: Label by fxid() - val codeArea: TextArea by fxid() + val codeAreaJVM: TextArea by fxid() + val codeAreaJS: TextArea by fxid() + val codeAreaCommon: TextArea by fxid() + val codeTabs: TabPane by fxid() val autosaveCheckbox: CheckBox by fxid() val autoExportCheckbox: CheckBox by fxid() @@ -68,13 +72,17 @@ class AppView : View() { classTree.root = null editorPane.children.clear() editorLabel.text = "No Item Selected" - codeArea.text = "" + codeAreaJVM.text = "" + codeAreaJS.text = "" + codeAreaCommon.text = "" if (new != null) { tables = new.tables.map { Pair(it.name, it) }.toMap() } } + var generationOptions = GenerationOptions("unknown") + fun setEditor(edit: EditableItem) { editorPane.children.clear() edit.root.anchorpaneConstraints { @@ -130,8 +138,10 @@ class AppView : View() { } fun generateCode(): String { - val t = db?.generateKotlin(saveFile?.path ?: "") ?: "" - codeArea.text = t + val t = db?.generateKotlin(generationOptions, saveFile?.path ?: "") ?: "" + codeAreaJVM.text = t + codeAreaJS.text = db?.generateKotlinJS(generationOptions) ?: "" + codeAreaCommon.text = db?.generateKotlinCommon(generationOptions) ?: "" return t } @@ -161,7 +171,7 @@ class AppView : View() { } fun saveDB(file: File) { - file.writeText(Gson().toJson(DBSave(db!!.data, exportFile?.path ?: ""))) + file.writeText(Gson().toJson(DBSave(db!!.data, exportFile?.path ?: "", generationOptions))) isSaveCurrent = true } @@ -171,6 +181,8 @@ class AppView : View() { db = json.db.create() + generationOptions = json.generationOptions + if (json.exportFilePath != "") { exportFile = File(json.exportFilePath) autoExportCheckbox.isSelected = true @@ -181,27 +193,37 @@ class AppView : View() { } var saveFile: File? by Delegates.observable(null as File?) { _, _, new -> - if (isSaveCurrent) - title = "Daogen: " + (new?.nameWithoutExtension ?: "Untitled") + " ==> " + (exportFile?.name ?: "None") + title = if (isSaveCurrent) + "Daogen: " + (new?.nameWithoutExtension ?: "Untitled") + " ==> " + (exportFile?.name ?: "None") else - title = "Daogen: *" + (new?.nameWithoutExtension ?: "Untitled") + " ==> " + (exportFile?.name ?: "None") + "Daogen: *" + (new?.nameWithoutExtension ?: "Untitled") + " ==> " + (exportFile?.name ?: "None") } var exportFile: File? by Delegates.observable(null as File?) { _, _, new -> - if (isSaveCurrent) - title = "Daogen: " + (saveFile?.nameWithoutExtension ?: "Untitled") + " ==> " + (new?.name ?: "None") + + if (new != null) { + Regex("package ([A-z.0-9]*)\n").find(new.readText())?.groupValues?.getOrNull(1)?.let { + generationOptions.outPackage = it + } + } + + title = if (isSaveCurrent) + "Daogen: " + (saveFile?.nameWithoutExtension ?: "Untitled") + " ==> " + (new?.name ?: "None") else - title = "Daogen: *" + (saveFile?.nameWithoutExtension ?: "Untitled") + " ==> " + (new?.name ?: "None") + "Daogen: *" + (saveFile?.nameWithoutExtension ?: "Untitled") + " ==> " + (new?.name ?: "None") } class NewModal : Fragment("New Daogen") { var connStr = "" + var schema = "" + var useTables = "" val model = NewModalModel() override val root: Parent = vbox { hbox { + vboxConstraints { marginTop = 10.0 } alignment = Pos.CENTER label("JDBC Connection String:").hboxConstraints { marginRight = 10.0; marginLeft = 10.0 } textfield { @@ -210,9 +232,29 @@ class AppView : View() { textProperty().bindBidirectional(model.connStr) } } + hbox { + vboxConstraints { marginTop = 10.0 } + alignment = Pos.CENTER + label("Schema:").hboxConstraints { marginRight = 10.0; marginLeft = 10.0 } + textfield { + hboxConstraints { marginRight = 10.0 } + prefColumnCount = 30 + textProperty().bindBidirectional(model.schema) + } + } + hbox { + vboxConstraints { marginTop = 10.0 } + alignment = Pos.CENTER + label("Use Tables:").hboxConstraints { marginRight = 10.0; marginLeft = 10.0 } + textfield { + hboxConstraints { hGrow = Priority.ALWAYS; marginRight = 10.0 } + prefColumnCount = 70 + textProperty().bindBidirectional(model.useTables) + } + } hbox { - vboxConstraints { marginTop = 20.0 } + vboxConstraints { marginTop = 10.0 } alignment = Pos.CENTER button("Create").setOnAction { close() @@ -223,6 +265,8 @@ class AppView : View() { inner class NewModalModel : ItemViewModel(this@NewModal) { var connStr = bind(NewModal::connStr, true) + var schema = bind(NewModal::schema, true) + var useTables = bind(NewModal::useTables, true) } } @@ -269,15 +313,246 @@ class AppView : View() { } + class OptionsModal : Fragment("Options") { + + var packageName = "" + var serialization = true + var doDao = true + var multiplatform = true + var nullableByDefault = true + + fun setOptions(options: GenerationOptions) { + model.packageName.value = options.outPackage + model.serialization.value = options.serialization + model.doDao.value = options.doDao + model.multiplatform.value = options.multiplatform + model.nullableByDefault.value = options.nullableByDefault + } + + val model = OptionsModalModel() + + override val root: Parent = vbox { + hbox { + vboxConstraints { marginBottom = 10.0 } + alignment = Pos.CENTER + label("Package:").hboxConstraints { marginRight = 10.0; marginLeft = 10.0 } + textfield { + hboxConstraints { marginRight = 10.0 } + prefColumnCount = 30 + textProperty().bindBidirectional(model.packageName) + } + } + hbox { + vboxConstraints { marginBottom = 10.0 } + alignment = Pos.CENTER + label("Use nullable types by default:") { + hboxConstraints { marginRight = 10.0 } + } + checkbox { + selectedProperty().bindBidirectional(model.nullableByDefault) + selectedProperty().onChange { + fireEvent(DBChangeEvent()) + } + } + } + hbox { + vboxConstraints { marginBottom = 10.0 } + alignment = Pos.CENTER + label("Generate KotlinX Serializer:") { + hboxConstraints { marginRight = 10.0 } + } + checkbox { + selectedProperty().bindBidirectional(model.serialization) + selectedProperty().onChange { + fireEvent(DBChangeEvent()) + } + } + } + hbox { + vboxConstraints { marginBottom = 10.0 } + alignment = Pos.CENTER + label("Generate DAO:") { + hboxConstraints { marginRight = 10.0 } + } + checkbox { + selectedProperty().bindBidirectional(model.doDao) + selectedProperty().onChange { + fireEvent(DBChangeEvent()) + } + } + } + hbox { + vboxConstraints { marginBottom = 10.0 } + alignment = Pos.CENTER + label("Multiplatform:") { + hboxConstraints { marginRight = 10.0 } + } + checkbox { + selectedProperty().bindBidirectional(model.multiplatform) + selectedProperty().onChange { + fireEvent(DBChangeEvent()) + } + } + } + + hbox { + alignment = Pos.CENTER + button("Done").setOnAction { + close() + } + } + + } + + inner class OptionsModalModel : ItemViewModel(this@OptionsModal) { + var packageName = bind(OptionsModal::packageName, true) + var serialization = bind(OptionsModal::serialization, true) + var doDao = bind(OptionsModal::doDao, true) + var multiplatform = bind(OptionsModal::multiplatform, true) + var nullableByDefault = bind(OptionsModal::nullableByDefault, true) + + } + + } + + class ExportFSModal : Fragment("Export to structured files") { + + var text1 = "" + var text2 = "" + + var baseDir = "" + + val model = ExportFSModalModel() + + override val root: Parent = vbox { + + hbox { + vboxConstraints { marginTop = 10.0 } + alignment = Pos.CENTER + label(model.text1) { + font = Font.font(font.family, FontWeight.BOLD, 14.0) + hboxConstraints { marginLeft = 20.0; marginRight = 20.0 } + isWrapText = true + alignment = Pos.CENTER + } + } + hbox { + vboxConstraints { marginTop = 5.0 } + alignment = Pos.CENTER + label(model.text2) { + font = Font.font(font.family, FontWeight.BOLD, 14.0) + hboxConstraints { marginLeft = 20.0; marginRight = 20.0 } + isWrapText = true + alignment = Pos.CENTER + } + } + + hbox { + vboxConstraints { marginTop = 10.0 } + alignment = Pos.CENTER + button("Choose").setOnAction { + val file = chooseDirectory("Base Directory") + if (file != null) + model.baseDir.value = file.absolutePath + } + } + + hbox { + vboxConstraints { marginTop = 10.0 } + alignment = Pos.CENTER + label("Currently Selected:") { + font = Font.font(font.family, FontWeight.BOLD, 14.0) + hboxConstraints { hGrow = Priority.ALWAYS; marginLeft = 20.0; marginRight = 20.0 } + isWrapText = true + alignment = Pos.CENTER + } + } + + hbox { + vboxConstraints { marginTop = 5.0 } + alignment = Pos.CENTER + label(model.baseDir) { + font = Font.font(font.family, FontWeight.BOLD, 14.0) + hboxConstraints { hGrow = Priority.ALWAYS; marginLeft = 20.0; marginRight = 20.0 } + isWrapText = true + alignment = Pos.CENTER + } + } + + hbox { + vboxConstraints { marginTop = 10.0 } + alignment = Pos.CENTER + button("Export").setOnAction { + close() + } + } + + } + + inner class ExportFSModalModel : ItemViewModel(this@ExportFSModal) { + var baseDir = bind(ExportFSModal::baseDir, true) + var text1 = bind(ExportFSModal::text1, true) + var text2 = bind(ExportFSModal::text2, true) + } + + } + + fun exportFiles() { + val modal = find { + if (generationOptions.multiplatform) { + model.text1.value = "Choose the directory that contains the platform modules (commonMain, jvmMain, etc)." + model.text2.value = "(\$projectDir/src for most InteliJ projects)" + } else { + model.text1.value = "Choose the directory that contains the first packages." + model.text2.value = "(\$projectDir/src/main/kotlin for most InteliJ projects)" + } + } + + modal.openModal(block = true) + + val baseDir = modal.baseDir + + if (generationOptions.multiplatform) + db?.generateToFileSystemMultiplatform(baseDir, generationOptions) + else + db?.generateToFileSystemPureJVM(baseDir, generationOptions) + + } + + fun options() { + val modal = find() + + modal.setOptions(generationOptions) + + modal.openModal(block = true) + + generationOptions.outPackage = modal.packageName + generationOptions.serialization = modal.serialization + generationOptions.doDao = modal.doDao + generationOptions.multiplatform = modal.multiplatform + generationOptions.nullableByDefault = modal.nullableByDefault + + if (!generationOptions.multiplatform) + codeTabs.selectionModel.select(0) + + codeTabs.tabs[1].isDisable = !generationOptions.multiplatform + codeTabs.tabs[2].isDisable = !generationOptions.multiplatform + + generateCode() + + } + fun newDB() { val modal = find() modal.openModal(block = true) val connStr = modal.connStr + val schema = modal.schema.let { if (it.isBlank()) null else it } + val useTables = if (modal.useTables.isBlank()) null else modal.useTables.split(",").map { it.trim() } saveFile = null exportFile = null - db = Database.fromConnection(connStr) + db = Database.fromConnection(connStr, schema, useTables) isSaveCurrent = true generateCode() @@ -390,4 +665,4 @@ class AppView : View() { val exportStarter = "// Made with Exposed DaoGen (https://github.com/rnett/ExposedDaoGen). Exported from " -data class DBSave(val db: Database.Data, val exportFilePath: String) \ No newline at end of file +data class DBSave(val db: Database.Data, val exportFilePath: String, val generationOptions: GenerationOptions) \ No newline at end of file diff --git a/src/main/kotlin/com/rnett/daogen/app/TreeItems.kt b/src/main/kotlin/com/rnett/daogen/app/TreeItems.kt index ba17f28..e3be866 100644 --- a/src/main/kotlin/com/rnett/daogen/app/TreeItems.kt +++ b/src/main/kotlin/com/rnett/daogen/app/TreeItems.kt @@ -10,17 +10,28 @@ class ClassItem(val table: Table) : TreeItem(table.classDisplayName), Ha override val item = table.classDisplay init { - children.addAll( - ColumnsItem(table, false), - FKsItem(table, false), - RKsItem(table, false)) - isExpanded = true - - item.model.displayName.addListener { _ -> - Event.fireEvent(this@ClassItem, TreeModificationEvent(TreeItem.valueChangedEvent(), this@ClassItem)) - } - item.model.otherDisplayName.addListener { _ -> - Event.fireEvent(this@ClassItem, TreeModificationEvent(TreeItem.valueChangedEvent(), this@ClassItem)) + if (table.canMakeClass) { + children.addAll( + ColumnsItem(table, false), + FKsItem(table, false), + RKsItem(table, false)) + isExpanded = true + + item.model.displayName.addListener { _ -> + Event.fireEvent(this@ClassItem, TreeModificationEvent(TreeItem.valueChangedEvent(), this@ClassItem)) + } + item.model.otherDisplayName.addListener { _ -> + Event.fireEvent(this@ClassItem, TreeModificationEvent(TreeItem.valueChangedEvent(), this@ClassItem)) + } + } else { + children.add(TreeItem("Can't make table with ${ + when { + table.pkType == Table.PKType.Composite -> "multiple" + table.primaryKeys.isEmpty() -> "no" + else -> "these" + } + } primary keys")) + isExpanded = true } } @@ -47,7 +58,7 @@ class ObjectItem(val table: Table) : TreeItem(table.objectDisplayName), class ColumnsItem(val table: Table, val isObject: Boolean) : TreeItem("Columns") { init { - children.addAll(table.columns.values.map { ColumnItem(it, isObject) }) + children.addAll(table.columns.values.map { ColumnItem(it, isObject, it in table.primaryKeys.map { it.key }) }) isExpanded = true } } @@ -67,7 +78,10 @@ class RKsItem(val table: Table, val isObject: Boolean) : TreeItem("Refer } } -class ColumnItem(val column: Column, val isObject: Boolean) : TreeItem(if (isObject) column.objectDisplayName else column.classDisplayName), HasItem { +class ColumnItem(val column: Column, val isObject: Boolean, val pk: Boolean) : TreeItem( + (if (pk) "* " else "") + + if (isObject) column.objectDisplayName else column.classDisplayName +), HasItem { override val item = column.Display(isObject) //I have no idea why using the fields NPEs init { diff --git a/src/main/kotlin/com/rnett/daogen/database/DB.kt b/src/main/kotlin/com/rnett/daogen/database/DB.kt index 8247a60..9e72efd 100644 --- a/src/main/kotlin/com/rnett/daogen/database/DB.kt +++ b/src/main/kotlin/com/rnett/daogen/database/DB.kt @@ -12,4 +12,5 @@ object DB { _connection = DriverManager.getConnection(str) return _connection!! } -} \ No newline at end of file +} + diff --git a/src/main/kotlin/com/rnett/daogen/ddl/Column.kt b/src/main/kotlin/com/rnett/daogen/ddl/Column.kt index a8ce018..16a6022 100644 --- a/src/main/kotlin/com/rnett/daogen/ddl/Column.kt +++ b/src/main/kotlin/com/rnett/daogen/ddl/Column.kt @@ -13,20 +13,70 @@ data class Column( val notNull: Boolean, val autoIncrement: Boolean ) : TableElement { - fun makeForObject(): String = "val $objectDisplayName = ${type.getKotlin(name)}" + - (if (!notNull) ".nullable()" else "") + + + fun useNullable(options: GenerationOptions) = (!notNull && options.nullableByDefault) || forceNullable + + fun makeForObject(options: GenerationOptions): String = "val $objectDisplayName = ${type.getKotlin(name)}" + + (if (useNullable(options)) ".nullable()" else "") + if (autoIncrement) ".autoIncrement()" else "" - fun makeForClass(objectName: String): String = "${if (mutable) "var" else "val"} $classDisplayName by $objectName.$objectDisplayName" + fun makeForClass(objectName: String, internalPrivate: Boolean, options: GenerationOptions): String { + if (type.type == Type.Decimal) { + if (!internalPrivate) + return buildString { + appendln("${if (mutable) "var" else "val"} ${classDisplayName}BD by $objectName.$objectDisplayName") + appendln("\t${if (options.multiplatform) "actual " else ""}${if (mutable) "var" else "val"} $classDisplayName") + appendln("\t\tget() = ${classDisplayName}BD.toDouble()") + if (mutable) + appendln("\t\tset(v){ ${classDisplayName}BD = v.toBigDecimal() }") + } + else { + return buildString { + appendln("var ${classDisplayName}BD by $objectName.$objectDisplayName") + if (!mutable) + appendln("\t\tprivate set") + appendln("\tactual var ${classDisplayName}") + appendln("\t\tget() = ${classDisplayName}BD.toDouble()") + + append("\t\t") + if (!mutable) + append("private") + + appendln("set(v){ ${classDisplayName}BD = v.toBigDecimal() }") + + } + } + } else { + if (!internalPrivate) + return "${if (options.multiplatform) "actual " else ""}${if (mutable) "var" else "val"} $classDisplayName by $objectName.$objectDisplayName" + else { + return buildString { + appendln("${if (options.multiplatform) "actual " else ""}var $classDisplayName by $objectName.$objectDisplayName") + if (!mutable) + appendln("\t\tprivate set") + } + } + } + } override fun toString(): String = "$classDisplayName $type" + (if (notNull) " not null" else "") + if (autoIncrement) " auto increment" else "" + fun makeForCommon(): String { + return "${if (mutable) "var" else "val"} $classDisplayName: ${type.type.kotlinType}" + } + + fun makeForJS(): String { + return "actual ${if (mutable) "var" else "val"} $classDisplayName: ${type.type.kotlinType}" + } + var classDisplayName: String = name var objectDisplayName: String = name var mutable: Boolean = false + var forceNullable = false + inner class Display(val isObject: Boolean) : EditableItem() { override var displayName @@ -47,6 +97,12 @@ data class Column( this@Column.mutable = v } + var forceNullable + get() = this@Column.forceNullable + set(v) { + this@Column.forceNullable = v + } + override val name: String = "Column in " + (if (isObject) "Object: " else "Class: ") + this@Column.toString() val mutableModel = MutableModel() @@ -56,10 +112,12 @@ data class Column( propTextBox("Object Property Name: ", model.displayName) propTextBox("Class Property Name: ", model.otherDisplayName) propCheckBox("Mutable: ", mutableModel.mutable) + propCheckBox("Force Nullable: ", mutableModel.forceNullable) } inner class MutableModel : ItemViewModel(this@Display) { val mutable = bind(Display::mutable, true) + val forceNullable = bind(Display::forceNullable, true) } } @@ -83,6 +141,8 @@ class ForigenKey( var nullable = !fromColumn.notNull + fun useNullable(options: GenerationOptions) = fromColumn.useNullable(options) + private fun getRKName(): String = if (toTable.referencingKeys.count { it.fromTable == fromTable } > 1) "${fromTable.classDisplayName}_${fromColumn.classDisplayName}" @@ -92,8 +152,11 @@ class ForigenKey( .removeSuffix("Id") .removeSuffix("id") .pluralize() - - if (test !in toTable.badClassNames) test else test + "_rk" + val bads = (toTable.badClassNames + toTable.objectDisplayName + fromTable.objectDisplayName) + if (test !in bads) + test + else + test + "_rk" } private fun getFKName(badNames: Set): String { @@ -207,16 +270,16 @@ class ForigenKey( //TODO mutable. class fk only? - fun makeReferencingForClass(): String = "val $rkClassName by ${fromTable.classDisplayName} " + - (if (nullable) "optionalReferrersOn" else "referrersOn") + + fun makeReferencingForClass(options: GenerationOptions): String = "${/*if(options.multiplatform) "actual " else*/ ""}val $rkClassName: SizedIterable<${fromTable.classDisplayName}> by ${fromTable.classDisplayName} " + + (if (useNullable(options)) "optionalReferrersOn" else "referrersOn") + " ${fromTable.objectDisplayName}.$fkObjectName" - fun makeForeignForObject() = "val $fkObjectName = " + - (if (nullable) "optReference" else "reference") + + fun makeForeignForObject(options: GenerationOptions) = "val $fkObjectName = " + + (if (useNullable(options)) "optReference" else "reference") + "(\"${fromColumn.name}\", ${toTable.objectDisplayName})" - fun makeForeignForClass() = "${if (mutable) "var" else "val"} $fkClassName by ${toTable.classDisplayName} " + - (if (nullable) "optionalReferencedOn" else "referencedOn") + + fun makeForeignForClass(options: GenerationOptions) = "${/*if(options.multiplatform) "actual " else*/ ""}${if (mutable) "var" else "val"} $fkClassName: ${toTable.classDisplayName + if (useNullable(options)) "?" else ""} by ${toTable.classDisplayName} " + + (if (useNullable(options)) "optionalReferencedOn" else "referencedOn") + " ${fromTable.objectDisplayName}.$fkObjectName" override fun toString(): String = "${fromTable.name}.${fromColumn.name} refers to ${toTable.name}.${toColumn.name}" @@ -252,8 +315,17 @@ class ForigenKey( return result } + fun makeForeignForJS() = "actual ${if (mutable) "var" else "val"} $fkClassName: ${toTable.classDisplayName}" + + (if (nullable) "?" else "") + + fun makeReferencingForJS() = "actual val $rkClassName: SizedIterable<${fromTable.classDisplayName}>" + + fun makeForeignForCommon() = "expect ${if (mutable) "var" else "val"} $fkClassName: ${toTable.classDisplayName}" + + (if (nullable) "?" else "") + + fun makeReferencingForCommon() = "expect val $rkClassName: SizedIterable<${fromTable.classDisplayName}>" + val fkClassDisplay get() = FKDisplay(false) val fkObjectDisplay get() = FKDisplay(true) val rkDisplay get() = RKDisplay() - } \ No newline at end of file diff --git a/src/main/kotlin/com/rnett/daogen/ddl/Database.kt b/src/main/kotlin/com/rnett/daogen/ddl/Database.kt index 5e36e57..86724e8 100644 --- a/src/main/kotlin/com/rnett/daogen/ddl/Database.kt +++ b/src/main/kotlin/com/rnett/daogen/ddl/Database.kt @@ -5,7 +5,9 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.rnett.daogen.app.exportStarter import com.rnett.daogen.database.DB +import java.io.File import java.sql.Connection +import kotlin.contracts.ExperimentalContracts data class Database(val schema: String?, val tables: MutableList = mutableListOf()) : Seraliziable { @@ -36,12 +38,12 @@ data class Database(val schema: String?, val tables: MutableList
= mutabl operator fun get(json: String) = fromJson(json) - fun fromConnection(connectionString: String, schema: String? = "public") = DB.connect(connectionString).generateDB(connectionString, schema) - operator fun invoke(connectionString: String, schema: String? = "public") = fromConnection(connectionString, schema) + fun fromConnection(connectionString: String, schema: String? = "public", useTables: List? = null) = DB.connect(connectionString).generateDB(connectionString, schema, useTables) + operator fun invoke(connectionString: String, schema: String? = "public", useTables: List? = null) = fromConnection(connectionString, schema, useTables) } } -fun Connection.generateDB(connectionString: String, schema: String? = "public"): Database { +fun Connection.generateDB(connectionString: String, schema: String? = "public", useTables: List?): Database { val db = Database(schema) @@ -51,7 +53,7 @@ fun Connection.generateDB(connectionString: String, schema: String? = "public"): }.filterNotNull().toList() // must be inside the use() block } - val tables = tableNames.map { Pair(it, Table(it, db)) }.toMap() + val tables = tableNames.filter { useTables?.contains(it) ?: true }.map { Pair(it, Table(it, db)) }.toMap() val allKeys = tables.flatMap { DB.connection!!.metaData.getImportedKeys(null, null, it.key).let { @@ -79,28 +81,207 @@ fun Connection.generateDB(connectionString: String, schema: String? = "public"): return db } -fun Database.generateKotlin(exportFilePath: String = "") = buildString { +data class GenerationOptions( + var outPackage: String, + var serialization: Boolean = true, + var serializationIncludeColumns: Boolean = true, + var doDao: Boolean = true, + var multiplatform: Boolean = true, + var nullableByDefault: Boolean = false +) + +@ExperimentalContracts +fun Database.generateKotlin(options: GenerationOptions, exportFilePath: String = "") = buildString { if (exportFilePath.isNotBlank()) appendln(exportStarter + exportFilePath) + appendln("\npackage ${options.outPackage}\n") + appendln() appendln(""" -import org.jetbrains.exposed.dao.IntEntity -import org.jetbrains.exposed.dao.IntEntityClass -import org.jetbrains.exposed.dao.IntIdTable -import org.jetbrains.exposed.dao.EntityID import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.dao.* """.trimIndent()) + if (options.serialization) + appendln(""" +import kotlinx.serialization.* +import kotlinx.serialization.internal.HexConverter +import kotlinx.serialization.internal.StringDescriptor +import kotlinx.serialization.internal.SerialClassDescImpl + """.trimIndent()) + appendln() val tableNames = this@generateKotlin.tables.flatMap { listOf(it.name, it.classDisplayName, it.objectDisplayName) }.toSet() this@generateKotlin.tables.forEach { try { - appendln(it.toKotlin(tableNames)) + appendln(it.toKotlin(options)) + } catch (e: Exception) { + } + } +} + +@ExperimentalContracts +fun Database.generateKotlinJS(options: GenerationOptions): String = buildString { + + appendln("\npackage ${options.outPackage}\n") + + appendln() + + if (options.serialization) + appendln(""" +import kotlinx.serialization.* +import kotlinx.serialization.internal.HexConverter +import kotlinx.serialization.internal.StringDescriptor +import kotlinx.serialization.internal.SerialClassDescImpl + """.trimIndent()) + + appendln() + + this@generateKotlinJS.tables.forEach { + try { + appendln(it.makeClassForJS(options)) + } catch (e: Exception) { + } + } +} + +@ExperimentalContracts +fun Database.generateKotlinCommon(options: GenerationOptions): String = buildString { + + appendln("\npackage ${options.outPackage}\n") + + appendln() + + if (options.serialization) + appendln(""" +import kotlinx.serialization.* +import kotlinx.serialization.internal.HexConverter +import kotlinx.serialization.internal.StringDescriptor +import kotlinx.serialization.internal.SerialClassDescImpl + """.trimIndent()) + + appendln() + + this@generateKotlinCommon.tables.forEach { + try { + appendln(it.makeClassForCommon(options)) } catch (e: Exception) { } } -} \ No newline at end of file +} + + +@ExperimentalContracts +fun Database.generateToFileSystemMultiplatform(baseDir: String, options: GenerationOptions) { + generateCommon(File(baseDir.trimEnd('/') + "/commonMain/kotlin/${options.outPackage.replace('.', '/')}"), options) + generateJS(File(baseDir.trimEnd('/') + "/jsMain/kotlin/${options.outPackage.replace('.', '/')}"), options) + generateJVM(File(baseDir.trimEnd('/') + "/jvmMain/kotlin/${options.outPackage.replace('.', '/')}"), options) +} + +@ExperimentalContracts +fun Database.generateToFileSystemPureJVM(baseDir: String, options: GenerationOptions) { + generateJVM(File(baseDir.trimEnd('/') + "/" + options.outPackage.replace('.', '/')), options) +} + +@ExperimentalContracts +private fun Database.generateJVM(packageBase: File, options: GenerationOptions) { + packageBase.mkdirs() + tables.forEach { + val out = File(packageBase.path.trimEnd('/') + "/${it.name}.kt") + out.createNewFile() + out.writeText(buildString { + appendln("\npackage ${options.outPackage}\n") + + appendln() + + appendln(""" +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.dao.* +""".trimIndent()) + + if (options.serialization) + appendln(""" +import kotlinx.serialization.* +import kotlinx.serialization.internal.HexConverter +import kotlinx.serialization.internal.StringDescriptor +import kotlinx.serialization.internal.SerialClassDescImpl + """.trimIndent()) + + appendln() + + try { + appendln(it.toKotlin(options)) + } catch (e: Exception) { + } + + }) + } +} + +@ExperimentalContracts +private fun Database.generateJS(packageBase: File, options: GenerationOptions) { + packageBase.mkdirs() + tables.forEach { + val out = File(packageBase.path.trimEnd('/') + "/${it.name}.kt") + out.createNewFile() + out.writeText(buildString { + appendln("\npackage ${options.outPackage}\n") + + appendln() + + if (options.serialization) + appendln(""" +import kotlinx.serialization.* +import kotlinx.serialization.internal.HexConverter +import kotlinx.serialization.internal.StringDescriptor +import kotlinx.serialization.internal.SerialClassDescImpl + """.trimIndent()) + + appendln() + + try { + appendln(it.makeClassForJS(options)) + } catch (e: Exception) { + } + + }) + } +} + +@ExperimentalContracts +private fun Database.generateCommon(packageBase: File, options: GenerationOptions) { + packageBase.mkdirs() + tables.forEach { + val out = File(packageBase.path.trimEnd('/') + "/${it.name}.kt") + out.createNewFile() + out.writeText(buildString { + appendln("\npackage ${options.outPackage}\n") + + appendln() + + if (options.serialization) + appendln(""" +import kotlinx.serialization.* +import kotlinx.serialization.internal.HexConverter +import kotlinx.serialization.internal.StringDescriptor +import kotlinx.serialization.internal.SerialClassDescImpl + """.trimIndent()) + + appendln() + + try { + appendln(it.makeClassForCommon(options)) + } catch (e: Exception) { + } + + }) + } +} diff --git a/src/main/kotlin/com/rnett/daogen/ddl/Table.kt b/src/main/kotlin/com/rnett/daogen/ddl/Table.kt index 204cd75..4ecd851 100644 --- a/src/main/kotlin/com/rnett/daogen/ddl/Table.kt +++ b/src/main/kotlin/com/rnett/daogen/ddl/Table.kt @@ -2,12 +2,14 @@ package com.rnett.daogen.ddl import com.cesarferreira.pluralize.pluralize import com.cesarferreira.pluralize.singularize +import com.rnett.core.advancedBuildString +import com.rnett.core.advancedBuildStringNoContract import com.rnett.daogen.app.EditableItem import com.rnett.daogen.database.DB -import com.rnett.daogen.doDAO import javafx.scene.Parent import tornadofx.* import java.sql.JDBCType +import kotlin.contracts.ExperimentalContracts data class PrimaryKey(val index: Int, val key: Column) { override fun toString(): String = key.name @@ -32,7 +34,7 @@ private fun getPkString(pks: Set): String { return sb1.toString() } -private fun getPkIdString(pks: Set): String { +private fun getPkIdString(pks: List): String { val list = pks.map { it.key.name } @@ -83,24 +85,21 @@ class Table( } //TODO use varchar as a key by hashcoding it? What about on postgres side? - enum class PKType(val valid: Boolean = true, val composite: Boolean = false) { - Int, Long, Other(false), CompositeInt(composite = true), CompositeLong(composite = true), CompositeOther(false, true) + enum class PKType(val valid: Boolean = true) { + Int, Long, Composite, Other(false) } + val primaryKey get() = primaryKeys.first() + val pkType get() = - if (primaryKeys.size > 1) - when { - primaryKeys.all { it.key.type.type == Type.IntType } -> PKType.CompositeInt - primaryKeys.all { it.key.type.type == Type.IntType || it.key.type.type == Type.LongType } -> PKType.CompositeInt - else -> PKType.CompositeOther - } - else - when { - primaryKeys.first().key.type.type == Type.IntType -> PKType.Int - primaryKeys.first().key.type.type == Type.LongType -> PKType.Int - else -> PKType.Other - } + when { + primaryKeys.size > 1 && primaryKeys.all { it.key.type.type == Type.IntType || it.key.type.type == Type.LongType } -> PKType.Composite + primaryKeys.size == 0 -> PKType.Other + primaryKey.key.type.type == Type.IntType -> PKType.Int + primaryKey.key.type.type == Type.LongType -> PKType.Long + else -> PKType.Other + } val objectName = name.toObjectName() val className = name.toClassName() @@ -108,13 +107,15 @@ class Table( var objectDisplayName = objectName var classDisplayName = className + var makeConstructor = false + override val data get() = Data(this) - class Data(val name: String, val columns: List, val pks: Set>, val className: String, val objectName: String) : Seralizer { - constructor(table: Table) : this(table.name, table.columns.values.toList(), table.primaryKeys.map { Pair(it.key.name, it.index) }.toSet(), table.classDisplayName, table.objectDisplayName) + class Data(val name: String, val columns: List, val pks: List>, val className: String, val objectName: String, val makeConstructor: Boolean) : Seralizer { + constructor(table: Table) : this(table.name, table.columns.values.toList(), table.primaryKeys.map { Pair(it.key.name, it.index) }, table.classDisplayName, table.objectDisplayName, table.makeConstructor) override fun create(parent: Database): Table { - val t = Table(name, columns.toSet(), pks.map { (key, idx) -> PrimaryKey(idx, columns.find { it.name == key }!!) }.toSet(), parent) + val t = Table(name, columns.toSet(), pks.map { pair -> PrimaryKey(pair.second, columns.find { it.name == pair.first }!!) }.toSet(), parent) t.classDisplayName = className t.objectDisplayName = objectName @@ -136,33 +137,49 @@ class Table( classDisplayName = v } + var makeConstructor + get() = this@Table.makeConstructor + set(v) { + this@Table.makeConstructor = v + } + override val name: String = (if (isObject) "Object: " else "Class: ") + this@Table.toString() + inner class MakeConstructorModel : ItemViewModel(this@Display) { + val makeConstructor = bind(Display::makeConstructor, true) + } + + val makeConstructorModel = MakeConstructorModel() + override val root: Parent = vbox { paddingTop = 20 propTextBox("Object Name: ", model.displayName) propTextBox("Class Name: ", model.otherDisplayName) + propCheckBox("Make Constructor: ", makeConstructorModel.makeConstructor) } } val blacklisted = mutableSetOf() - fun toKotlin(tableNames: Set) = "${makeForObject(tableNames)}${if (doDAO) "\n\n\n" + makeForClass(tableNames) else ""}" + @ExperimentalContracts + fun toKotlin(options: GenerationOptions) = + "${makeForObject(options)}${if (options.doDao) "\n\n\n" + makeForClass(options) else ""}" val badClassNames get() = (database.tables.flatMap { listOf(it.classDisplayName, it.objectDisplayName) } + columns.filter { it.value !in blacklisted }.map { it.value.classDisplayName }).toSet() val badObjectNames get() = (database.tables.flatMap { listOf(it.classDisplayName, it.objectDisplayName) } + columns.filter { it.value !in blacklisted }.map { it.value.objectDisplayName }).toSet() - fun makeForObject(tableNames: Set): String = - buildString { + @ExperimentalContracts + fun makeForObject(options: GenerationOptions): String = + advancedBuildString { append("object $objectDisplayName : ") - when (pkType) { - PKType.Int -> "IntIdTable(\"$name\", \"${primaryKeys.first().key.name}\")" - PKType.Long -> "LongIdTable(\"$name\", \"${primaryKeys.first().key.name}\")" - PKType.CompositeInt -> "IntIdTable(\"$name\", \"${getPkString(primaryKeys)}\")" - PKType.CompositeLong -> "LongIdTable(\"$name\", \"${getPkString(primaryKeys)}\")" + append(when (pkType) { + PKType.Int -> "IntIdTable(\"$name\", \"${primaryKey.key.name}\")" + PKType.Long -> "LongIdTable(\"$name\", \"${primaryKey.key.name}\")" + PKType.Composite -> "IntIdTable(\"$name\", \"${getPkString(primaryKeys)}\")" + else -> "Table(\"$name\")" - }.let { append(it) } + }) appendln(" {") appendln() @@ -171,7 +188,7 @@ class Table( columns.values.filter { it !in blacklisted }.forEach { append("\t") - append(it.makeForObject()) + append(it.makeForObject(options)) primaryKeys.find { pk -> pk.key == it }?.apply { if (primaryKeys.count() > 1) @@ -179,82 +196,133 @@ class Table( else append(".primaryKey()") } - appendln() + +"" } - if (foreignKeys.any { it !in blacklisted }) { + if (foreignKeys.any { it !in blacklisted && it.toTable.canMakeClass && it.fromTable.canMakeClass }) { appendln("\n") appendln("\t// Foreign/Imported Keys (One to Many)\n") - foreignKeys.filter { it !in blacklisted }.forEach { + foreignKeys.filter { it !in blacklisted && it.toTable.canMakeClass && it.fromTable.canMakeClass }.forEach { append("\t") - appendln(it.makeForeignForObject()) + appendln(it.makeForeignForObject(options)) } } - if (referencingKeys.any { it !in blacklisted }) { + if (referencingKeys.any { it !in blacklisted && it.toTable.canMakeClass && it.fromTable.canMakeClass }) { appendln("\n") appendln("\t// Referencing/Exported Keys (One to Many)\n") - appendln("\t// ${referencingKeys.count { it !in blacklisted }} keys. Not present in object") + appendln("\t// ${referencingKeys.count { it !in blacklisted && it.toTable.canMakeClass && it.fromTable.canMakeClass }} keys. Not present in object") } - appendln("}") + +"}" } - fun makeForClass(tableNames: Set): String = - buildString { + fun makeForClass(options: GenerationOptions): String = + advancedBuildStringNoContract { + //TODO use the advanced features - if (pkType == PKType.Other || pkType == PKType.CompositeOther) - throw IllegalArgumentException("Can not (yet) make classes for non-Int or Long keyed or composite keyed tables") + if (pkType == PKType.Composite || pkType == PKType.Other) + return@advancedBuildStringNoContract val keyType = when (pkType) { PKType.Int -> "Int" PKType.Long -> "Long" - PKType.CompositeInt -> "Int" - PKType.CompositeLong -> "Long" else -> "" } - appendln("class $classDisplayName(id: EntityID<$keyType>) : ${keyType}Entity(id) {") + appendln("${if (options.multiplatform) "actual " else ""}class $classDisplayName(${if (options.serialization) "val myId" else "id"}: EntityID<$keyType>) : ${keyType}Entity(${if (options.serialization) "myId" else "id"}) {\n") - appendln("\tcompanion object : ${keyType}EntityClass<$classDisplayName>($objectDisplayName) {") + if (options.serialization) + appendln("\t@Serializer($classDisplayName::class)") - append("\t\t") - appendln("fun idFromPKs(" + - primaryKeys.filter { it.key !in blacklisted }.joinToString(", ") { "${it.key.name}: ${it.key.type.type.kotlinType}" } + - "): " + - (if (pkType == PKType.CompositeInt || pkType == PKType.Int) "Int" else "Long") + - " = " + - getPkIdString(primaryKeys.filter { it.key !in blacklisted }.toSet())) - appendln() + append("\t${if (options.multiplatform) "actual " else ""}companion object : ${keyType}EntityClass<$classDisplayName>($objectDisplayName)") - append("\t\t") - appendln("fun findByPKs(" + - primaryKeys.filter { it.key !in blacklisted }.joinToString(", ") { "${it.key.name}: ${it.key.type.type.kotlinType}" } + - ") = findById(idFromPKs(" + - primaryKeys.filter { it.key !in blacklisted }.joinToString(", ") { it.key.name } + - "))") + if (options.serialization) + append(", KSerializer<$classDisplayName>") - if (pkType.composite) { - appendln() + appendln("{") + + if (options.serialization) { + + if (!options.serializationIncludeColumns) { + + append("\t\t") + appendln("${if (options.multiplatform) "actual " else ""}override val descriptor: SerialDescriptor = StringDescriptor.withName(\"$classDisplayName\")") + appendln() + + append("\t\t") + appendln("${if (options.multiplatform) "actual " else ""}override fun serialize(output: Encoder, obj: $classDisplayName) {") + appendln("\t\t\toutput.encodeString(HexConverter.printHexBinary(obj.${primaryKey.key.name}.toString().toUtf8Bytes()))") + appendln("\t\t}\n") + + append("\t\t") + appendln("${if (options.multiplatform) "actual " else ""}override fun deserialize(input: Decoder): $classDisplayName {") + appendln("\t\t\treturn $classDisplayName[stringFromUtf8Bytes(HexConverter.parseHexBinary(input.decodeString())).to$pkType()]") + appendln("\t\t}\n") + } else { + + val indicies = columns.values.filter { it !in blacklisted }.toList().mapIndexed { i, c -> Pair(i, c) }.toMap() + + val pkIndex = indicies.entries.find { it.value == primaryKey.key }!!.key + + append("\t\t") + appendln("${if (options.multiplatform) "actual " else ""}override val descriptor: SerialDescriptor = object : SerialClassDescImpl(\"$classDisplayName\") {") + appendln("\t\t\tinit{") + indicies.entries.sortedBy { it.key }.forEach { + appendln("\t\t\t\taddElement(\"${it.value.name}\")") + } + appendln("\t\t\t}") + appendln("\t\t}\n") + + append("\t\t") + appendln("${if (options.multiplatform) "actual " else ""}override fun serialize(output: Encoder, obj: $classDisplayName) {") + appendln("\t\t\tval compositeOutput: CompositeEncoder = output.beginStructure(descriptor)") + indicies.entries.sortedBy { it.key }.forEach { + appendln("\t\t\tcompositeOutput.encodeStringElement(descriptor, ${it.key}, HexConverter.printHexBinary(obj.${it.value.name}.toString().toUtf8Bytes()))") + } + appendln("\t\t\tcompositeOutput.endStructure(descriptor)") + appendln("\t\t}\n") + + append("\t\t") + appendln("${if (options.multiplatform) "actual " else ""}override fun deserialize(input: Decoder): $classDisplayName {") + appendln("\t\t\tval inp: CompositeDecoder = input.beginStructure(descriptor)") + appendln("\t\t\tvar id: $pkType? = null") + appendln("\t\t\tloop@ while (true) {") + + appendln("\t\t\t\twhen (val i = inp.decodeElementIndex(descriptor)) {") + appendln("\t\t\t\t\tCompositeDecoder.READ_DONE -> break@loop") + appendln("\t\t\t\t\t$pkIndex -> id = stringFromUtf8Bytes(HexConverter.parseHexBinary(inp.decodeStringElement(descriptor, i))).to$pkType()") + appendln("\t\t\t\t\telse -> if (i < descriptor.elementsCount) continue@loop else throw SerializationException(\"Unknown index \$i\")") + appendln("\t\t\t\t}") + appendln("\t\t\t}\n") + appendln("\t\t\tinp.endStructure(descriptor)") + appendln("\t\t\tif(id == null)") + appendln("\t\t\t\tthrow SerializationException(\"Id '${primaryKey.key.name}' @ index $pkIndex not found\")") + appendln("\t\t\telse") + appendln("\t\t\t\treturn $classDisplayName[id]") + + appendln("\t\t}\n") + + } append("\t\t") - appendln("operator fun get(" + - primaryKeys.filter { it.key !in blacklisted }.joinToString(", ") { "${it.key.name}: ${it.key.type.type.kotlinType}" } + - ") = findByPKs(" + - primaryKeys.filter { it.key !in blacklisted }.joinToString(", ") { it.key.name } + - ")") + appendln("${if (options.multiplatform) "actual " else ""}fun serializer(): KSerializer<$classDisplayName> = this") appendln() + } + if (makeConstructor) { append("\t\t") - appendln("fun new(" + - primaryKeys.filter { it.key !in blacklisted }.joinToString(", ") { "${it.key.name}: ${it.key.type.type.kotlinType}" } + - ", init: $classDisplayName.() -> Unit) = new(idFromPKs(" + - primaryKeys.filter { it.key !in blacklisted }.joinToString(", ") { it.key.name } + - "), init)") + appendln("fun new(${columns.values.filter { it !in blacklisted }.joinToString(", ") { + "${it.name}: ${it.type.type.kotlinType}" + }}) = new {") + columns.values.filter { it !in blacklisted }.forEach { + appendln("\t\t\t_${it.name} = ${it.name}") + } + appendln("\t\t}") } appendln("\t}\n") @@ -263,28 +331,28 @@ class Table( columns.values.filter { it !in blacklisted }.forEach { append("\t") - appendln(it.makeForClass(objectDisplayName)) + appendln(it.makeForClass(objectDisplayName, makeConstructor, options)) } - if (foreignKeys.any { it !in blacklisted }) { + if (foreignKeys.any { it !in blacklisted && it.toTable.canMakeClass && it.fromTable.canMakeClass }) { appendln("\n") appendln("\t// Foreign/Imported Keys (One to Many)\n") - foreignKeys.filter { it !in blacklisted }.forEach { + foreignKeys.filter { it !in blacklisted && it.toTable.canMakeClass && it.fromTable.canMakeClass }.forEach { append("\t") - appendln(it.makeForeignForClass()) + appendln(it.makeForeignForClass(options)) } } - if (referencingKeys.any { it !in blacklisted }) { + if (referencingKeys.any { it !in blacklisted && it.toTable.canMakeClass && it.fromTable.canMakeClass }) { appendln("\n") appendln("\t// Referencing/Exported Keys (One to Many)\n") - referencingKeys.filter { it !in blacklisted }.forEach { + referencingKeys.filter { it !in blacklisted && it.toTable.canMakeClass && it.fromTable.canMakeClass }.forEach { append("\t") - appendln(it.makeReferencingForClass()) + appendln(it.makeReferencingForClass(options)) } } @@ -292,31 +360,197 @@ class Table( appendln("\t// Helper Methods\n") - appendln("\toverride fun equals(other: Any?): Boolean {") + appendln("\t${if (options.multiplatform) "actual " else ""}override fun equals(other: Any?): Boolean {") appendln("\t\tif(other == null || other !is $classDisplayName)") appendln("\t\t\treturn false") appendln() - appendln("\t\treturn ${primaryKeys.filter { it.key !in blacklisted }.map { it.key.name }.joinToString(" && ") { "$it == other.$it" }}") + appendln("\t\treturn ${primaryKey.key.name} == other.${primaryKey.key.name}") appendln("\t}") appendln("\n") - - appendln("\toverride fun hashCode(): Int = ${primaryKeys.first { it.key !in blacklisted }.key.name}") + appendln("\t${if (options.multiplatform) "actual " else ""}override fun hashCode() = ${primaryKey.key.name}${if (pkType == PKType.Long) ".hashCode()" else ""} ") appendln("\n") if (columns.values.filter { it.isNameColumn }.size == 1) - appendln("\toverride fun toString() = ${columns.values.find { it.isNameColumn }!!.name}") + appendln("\t${if (options.multiplatform) "actual " else ""}override fun toString() = ${columns.values.find { it.isNameColumn }!!.name}") appendln("}") } + val canMakeClass get() = pkType != PKType.Composite && pkType != PKType.Other + + @ExperimentalContracts + fun makeClassForCommon(options: GenerationOptions) = advancedBuildString { + + if (pkType == PKType.Composite || pkType == PKType.Other) + return@advancedBuildString + + +"expect class $classDisplayName" + codeBlock { + columns.values.filter { it !in blacklisted }.forEach { + +it.makeForCommon() + } + //TODO figure out how I want to handle references. (probably using kframe-data to make the get operator available) + + /* + foreignKeys.filter { it !in blacklisted }.forEach { + +it.makeForeignForCommon() + } + referencingKeys.filter { it !in blacklisted }.forEach { + +it.makeReferencingForCommon() + }*/ + +"" + +"override fun equals(other: Any?): Boolean" + +"override fun hashCode(): Int" + if (columns.values.filter { it.isNameColumn }.size == 1) + +"override fun toString(): String" + + + if (options.serialization) { + +"" + +"@Serializer($classDisplayName::class)" + +"companion object : KSerializer<$classDisplayName>" + codeBlock { + +"override val descriptor: SerialDescriptor\n" + +"override fun serialize(output: Encoder, obj: $classDisplayName)\n" + +"override fun deserialize(input: Decoder): $classDisplayName\n" + +"fun serializer(): KSerializer<$classDisplayName>" + } + } + } + } + + @ExperimentalContracts + fun makeClassForJS(options: GenerationOptions) = advancedBuildString { + + if (pkType == PKType.Composite || pkType == PKType.Other) + return@advancedBuildString + + +"actual data class $classDisplayName(" + +"\t${columns.values.filter { it !in blacklisted }.joinToString(",\n\t") { it.makeForJS() }}" + /* + +"\t${foreignKeys.filter { it !in blacklisted }.joinToString(",\n\t") { it.makeForeignForJS() }}" + +"\t${referencingKeys.filter { it !in blacklisted }.joinToString(",\n\t") { it.makeReferencingForJS() }}" + */ + +"){" + + appendln("\tactual override fun equals(other: Any?): Boolean {") + appendln("\t\tif(other == null || other !is $classDisplayName)") + appendln("\t\t\treturn false") + appendln() + appendln("\t\treturn ${primaryKey.key.name} == other.${primaryKey.key.name}") + appendln("\t}") + + appendln("\n") + appendln("\tactual override fun hashCode() = ${primaryKey.key.name}${if (pkType == PKType.Long) ".hashCode()" else ""}") + + appendln("\n") + + if (columns.values.filter { it.isNameColumn }.size == 1) + appendln("\tactual override fun toString() = ${columns.values.find { it.isNameColumn }!!.name}") + + +"" + + if (options.serialization) { + + +"\t@Serializer($classDisplayName::class)" + +"\tactual companion object : KSerializer<$classDisplayName> {" + + if (!options.serializationIncludeColumns) { + + append("\t\t") + appendln("actual override val descriptor: SerialDescriptor = StringDescriptor.withName(\"$classDisplayName\")") + appendln() + + append("\t\t") + appendln("actual override fun serialize(output: Encoder, obj: $classDisplayName) {") + appendln("\t\t\toutput.encodeString(HexConverter.printHexBinary(obj.${primaryKey.key.name}.toString().toUtf8Bytes()))") + appendln("\t\t}\n") + + append("\t\t") + appendln("actual override fun deserialize(input: Decoder): $classDisplayName {") + appendln("\t\t\treturn $classDisplayName[stringFromUtf8Bytes(HexConverter.parseHexBinary(input.decodeString())).toInt()]") + appendln("\t\t}\n") + } else { + + val indicies = columns.values.filter { it !in blacklisted }.toList().mapIndexed { i, c -> Pair(i, c) }.toMap() + + val pkIndex = indicies.entries.find { it.value == primaryKey.key } + + append("\t\t") + appendln("actual override val descriptor: SerialDescriptor = object : SerialClassDescImpl(\"$classDisplayName\") {") + appendln("\t\t\tinit{") + indicies.entries.sortedBy { it.key }.forEach { + appendln("\t\t\t\taddElement(\"${it.value.name}\")") + } + appendln("\t\t\t}") + appendln("\t\t}\n") + + append("\t\t") + appendln("actual override fun serialize(output: Encoder, obj: $classDisplayName) {") + appendln("\t\t\tval compositeOutput: CompositeEncoder = output.beginStructure(descriptor)") + indicies.entries.sortedBy { it.key }.forEach { + appendln("\t\t\tcompositeOutput.encodeStringElement(descriptor, ${it.key}, HexConverter.printHexBinary(obj.${it.value.name}.toString().toUtf8Bytes()))") + } + appendln("\t\t\tcompositeOutput.endStructure(descriptor)") + appendln("\t\t}\n") + + append("\t\t") + appendln("actual override fun deserialize(input: Decoder): $classDisplayName {") + appendln("\t\t\tval inp: CompositeDecoder = input.beginStructure(descriptor)") + + indicies.entries.sortedBy { it.key }.forEach { + +"\t\t\tvar temp_${it.value.name}: ${it.value.type.type.kotlinType}? = null" + } + + appendln("\t\t\tloop@ while (true) {") + + appendln("\t\t\t\twhen (val i = inp.decodeElementIndex(descriptor)) {") + appendln("\t\t\t\t\tCompositeDecoder.READ_DONE -> break@loop") + + indicies.entries.sortedBy { it.key }.forEach { (index, col) -> + +"\t\t\t\t\t$index -> temp_${col.name} = stringFromUtf8Bytes(HexConverter.parseHexBinary(inp.decodeStringElement(descriptor, i)))${col.type.type.fromString}" + } + + //appendln("\t\t\t\t\t$pkIndex -> id = HexConverter.parseHexBinary(inp.decodeStringElement(descriptor, i)).toString().toInt()") + + appendln("\t\t\t\t\telse -> if (i < descriptor.elementsCount) continue@loop else throw SerializationException(\"Unknown index \$i\")") + appendln("\t\t\t\t}") + appendln("\t\t\t}\n") + appendln("\t\t\tinp.endStructure(descriptor)") + + +"" + + appendln("\t\t\t\treturn $classDisplayName(${indicies.entries.sortedBy { it.key }.map { it.value }.joinToString(",\n\t\t\t\t", "\n\t\t\t\t", "\n\t\t\t") { + "temp_${it.name} ?: throw SerializationException(\"Missing value for ${it.name}\")" + }})") + + appendln("\t\t}\n") + + } + + append("\t\t") + appendln("actual fun serializer(): KSerializer<$classDisplayName> = this") + +"\t}" + appendln() + } + + +"}" + } + override fun toString(): String { return buildString { appendln(name + ":") - appendln("\tPrimary Key(s): ${primaryKeys.filter { it.key !in blacklisted }.sortedBy { it.index }.joinToString(", ")}") + appendln("\tPrimary Key: ${ + when (primaryKeys.size) { + 0 -> "None" + 1 -> primaryKey.key.name + else -> primaryKeys.joinToString(", ") { it.key.name } + } + }") appendln() columns.forEach { @@ -347,7 +581,7 @@ class Table( if (name != other.name) return false if (columns != other.columns) return false - if (primaryKeys != other.primaryKeys) return false + if (primaryKey != other.primaryKey) return false if (_foreignKeys != other._foreignKeys) return false if (_referencingKeys != other._referencingKeys) return false if (objectDisplayName != other.objectDisplayName) return false @@ -360,7 +594,7 @@ class Table( override fun hashCode(): Int { var result = name.hashCode() result = 31 * result + columns.hashCode() - result = 31 * result + primaryKeys.hashCode() + result = 31 * result + primaryKey.hashCode() result = 31 * result + _foreignKeys.hashCode() result = 31 * result + _referencingKeys.hashCode() result = 31 * result + objectDisplayName.hashCode() @@ -386,7 +620,9 @@ class Table( val type = when { typeName == "integer" -> Type.IntType.withData() - typeName == "double" -> Type.FloatType.withData() + typeName == "double" -> Type.DoubleType.withData() + typeName == "real" -> Type.FloatType.withData() + typeName == "float" -> Type.FloatType.withData() typeName == "varchar" && data == "text" -> Type.Text.withData() typeName == "varchar" -> Type.Varchar.withData(dataSize) typeName == "bigint" -> Type.LongType.withData() diff --git a/src/main/kotlin/com/rnett/daogen/ddl/Type.kt b/src/main/kotlin/com/rnett/daogen/ddl/Type.kt index 9b4d1a0..635030a 100644 --- a/src/main/kotlin/com/rnett/daogen/ddl/Type.kt +++ b/src/main/kotlin/com/rnett/daogen/ddl/Type.kt @@ -1,15 +1,16 @@ package com.rnett.daogen.ddl -enum class Type(val database: String, val kotlin: String, val kotlinType: String, val params: Int = 0) { +enum class Type(val database: String, val kotlin: String, val kotlinType: String, val params: Int = 0, val fromString: String = ".to$kotlinType()") { //TODO support for type name aliases, e.g. real and float IntType("int", "integer", "Int"), LongType("bigint", "long", "Long"), FloatType("float", "float", "Float"), DoubleType("double precision", "double", "Double"), - Decimal("decimal(\$1, \$2)", "decimal(\$name, \$1, \$2)", "BigDecimal", 2),//TODO w/ params + RealType("real", "double", "Double"), + Decimal("decimal(\$1, \$2)", "decimal(\$name, \$1, \$2)", "Double", 2),//TODO only do shenanigans if using multiplatform Bool("boolean", "bool", "Boolean"), - Char("char", "char", "Char"), - Varchar("varchar(\$1)", "varchar(\$name, \$1)", "String", 1),//TODO w/ param + Char("char", "char", "Char", fromString = "[0]"), + Varchar("varchar(\$1)", "varchar(\$name, \$1)", "String", 1), Text("text", "text", "String"), Unknown("", "//TODO unknown type", "String") //TODO more advanced types diff --git a/src/main/resources/com/rnett/daogen/app/AppView.fxml b/src/main/resources/com/rnett/daogen/app/AppView.fxml index 1b23ad3..4f366b6 100644 --- a/src/main/resources/com/rnett/daogen/app/AppView.fxml +++ b/src/main/resources/com/rnett/daogen/app/AppView.fxml @@ -71,6 +71,23 @@ shift="UP" shortcut="UP"/> + + + + + + + + + + + + + + @@ -172,16 +189,70 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +