Repository: julioz/FloorPlan (kotlin)

Issue:
Write output to file
Instead of printing the conversion output, passing an output file path argument to the program will write the DBML result to disk.

Pull request description:
Fixes #6

Patch:
diff --git a/README.md b/README.md
index 917b74a..2c45860 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,12 @@ For instance, when translating [Tivi](https://github.com/chrisbanes/tivi)'s [sch
 
 After checking out this repository, make sure you have Gradle installed and run:
 ```
-gradlew run --args=<path-to-schema-file>
+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:
+```
+gradlew run --args="<path-to-schema-file> --output=<path-to-output-file>"
 ```
 
 ### Tests
diff --git a/src/main/kotlin/com/zynger/floorplan/App.kt b/src/main/kotlin/com/zynger/floorplan/App.kt
index 9ecd780..64893b2 100644
--- a/src/main/kotlin/com/zynger/floorplan/App.kt
+++ b/src/main/kotlin/com/zynger/floorplan/App.kt
@@ -7,15 +7,7 @@ import kotlinx.serialization.json.JsonConfiguration
 import java.io.File
 
 fun main(args: Array<String>) {
-    require(args.isNotEmpty()) {
-        """
-            Pass the source Room JSON schema as an argument.
-            
-            e.g.: gradlew run --args=<path-to-schema-file>
-        """.trimIndent()
-    }
-
-    val input = InputParser.parse(args.first())
+    val input = InputParser.parse(args)
     val src = File(input.schemaPath)
     val json = Json(JsonConfiguration.Stable.copy(ignoreUnknownKeys = true, isLenient = true))
 
@@ -25,5 +17,12 @@ fun main(args: Array<String>) {
         .entities
         .map { Table(it) }
         .joinToString(separator = "\n\n")
-    print(dbml)
+
+    if (input.outputPath == null) {
+        print(dbml)
+    } else {
+        val outputFile = File(input.outputPath)
+        outputFile.parentFile.mkdirs()
+        outputFile.writeText(dbml)
+    }
 }
diff --git a/src/main/kotlin/com/zynger/floorplan/InputParser.kt b/src/main/kotlin/com/zynger/floorplan/InputParser.kt
index 3e230ac..d75a8ef 100644
--- a/src/main/kotlin/com/zynger/floorplan/InputParser.kt
+++ b/src/main/kotlin/com/zynger/floorplan/InputParser.kt
@@ -4,22 +4,52 @@ import java.io.File
 
 object InputParser {
 
-    data class Input(val schemaPath: String)
+    data class Input(
+        val schemaPath: String,
+        val outputPath: String?
+    )
 
-    fun parse(inputFilePath: String): Input {
-        return Input(sanitizeInputFilePath(inputFilePath))
+    private const val OUTPUT_ARG_KEY: String = "output"
+
+    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.
+            
+            e.g.: gradlew run --args="<path-to-schema-file> [--output=<output-file-path>]"
+        """.trimIndent()
+        }
+
+        val inputFilePath: String = sanitizeFilePath(args.first())
+        val outputFilePath: String? = if (args.containsOutputArg()) sanitizeFilePath(args.extractOutputArg()) else null
+
+        return Input(inputFilePath, outputFilePath)
+    }
+
+    private fun Array<String>.containsOutputArg(): Boolean {
+        return any { it.contains("--$OUTPUT_ARG_KEY=") }
+    }
+
+    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."
+        }
+
+        return outputFileArgumentPair.substringAfter("=").substringBefore(" ")
     }
 
-    private fun sanitizeInputFilePath(inputFilePath: String): String {
+    private fun sanitizeFilePath(path: String): String {
         return when {
-            inputFilePath.startsWith("~" + File.separator) -> System.getProperty("user.home") + inputFilePath.substring(1)
-            inputFilePath.startsWith("~") -> {
+            path.startsWith("~" + File.separator) -> System.getProperty("user.home") + path.substring(1)
+            path.startsWith("~") -> {
                 // Don't support explicit username relative paths as '~otheruser/Documents'
                 // https://stackoverflow.com/a/7163446
                 throw UnsupportedOperationException("Home directory expansion is not supported for explicit user names.\n" +
-                        "Provide an absolute path to your input schema file.")
+                        "Provide an absolute path to your file.")
             }
-            else -> inputFilePath
+            else -> path
         }
     }
 }


Test patch:
diff --git a/src/test/kotlin/com/zynger/floorplan/InputParserTest.kt b/src/test/kotlin/com/zynger/floorplan/InputParserTest.kt
index 829192d..591fd67 100644
--- a/src/test/kotlin/com/zynger/floorplan/InputParserTest.kt
+++ b/src/test/kotlin/com/zynger/floorplan/InputParserTest.kt
@@ -1,8 +1,8 @@
 package com.zynger.floorplan
 
-import org.junit.Assert.assertEquals
-import org.junit.Assert.fail
+import org.junit.Assert.*
 import org.junit.Test
+import java.lang.IllegalArgumentException
 import java.lang.UnsupportedOperationException
 
 
@@ -12,7 +12,7 @@ class InputParserTest {
     fun `parses absolute file paths`() {
         val path = "samples/db.json"
 
-        val input = InputParser.parse(path)
+        val input = InputParser.parse(arrayOf(path))
 
         assertEquals(path, input.schemaPath)
     }
@@ -22,7 +22,7 @@ class InputParserTest {
         val path = "~/foo/bar/db.json"
         val expected = System.getProperty("user.home") + "/foo/bar/db.json"
 
-        val input = InputParser.parse(path)
+        val input = InputParser.parse(arrayOf(path))
 
         assertEquals(expected, input.schemaPath)
     }
@@ -32,10 +32,67 @@ class InputParserTest {
         val path = "~otheruser/Documents/foo/bar/db.json"
 
         try {
-            InputParser.parse(path)
+            InputParser.parse(arrayOf(path))
             fail()
         } catch(e: UnsupportedOperationException) {
             // ok
         }
     }
+
+    @Test
+    fun `does not pick up output path argument if not specified`() {
+        val path = "samples/db.json"
+
+        val input = InputParser.parse(arrayOf(path))
+
+        assertNull(input.outputPath)
+    }
+
+    @Test
+    fun `throws when output argument is specified with no value`() {
+        val args = "samples/db.json --output="
+            .split(" ")
+            .toTypedArray()
+
+        try {
+            InputParser.parse(args)
+            fail()
+        } catch (e: IllegalArgumentException) {
+            // ok
+        }
+    }
+
+    @Test
+    fun `parses absolute file output paths`() {
+        val args = "samples/db.json --output=result/mydb.dbml"
+            .split(" ")
+            .toTypedArray()
+
+        val input = InputParser.parse(args)
+
+        assertEquals("result/mydb.dbml", input.outputPath)
+    }
+
+    @Test
+    fun `parses absolute file output paths by ignoring following arguments`() {
+        val args = "samples/db.json --output=result/mydb.dbml --something-else"
+            .split(" ")
+            .toTypedArray()
+
+        val input = InputParser.parse(args)
+
+        assertEquals("result/mydb.dbml", input.outputPath)
+    }
+
+    @Test
+    fun `parses relative file output paths`() {
+        val args = "samples/db.json --output=~/foo/bar/result/mydb.dbml"
+            .split(" ")
+            .toTypedArray()
+        val expected = System.getProperty("user.home") + "/foo/bar/result/mydb.dbml"
+
+        val input = InputParser.parse(args)
+
+        assertEquals(expected, input.outputPath)
+    }
 }