Repository: julioz/FloorPlan (kotlin)

Issue:
Gradle Plugin: Preserve directory structure
Output files stored within directories and preserve structure
  - For example, `<input_dir>/com.mypackage.mydatabase/1.json` should end up as `<output_dir>/com.mypackage.mydatabase/1.dbml`

Pull request description:
Fixes https://github.com/julioz/FloorPlan/issues/33

Room's directory structure when generating DB schemas adds the database definition package name and database name to a parent directory to prevent name clashes and conflicts.

Here we move the schemas from our sample android application to respect Room's directory structure, and fix the issue that output representations are generated only on the root directory (defined by `outputLocation`) gradle property.

So, we now preserve the directory structure and mimic it when generating output files.

In other words, for a schema located in
```
$schemaLocation/com.mypackage.name.MyDatabase
```
the output files will be generated in
```
$outputLocation/com.mypackage.name.MyDatabase
```

This PR also pulls out the logic into a collaborator class to allow for unit testing of the gradle `Task` logic.

Patch:
diff --git a/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/FloorPlanTask.kt b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/FloorPlanTask.kt
index c69d97b..35fba55 100644
--- a/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/FloorPlanTask.kt
+++ b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/FloorPlanTask.kt
@@ -1,11 +1,7 @@
 package com.zynger.floorplan.gradle
 
-import com.zynger.floorplan.Destination
-import com.zynger.floorplan.FloorPlan
-import com.zynger.floorplan.Format
-import com.zynger.floorplan.Output
+import com.zynger.floorplan.*
 import com.zynger.floorplan.dbml.Project
-import com.zynger.floorplan.DbmlConfiguration
 import com.zynger.floorplan.gradle.model.OutputFormat
 import com.zynger.floorplan.room.RoomConsumer
 import org.gradle.api.DefaultTask
@@ -28,9 +24,9 @@ open class FloorPlanTask : DefaultTask() {
         val schemas: List<File> = schemaLocation.findRoomSchemas()
         schemas.forEach { schema ->
             val project: Project = RoomConsumer.read(schema)
-            val outputFormat = outputFormat.mapOutputFormat()
-            val outputFileName = "${schema.nameWithoutExtension}.${outputFormat.extension}"
-            val outputFile = File(outputLocation, outputFileName)
+            val outputHandler = OutputParameterHandler(schema, schemaLocation)
+            val outputFormat: Format = outputHandler.format(outputFormat)
+            val outputFile: File = outputHandler.file(outputLocation, outputFormat)
 
             FloorPlan.render(
                 project = project,
@@ -42,18 +38,4 @@ open class FloorPlanTask : DefaultTask() {
     private fun File.findRoomSchemas(): List<File> {
         return walk().filter { it.extension == "json" }.toList()
     }
-
-    private fun OutputFormat.mapOutputFormat(): Format {
-        return when (this) {
-            is OutputFormat.DBML -> Format.DBML(
-                DbmlConfiguration(
-                    this.config.creationSqlAsTableNote,
-                    this.config.renderNullableFields
-                )
-            )
-            OutputFormat.DOT -> Format.DOT
-            OutputFormat.SVG -> Format.SVG
-            OutputFormat.PNG -> Format.PNG
-        }
-    }
 }
diff --git a/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/OutputParameterHandler.kt b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/OutputParameterHandler.kt
new file mode 100644
index 0000000..f0df834
--- /dev/null
+++ b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/OutputParameterHandler.kt
@@ -0,0 +1,48 @@
+package com.zynger.floorplan.gradle
+
+import com.zynger.floorplan.DbmlConfiguration
+import com.zynger.floorplan.Format
+import com.zynger.floorplan.gradle.model.OutputFormat
+import java.io.File
+
+class OutputParameterHandler(
+    private val schema: File,
+    private val schemaLocation: File
+) {
+
+    fun format(outputFormat: OutputFormat): Format {
+        return outputFormat.mapOutputFormat()
+    }
+
+    private fun OutputFormat.mapOutputFormat(): Format {
+        return when (this) {
+            is OutputFormat.DBML -> Format.DBML(
+                DbmlConfiguration(
+                    this.config.creationSqlAsTableNote,
+                    this.config.renderNullableFields
+                )
+            )
+            OutputFormat.DOT -> Format.DOT
+            OutputFormat.SVG -> Format.SVG
+            OutputFormat.PNG -> Format.PNG
+        }
+    }
+
+    fun file(outputLocation: File, outputFormat: Format): File {
+        val outputFileName = "${schema.nameWithoutExtension}.${outputFormat.extension}"
+        return File(getOutputFileParentDirectory(outputLocation), outputFileName)
+    }
+
+    private fun getOutputFileParentDirectory(outputLocation: File): File {
+        val relativizedPathToSchemaInput = schemaLocation.toURI().relativize(schema.toURI()).path
+        return if (relativizedPathToSchemaInput.contains(File.separator)) {
+            // schema is stored under a sub-directory,
+            // we must relativize the directory structure to point to the parent dir.
+            val relativizedPath = relativizedPathToSchemaInput.substringBeforeLast(File.separator)
+            File(outputLocation.path + File.separator + relativizedPath)
+        } else {
+            // schema is directly under [outputLocation], so the parent is [outputLocation] itself.
+            outputLocation
+        }
+    }
+}
diff --git a/sample-android-project/schemas/1.json b/sample-android-project/schemas/com.zynger.floorplan.gradle.sample.MyRoomDatabase/1.json
similarity index 100%
rename from sample-android-project/schemas/1.json
rename to sample-android-project/schemas/com.zynger.floorplan.gradle.sample.MyRoomDatabase/1.json
diff --git a/sample-android-project/schemas/2.json b/sample-android-project/schemas/com.zynger.floorplan.gradle.sample.MyRoomDatabase/2.json
similarity index 100%
rename from sample-android-project/schemas/2.json
rename to sample-android-project/schemas/com.zynger.floorplan.gradle.sample.MyRoomDatabase/2.json


Test patch:
diff --git a/floorplan-gradle-plugin/src/test/kotlin/com/zynger/floorplan/gradle/OutputParameterHandlerTest.kt b/floorplan-gradle-plugin/src/test/kotlin/com/zynger/floorplan/gradle/OutputParameterHandlerTest.kt
new file mode 100644
index 0000000..b18afb6
--- /dev/null
+++ b/floorplan-gradle-plugin/src/test/kotlin/com/zynger/floorplan/gradle/OutputParameterHandlerTest.kt
@@ -0,0 +1,97 @@
+package com.zynger.floorplan.gradle
+
+import com.google.common.truth.Truth.*
+import com.zynger.floorplan.DbmlConfiguration
+import com.zynger.floorplan.Format
+import com.zynger.floorplan.gradle.model.OutputFormat
+import org.junit.Test
+import java.io.File
+import com.zynger.floorplan.gradle.model.DbmlConfiguration as GradlePluginDbmlConfiguration
+
+class OutputParameterHandlerTest {
+
+    @Test
+    fun `dbml gets mapped to output format`() {
+        val outputParameterHandler = OutputParameterHandler(SCHEMA, SCHEMA_LOCATION)
+
+        val format = outputParameterHandler.format(OutputFormat.DBML(GradlePluginDbmlConfiguration()))
+
+        assertThat(format).isEqualTo(Format.DBML(DbmlConfiguration()))
+    }
+
+    @Test
+    fun `svg gets mapped to output format`() {
+        val outputParameterHandler = OutputParameterHandler(SCHEMA, SCHEMA_LOCATION)
+
+        val format = outputParameterHandler.format(OutputFormat.SVG)
+
+        assertThat(format).isEqualTo(Format.SVG)
+    }
+
+    @Test
+    fun `png gets mapped to output format`() {
+        val outputParameterHandler = OutputParameterHandler(SCHEMA, SCHEMA_LOCATION)
+
+        val format = outputParameterHandler.format(OutputFormat.PNG)
+
+        assertThat(format).isEqualTo(Format.PNG)
+    }
+
+    @Test
+    fun `dot gets mapped to output format`() {
+        val outputParameterHandler = OutputParameterHandler(SCHEMA, SCHEMA_LOCATION)
+
+        val format = outputParameterHandler.format(OutputFormat.DOT)
+
+        assertThat(format).isEqualTo(Format.DOT)
+    }
+
+    @Test
+    fun `output file name uses output format extension`() {
+        val outputParameterHandler = OutputParameterHandler(SCHEMA, SCHEMA_LOCATION)
+
+        val outputFile = outputParameterHandler.file(OUTPUT_LOCATION, Format.SVG)
+
+        assertThat(outputFile.extension).isEqualTo(Format.SVG.extension)
+    }
+
+    @Test
+    fun `output file is stored in output location when there are no subdirectories`() {
+        val schema = File(SCHEMA_LOCATION, SCHEMA_FILE_NAME)
+        val outputParameterHandler = OutputParameterHandler(schema, SCHEMA_LOCATION)
+
+        val outputFile = outputParameterHandler.file(OUTPUT_LOCATION, Format.SVG)
+
+        val expected = File(OUTPUT_LOCATION, "1.svg")
+        assertThat(outputFile.path).isEqualTo(expected.path)
+    }
+
+    @Test
+    fun `output file is stored in output sub-directory location when there is a subdirectories`() {
+        val schema = File("$SCHEMA_LOCATION${File.separator}subdirectory", SCHEMA_FILE_NAME)
+        val outputParameterHandler = OutputParameterHandler(schema, SCHEMA_LOCATION)
+
+        val outputFile = outputParameterHandler.file(OUTPUT_LOCATION, Format.SVG)
+
+        val expected = File("$OUTPUT_LOCATION${File.separator}subdirectory", "1.svg")
+        assertThat(outputFile.path).isEqualTo(expected.path)
+    }
+
+    @Test
+    fun `output file is stored in output sub-directory location when there are multiple subdirectories`() {
+        val schema = File("$SCHEMA_LOCATION${File.separator}subdirectory${File.separator}sub-dir2", SCHEMA_FILE_NAME)
+        val outputParameterHandler = OutputParameterHandler(schema, SCHEMA_LOCATION)
+
+        val outputFile = outputParameterHandler.file(OUTPUT_LOCATION, Format.SVG)
+
+        val expected = File("$OUTPUT_LOCATION${File.separator}subdirectory${File.separator}sub-dir2", "1.svg")
+        assertThat(outputFile.path).isEqualTo(expected.path)
+    }
+
+    private companion object {
+        private const val SCHEMA_FILE_NAME = "1.json"
+        private val SCHEMA = File(SCHEMA_FILE_NAME)
+        private val SCHEMA_LOCATION = File("/sample-android-project/schemas")
+        private val OUTPUT_LOCATION = File("/sample-android-project/outputs")
+    }
+}