Repository: julioz/FloorPlan (kotlin)

Issue:
Optionally annotate tables with creation SQL
Add [DBML table notes](https://www.dbml.org/docs/#table-notes) using the creation SQL script exposed by Room's exported schema.

This should be toggled as an optionally enabled command line argument since SQL can be quite verbose and would in anycase 'duplicate' what the UI rendering for the DBML potentially is.

Pull request description:
Fixes #5 

TODO:
- [x] Introduce command line argument to opt-in to the feature

Patch:
diff --git a/README.md b/README.md
index 2c45860..f79aeb5 100644
--- a/README.md
+++ b/README.md
@@ -19,11 +19,18 @@ After checking out this repository, make sure you have Gradle installed and run:
 gradlew run --args="<path-to-schema-file>"
 ```
 
-**Optionally**, one can also pipe the output of FloorPlan to a file in disk instead of the terminal, by specifying the `output` argument as such:
+**Optionally**, one can also:
+
+- pipe the output of FloorPlan to a file in disk instead of the terminal, by specifying the `output` argument as such:
 ```
 gradlew run --args="<path-to-schema-file> --output=<path-to-output-file>"
 ```
 
+- add the 'creation SQL' as a note to table representation. This option is disabled by default since it can be quite verbose and would 'duplicate' what a UI rendering tool (better) provides:
+```
+gradlew run --args="<path-to-schema-file> --creation-sql-as-table-note"
+```
+
 ### Tests
 
 The project is unit tested. These can be run via Gradle with:
diff --git a/src/main/kotlin/com/zynger/floorplan/App.kt b/src/main/kotlin/com/zynger/floorplan/App.kt
index 64893b2..47efd52 100644
--- a/src/main/kotlin/com/zynger/floorplan/App.kt
+++ b/src/main/kotlin/com/zynger/floorplan/App.kt
@@ -11,11 +11,12 @@ fun main(args: Array<String>) {
     val src = File(input.schemaPath)
     val json = Json(JsonConfiguration.Stable.copy(ignoreUnknownKeys = true, isLenient = true))
 
+    val settings = Settings(input.creationSqlAsTableNote)
     val dbml = json
         .parse(Schema.serializer(), src.readText())
         .database
         .entities
-        .map { Table(it) }
+        .map { Table(it, settings) }
         .joinToString(separator = "\n\n")
 
     if (input.outputPath == null) {
diff --git a/src/main/kotlin/com/zynger/floorplan/InputParser.kt b/src/main/kotlin/com/zynger/floorplan/InputParser.kt
index d75a8ef..8f95c54 100644
--- a/src/main/kotlin/com/zynger/floorplan/InputParser.kt
+++ b/src/main/kotlin/com/zynger/floorplan/InputParser.kt
@@ -6,38 +6,48 @@ object InputParser {
 
     data class Input(
         val schemaPath: String,
-        val outputPath: String?
+        val outputPath: String?,
+        val creationSqlAsTableNote: Boolean
     )
 
     private const val OUTPUT_ARG_KEY: String = "output"
+    private const val CREATION_SQL_AS_TABLE_NOTES_ARG_KEY: String = "creation-sql-as-table-note"
 
     fun parse(args: Array<String>): Input {
         require(args.isNotEmpty()) {
             """
             Pass the source Room JSON schema as an argument.
-            Optionally, specify an output file for the DBML representation.
+            Optionally, use:
             
-            e.g.: gradlew run --args="<path-to-schema-file> [--output=<output-file-path>]"
+            * output: specify an output file for the DBML representation.
+            * creation-sql-as-table-note: adds the SQL used to create tables as notes in their DBML representation.
+            
+            e.g.: gradlew run --args="<path-to-schema-file> [--output=<output-file-path>] [--creation-sql-as-table-note]"
         """.trimIndent()
         }
 
         val inputFilePath: String = sanitizeFilePath(args.first())
-        val outputFilePath: String? = if (args.containsOutputArg()) sanitizeFilePath(args.extractOutputArg()) else null
+        val outputFilePath: String? = args.getArgumentValue(OUTPUT_ARG_KEY)?.let { sanitizeFilePath(it) }
+        val noteCreationSql = args.argumentExists(CREATION_SQL_AS_TABLE_NOTES_ARG_KEY)
 
-        return Input(inputFilePath, outputFilePath)
+        return Input(inputFilePath, outputFilePath, noteCreationSql)
     }
 
-    private fun Array<String>.containsOutputArg(): Boolean {
-        return any { it.contains("--$OUTPUT_ARG_KEY=") }
-    }
+    private fun Array<String>.getArgumentValue(argKey: String): String? {
+        return if (none { it.contains("--$argKey=") }) {
+            null
+        } else {
+            val argumentPair = find { it.contains("--$argKey=") }!!
+            require(argumentPair.substringAfter("=").trim().isNotEmpty()) {
+                "Please inform a value for the --$argKey argument."
+            }
 
-    private fun Array<String>.extractOutputArg(): String {
-        val outputFileArgumentPair = find { it.contains("--$OUTPUT_ARG_KEY=") }!!
-        require(outputFileArgumentPair.substringAfter("=").trim().isNotEmpty()) {
-            "Please inform an output file path with the --output argument."
+            argumentPair.substringAfter("=").substringBefore(" ")
         }
+    }
 
-        return outputFileArgumentPair.substringAfter("=").substringBefore(" ")
+    private fun Array<String>.argumentExists(argKey: String): Boolean {
+        return any { it == "--$argKey" }
     }
 
     private fun sanitizeFilePath(path: String): String {
diff --git a/src/main/kotlin/com/zynger/floorplan/Settings.kt b/src/main/kotlin/com/zynger/floorplan/Settings.kt
new file mode 100644
index 0000000..6e75678
--- /dev/null
+++ b/src/main/kotlin/com/zynger/floorplan/Settings.kt
@@ -0,0 +1,5 @@
+package com.zynger.floorplan
+
+data class Settings(
+    val creationSqlAsTableNote: Boolean
+)
\ No newline at end of file
diff --git a/src/main/kotlin/com/zynger/floorplan/dbml/Table.kt b/src/main/kotlin/com/zynger/floorplan/dbml/Table.kt
index e637dc4..6442a41 100644
--- a/src/main/kotlin/com/zynger/floorplan/dbml/Table.kt
+++ b/src/main/kotlin/com/zynger/floorplan/dbml/Table.kt
@@ -1,14 +1,17 @@
 package com.zynger.floorplan.dbml
 
+import com.zynger.floorplan.Settings
 import com.zynger.floorplan.model.Entity
 
 class Table(
-    entity: Entity
+    entity: Entity,
+    private val settings: Settings
 ) {
     private val tableName: String = entity.tableName
     private val fields: List<Field> = entity.fields.map { Field(it, entity.primaryKey) }
     private val indices: List<Index> = entity.indices.map { Index(it) }
     private val references: List<Reference> = entity.foreignKeys.map { Reference(tableName, it) }
+    private val createSql: String = entity.createSql
 
     override fun toString(): String {
         return StringBuilder()
@@ -26,6 +29,16 @@ class Table(
                     appendIndicesBlock()
                 }
             }
+            .apply {
+                if (settings.creationSqlAsTableNote) {
+                    appendln("  ")
+                    append("Note: ".prependIndent("  "))
+                    append("'")
+                    append(createSql)
+                    append("'")
+                    appendln()
+                }
+            }
             .append("}")
             .apply {
                 if (this@Table.references.isNotEmpty()) {


Test patch:
diff --git a/src/test/kotlin/com/zynger/floorplan/InputParserTest.kt b/src/test/kotlin/com/zynger/floorplan/InputParserTest.kt
index 591fd67..21a9494 100644
--- a/src/test/kotlin/com/zynger/floorplan/InputParserTest.kt
+++ b/src/test/kotlin/com/zynger/floorplan/InputParserTest.kt
@@ -95,4 +95,27 @@ class InputParserTest {
 
         assertEquals(expected, input.outputPath)
     }
+
+    @Test
+    fun `creation sql is not part of table notes when argument isn't specified`() {
+        val input = InputParser.parse(
+            arrayOf(
+                "samples/db.json"
+            )
+        )
+
+        assertFalse(input.creationSqlAsTableNote)
+    }
+
+    @Test
+    fun `creation sql is part of table notes when argument is specified`() {
+        val input = InputParser.parse(
+            arrayOf(
+                "samples/db.json",
+                "--creation-sql-as-table-note"
+            )
+        )
+
+        assertTrue(input.creationSqlAsTableNote)
+    }
 }
diff --git a/src/test/kotlin/com/zynger/floorplan/dbml/TableTest.kt b/src/test/kotlin/com/zynger/floorplan/dbml/TableTest.kt
index a8d1548..9df3c0e 100644
--- a/src/test/kotlin/com/zynger/floorplan/dbml/TableTest.kt
+++ b/src/test/kotlin/com/zynger/floorplan/dbml/TableTest.kt
@@ -1,5 +1,6 @@
 package com.zynger.floorplan.dbml
 
+import com.zynger.floorplan.Settings
 import com.zynger.floorplan.model.*
 import org.junit.Assert.assertEquals
 import org.junit.Test
@@ -21,6 +22,8 @@ class TableTest {
 
         private val ON_DELETE = ForeignKeyAction.NO_ACTION
         private val ON_UPDATE = ForeignKeyAction.NO_ACTION
+
+        private val DEFAULT_SETTINGS = Settings(creationSqlAsTableNote = false)
     }
 
     @Test
@@ -33,13 +36,40 @@ class TableTest {
                 primaryKey = PrimaryKey(emptyList(), false),
                 indices = emptyList(),
                 foreignKeys = emptyList()
-            )
+            ),
+            DEFAULT_SETTINGS
+        )
+
+        assertEquals(
+            """
+                Table $TABLE_NAME {
+                  $COLUMN_NAME_1 varchar [note: 'not null']
+                }
+            """.trimIndent(),
+            table.toString()
+        )
+    }
+
+    @Test
+    fun `table with single field and creation SQL as note`() {
+        val table = Table(
+            Entity(
+                tableName = TABLE_NAME,
+                createSql = CREATE_SQL,
+                fields = listOf(FIELD_1),
+                primaryKey = PrimaryKey(emptyList(), false),
+                indices = emptyList(),
+                foreignKeys = emptyList()
+            ),
+            Settings(creationSqlAsTableNote = true)
         )
 
         assertEquals(
             """
                 Table $TABLE_NAME {
                   $COLUMN_NAME_1 varchar [note: 'not null']
+                  
+                  Note: '$CREATE_SQL'
                 }
             """.trimIndent(),
             table.toString()
@@ -56,7 +86,8 @@ class TableTest {
                 primaryKey = PrimaryKey(emptyList(), false),
                 indices = emptyList(),
                 foreignKeys = emptyList()
-            )
+            ),
+            DEFAULT_SETTINGS
         )
 
         assertEquals(
@@ -83,7 +114,8 @@ class TableTest {
                     Index(INDEX_1_NAME, true, listOf(COLUMN_NAME_1), CREATE_SQL)
                 ),
                 foreignKeys = emptyList()
-            )
+            ),
+            DEFAULT_SETTINGS
         )
 
         assertEquals(
@@ -113,7 +145,40 @@ class TableTest {
                     Index(INDEX_2_NAME, false, listOf(COLUMN_NAME_2, COLUMN_NAME_3), CREATE_SQL)
                 ),
                 foreignKeys = emptyList()
-            )
+            ),
+            DEFAULT_SETTINGS
+        )
+
+        assertEquals(
+            """
+                Table $TABLE_NAME {
+                  $COLUMN_NAME_1 varchar [note: 'not null']
+                
+                  Indexes  {
+                    ($COLUMN_NAME_1) [name:'$INDEX_1_NAME', unique]
+                    ($COLUMN_NAME_2,$COLUMN_NAME_3) [name:'$INDEX_2_NAME']
+                  }
+                }
+            """.trimIndent(),
+            table.toString()
+        )
+    }
+
+    @Test
+    fun `table with multiple indices and creation sql as note`() {
+        val table = Table(
+            Entity(
+                tableName = TABLE_NAME,
+                createSql = CREATE_SQL,
+                fields = listOf(FIELD_1),
+                primaryKey = PrimaryKey(emptyList(), false),
+                indices = listOf(
+                    Index(INDEX_1_NAME, true, listOf(COLUMN_NAME_1), CREATE_SQL),
+                    Index(INDEX_2_NAME, false, listOf(COLUMN_NAME_2, COLUMN_NAME_3), CREATE_SQL)
+                ),
+                foreignKeys = emptyList()
+            ),
+            Settings(creationSqlAsTableNote = true)
         )
 
         assertEquals(
@@ -125,6 +190,8 @@ class TableTest {
                     ($COLUMN_NAME_1) [name:'$INDEX_1_NAME', unique]
                     ($COLUMN_NAME_2,$COLUMN_NAME_3) [name:'$INDEX_2_NAME']
                   }
+                  
+                  Note: '$CREATE_SQL'
                 }
             """.trimIndent(),
             table.toString()
@@ -143,7 +210,8 @@ class TableTest {
                 foreignKeys = listOf(
                     ForeignKey(TABLE_NAME_2, listOf(COLUMN_NAME_1), listOf(COLUMN_NAME_3), ON_DELETE, ON_UPDATE)
                 )
-            )
+            ),
+            DEFAULT_SETTINGS
         )
 
         assertEquals(
@@ -171,7 +239,8 @@ class TableTest {
                     ForeignKey(TABLE_NAME_2, listOf(COLUMN_NAME_3), listOf(COLUMN_NAME_1), ON_DELETE, ON_UPDATE),
                     ForeignKey(TABLE_NAME_3, listOf(COLUMN_NAME_1), listOf(COLUMN_NAME_2), ON_DELETE, ON_UPDATE)
                 )
-            )
+            ),
+            DEFAULT_SETTINGS
         )
 
         assertEquals(
@@ -203,7 +272,48 @@ class TableTest {
                     ForeignKey(TABLE_NAME_2, listOf(COLUMN_NAME_3), listOf(COLUMN_NAME_1), ON_DELETE, ON_UPDATE),
                     ForeignKey(TABLE_NAME_3, listOf(COLUMN_NAME_1), listOf(COLUMN_NAME_2), ON_DELETE, ON_UPDATE)
                 )
-            )
+            ),
+            DEFAULT_SETTINGS
+        )
+
+        assertEquals(
+            """
+                Table Users {
+                  userId varchar [note: 'not null']
+                  age int [note: 'nullable']
+                  username varchar [note: 'not null']
+                
+                  Indexes  {
+                    (userId) [name:'index1', unique]
+                    (age,username) [name:'index2']
+                  }
+                }
+                
+                Ref: Users.username - Songs.userId [delete: no action, update: no action]
+                Ref: Users.userId - Albums.age [delete: no action, update: no action]
+            """.trimIndent(),
+            table.toString()
+        )
+    }
+
+    @Test
+    fun `table with multiple fields, indices, references and creation sql as note`() {
+        val table = Table(
+            Entity(
+                tableName = TABLE_NAME,
+                createSql = CREATE_SQL,
+                fields = listOf(FIELD_1, FIELD_2, FIELD_3),
+                primaryKey = PrimaryKey(emptyList(), false),
+                indices = listOf(
+                    Index(INDEX_1_NAME, true, listOf(COLUMN_NAME_1), CREATE_SQL),
+                    Index(INDEX_2_NAME, false, listOf(COLUMN_NAME_2, COLUMN_NAME_3), CREATE_SQL)
+                ),
+                foreignKeys = listOf(
+                    ForeignKey(TABLE_NAME_2, listOf(COLUMN_NAME_3), listOf(COLUMN_NAME_1), ON_DELETE, ON_UPDATE),
+                    ForeignKey(TABLE_NAME_3, listOf(COLUMN_NAME_1), listOf(COLUMN_NAME_2), ON_DELETE, ON_UPDATE)
+                )
+            ),
+            Settings(creationSqlAsTableNote = true)
         )
 
         assertEquals(
@@ -217,6 +327,8 @@ class TableTest {
                     (userId) [name:'index1', unique]
                     (age,username) [name:'index2']
                   }
+                  
+                  Note: '$CREATE_SQL'
                 }
                 
                 Ref: Users.username - Songs.userId [delete: no action, update: no action]