From ba6dc52780da0c6ecc87737070c09926a6bb40ef Mon Sep 17 00:00:00 2001
From: Claude Brisson <claude@renegat.net>
Date: Tue, 5 Oct 2021 01:03:42 +0200
Subject: [PATCH] [wip] Modality format (compilation of generated sources seems
 ok)

---
 build.gradle.kts                              |   9 +-
 gradle.properties                             |   3 +
 ksplit.sh                                     |   8 +-
 .../kotlin/com/republicate/kddl/modality.kt   | 103 +++++++++++++-----
 4 files changed, 92 insertions(+), 31 deletions(-)

diff --git a/build.gradle.kts b/build.gradle.kts
index b4755c3..eddd4e4 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,7 +1,7 @@
 plugins {
     idea
     application
-    kotlin("multiplatform") version "1.4.10"
+    kotlin("multiplatform") version "1.5.31"
     `maven-publish`
 }
 group = "com.republicate.kddl"
@@ -40,8 +40,11 @@ kotlin {
 //                        from(zipTree(file.absoluteFile))
 //                    }
 //                }
-
-                kotlinOptions.jvmTarget = "1.8"
+                kotlinOptions {
+                    jvmTarget = "1.8"
+                    apiVersion = "1.5"
+                    languageVersion = "1.5"
+                }
             }
         }
         withJava()
diff --git a/gradle.properties b/gradle.properties
index ad6abc4..dcaad4b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,9 @@
+kotlin_version=1.5.31
 kotlin.code.style=official
 kotlin.js.generate.executable.default=false
 kotlin.mpp.stability.nowarn=true
+kotlin.mpp.enableGranularSourceSetsMetadata=true                                                                                                                                                                     
+kotlin.native.enableDependencyPropagation=false                                                                                                                                                                      
 #version_agl = 3.5.1-cb4
 version_agl = 3.5.1
 
diff --git a/ksplit.sh b/ksplit.sh
index 3c35126..3015b56 100755
--- a/ksplit.sh
+++ b/ksplit.sh
@@ -7,9 +7,11 @@ mkdir -p $KSPKIT_TMP
 DEST=$(pwd)
 pushd .
 cd $KSPKIT_TMP
-cat /dev/stdin | tail -n +2 | csplit -n5 -sz '// Database.*' '{*}'
-for file in $(ls) do
+cat /dev/stdin | csplit - -n5 -sz '/\/\/ Database.*/' '{*}'
+for file in $(ls)
+do
   dst=$(head -2 $file | tail -1 | sed -r -e 's|// Schema ||')
-  mv $file $DEST/${dst^}.java
+  echo mv $file $DEST/${dst^}.kt
+  mv $file $DEST/${dst^}.kt
 done
 popd
diff --git a/src/commonMain/kotlin/com/republicate/kddl/modality.kt b/src/commonMain/kotlin/com/republicate/kddl/modality.kt
index 7f1daf6..39aff87 100644
--- a/src/commonMain/kotlin/com/republicate/kddl/modality.kt
+++ b/src/commonMain/kotlin/com/republicate/kddl/modality.kt
@@ -1,5 +1,6 @@
 package com.republicate.kddl
 
+import java.util.Locale
 import net.akehurst.language.agl.processor.Agl
 import net.akehurst.language.api.processor.LanguageProcessor
 
@@ -37,7 +38,13 @@ class ModalityFormatter : KDDLFormatter() {
         ret.append("// Schema ${schema.name}$EOL")
         ret.append("${indent}package _package_.${schema.db.name}.${schema.name}$EOL$EOL")
         ret.append("import com.republicate.modality.Instance$EOL")
-//        ret.append("import com.republicate.modality.util.ConversionUtils$EOL")
+        ret.append("import com.republicate.modality.Model$EOL")
+        ret.append("import java.util.Date$EOL") // CB TODO - kotlinx-datetime objects are not yet serializable (v0.3.0 at the time of writing)
+        ret.append(EOL)
+        // CB TODO - temporary hack, fixing needed in upstream Modality
+        ret.append("fun Instance.getInt(key: String): Int = getInteger(key)$EOL")
+        ret.append("fun Instance.now(): Date = Date()$EOL")
+
         ret.append(EOL)
         ret.append(
             schema.tables.map {
@@ -47,16 +54,18 @@ class ModalityFormatter : KDDLFormatter() {
         return ret.toString()
     }
 
+    fun rawType(type: String?): String? = type?.replace(Regex("\\(.*\\)"), "")
+
     override fun format(indent: String, table: Table): String {
         val ret = StringBuilder()
         if (table !is JoinTable) {
             // TODO something to do for static atomic id generator with isDefaultKey()
-            ret.append("${indent}public class ${table.name}")
+            ret.append("${indent}public class ${table.name.capitalize()}")
             ret.append("(model: Model")
             val pk = table.getPrimaryKey()
             val defaultPk = pk.size == 1 && pk.first().isDefaultKey()
-            if ( !pk.isEmpty() && !defaultPk) ret.append(", ")
             for ( pkField in pk ) {
+                ret.append(", ")
                 ret.append(format("", pkField, true))
             }
             ret.append("): Instance(model.getEntity(\"${table.name.decapitalize()}\"))")
@@ -76,6 +85,17 @@ class ModalityFormatter : KDDLFormatter() {
                     ret.append("$EOL$indent  }")
                 }
 
+                ret.append("$EOL$EOL$indent  fun getInt(key: String) = super.getInteger(key)$EOL")
+
+                fields.filter {
+                    rawType(it.type)?.equals("enum") ?: false
+                }.forEach {
+                    ret.append("$EOL$indent  enum class ${it.name.capitalize()} { ")
+//                    ret.append("/*${it.type.replace(Regex("^.*\\((.*)\\)"), "$1")}*/")
+                    val values = it.type.replace(Regex("^.*\\((.*)\\)"), "$1").split(",").joinToString { it.removeSurrounding("'", "'").uppercase(Locale.ROOT) }
+                    ret.append("$values }")
+                }
+
                 for (field in fields) {
                     ret.append(EOL)
                     ret.append(format("${indent}  ", field))
@@ -91,11 +111,12 @@ class ModalityFormatter : KDDLFormatter() {
     }
 
     private val typeMap = mapOf(
+        "char" to "String",
         "varchar" to "String",
         "text" to "String",
         "boolean" to "Boolean",
-        "date" to "LocalDate",
-        "datetime" to "LocalDateTime",
+        "date" to "Date",
+        "datetime" to "Date",
         "integer" to "Int",
         "long" to "Long",
         "float" to "Float",
@@ -104,12 +125,14 @@ class ModalityFormatter : KDDLFormatter() {
         "numeric" to "Double",
         "serial" to "Long",
         "time" to "String",
-        "datetimetz" to "Instant",
+        "datetimetz" to "Date",
         "json" to "Json",
-        "jsonb" to "Json"
+        "jsonb" to "Json",
+        "enum" to "Enum"
     )
 
     private val defaultMap = mapOf(
+        "char" to "\"\"",
         "varchar" to "\"\"",
         "text" to "\"\"",
         "boolean" to "false",
@@ -135,8 +158,12 @@ class ModalityFormatter : KDDLFormatter() {
     fun format(indent: String, field: Field, typeParam: Boolean): String {
         val ret = StringBuilder(indent)
         field.apply {
-            val rawType = type?.replace(Regex("\\(\\d+(?:,\\d+)*\\)"), "")
-            val kotlinType = typeMap[rawType] ?: "UnknownType[${rawType}]"
+            val raw = rawType(type)
+            val kotlinType = when(raw) {
+                null -> "UnknownType[${raw}]"
+                "enum" -> name.capitalize()
+                else -> typeMap[raw]
+            }
             val mandatory = nonNull || primaryKey && !isDefaultKey()
             if (!typeParam) {
                 if (primaryKey) ret.append("val ")
@@ -145,23 +172,49 @@ class ModalityFormatter : KDDLFormatter() {
             val suffix = if(typeParam) "Param" else ""
             ret.append("${name}${suffix}: ${kotlinType}")
             if (!mandatory) ret.append("?")
-            if (!typeParam && default != null) {
-                val def = default.toString()
-                ret.append(
-                    " = ${
-                        when {
-                            def.startsWith("'") -> "\"${def.removeSurrounding("'", "'")}\""
-                            else -> def
-                        }
-                    }"
-                )
-            } else if (mandatory) {
-                ret.append(" // = ${defaultMap[rawType] ?: "UnknownDefault[${rawType}]"}")
-            }
+            // OLD default handling
+//            if (!typeParam && default != null) {
+//                val def = default.toString()
+//                ret.append(
+//                    " = ${
+//                        when {
+//                            def.startsWith("'") -> "\"${def.removeSurrounding("'", "'")}\""
+//                            else -> def
+//                        }
+//                    }"
+//                )
+//            }
+
             if (!typeParam) {
-                ret.append("$EOL$indent  get() = get${kotlinType}(\"${name}\")")
-                if (mandatory) ret.append("!!")
-                if (!primaryKey) ret.append("$EOL$indent  set(value) = put(\"${name}\", value)")
+//                OLD: ret.append("$EOL$indent  get() = get$kotlinType(\"${name}\")")
+                // CB TODO - by not returning null (for a bran new instance) for a mandatory field with a default, there is a small risk of introducing side effects
+                var def = when(default) {
+                    null -> ""
+                    is Boolean -> default.toString()
+                    is Number -> default.toString()
+                    // is String -> ret.append("DEFAULT ${defaultMap[default] ?: default}")
+                    is String -> "\"${default.toString()}\""
+                    is Function -> "$default()"
+                    is Function0<*> -> "${(default as Function0<String>).invoke()}"
+                    else -> throw RuntimeException("Unhandled default value type: $default")
+                }
+                if (!def.isEmpty()) ret.append("$EOL // @@@@@@@@@@@@@@@@@@@@@@@@  type = $type, default = $default ; def = $def")
+                val getter = when(raw) {
+                    null -> "UnknownGetter[${raw}]"
+                    "enum" -> "${name.capitalize()}.valueOf(super.getString(\"${name}\")${ if(mandatory) if (!def.isEmpty()) " ?: $def " else "!!" else "" })"
+                    else -> "get$kotlinType(\"${name}\")${ if (mandatory) if(!def.isEmpty()) " ?: $def" else "!!" else "" }"
+                }
+                ret.append("$EOL$indent  get() = $getter")
+                if (!primaryKey) {
+//                    OLD: ret.append("$EOL$indent  set(value) { put(\"${name}\", value) }")
+                    val setter = when(raw) {
+                        null -> "UnknownSetter[$type]"
+                        "enum" -> "super.put(\"${name}\", value?.name)"
+                        "date", "datetime", "datetimez" -> "super.put(\"${name}\", value?.toInstant().toString())"
+                        else -> "super.put(\"${name}\", value)"
+                    }
+                    ret.append("$EOL$indent  set(value) { $setter }")
+                }
             }
         }
         return ret.toString()
-- 
GitLab