diff --git a/app/resources/init/index.d.ts b/app/resources/init/index.d.ts index b7fcb1f..7df448a 100644 --- a/app/resources/init/index.d.ts +++ b/app/resources/init/index.d.ts @@ -5,42 +5,39 @@ */ declare function echo(message: string): void -/** - * Prompt a user for text input - * - * @param text - The text to display for the prompt - * @param def - A default value - */ -declare function prompt(text: string, def?: string): string +declare interface prompt { -/** - * Prompt a user for text input - * - * @param text - The text to display for the prompt - * @param def - A default value - */ -declare function promptBoolean(text: string, def?: boolean): boolean + /** + * Prompt a user for text input + * + * @param text - The text to display for the prompt + * @param def - A default value + */ + string(text: string, def?: string): string + + /** + * Prompt a user for text input + * + * @param text - The text to display for the prompt + * @param def - A default value + */ + boolean(text: string, def?: boolean): boolean + +} /** * Renders a template * * @param name - The template name * @param context - The template context + * @param output - The output path, if absent, default to the template name */ -declare function render(name: string, context: any): string - -/** - * Write a string to a file - * - * @param input - The content of the file - * @param output - The output path - */ -declare function write(input: string, output: string): void +declare function render(name: string, context: any, output?: string): string /** * Copy a file * * @param input - The input path - * @param output - The output path + * @param output - The output path, if absent, default to the input path */ -declare function copy(input: string, output: string): void \ No newline at end of file +declare function copy(input: string, output?: string): void \ No newline at end of file diff --git a/app/resources/init/index.js b/app/resources/init/index.js index 80762e6..d712a90 100644 --- a/app/resources/init/index.js +++ b/app/resources/init/index.js @@ -1,7 +1,7 @@ echo("Example") -const name = prompt("Project name") +const name = prompt.string("Project name") const ctx = {name: name} -write(render("README.md", ctx), "README.md") \ No newline at end of file +render("README.md", ctx) \ No newline at end of file diff --git a/app/src/Prompt.kt b/app/src/Prompt.kt new file mode 100644 index 0000000..6eab1d3 --- /dev/null +++ b/app/src/Prompt.kt @@ -0,0 +1,37 @@ +package scaffold + +import com.github.ajalt.clikt.core.UsageError +import com.github.ajalt.clikt.output.CliktConsole +import com.github.ajalt.clikt.output.TermUi +import com.github.ajalt.clikt.output.defaultCliktConsole + +class Prompt(private val console: CliktConsole = defaultCliktConsole()) { + + fun string(text: String, default: String?): String = TermUi.prompt( + text = text, + default = default, + console = console + ) { it }!! + + fun boolean(text: String, default: Boolean?): Boolean { + val (defaultString, suffix) = when (default) { + true -> "y" to "[Y/n]" + false -> "n" to "[y/N]" + null -> null to "[y/n]" + } + + return TermUi.prompt( + "$text $suffix", + default = defaultString, + showDefault = false, + console = console + ) { + when (it.toLowerCase()) { + "y" -> true + "n" -> false + else -> throw UsageError("Can only be [y/n]") + } + }!! + } + +} \ No newline at end of file diff --git a/app/src/commands/GenerateCommand.kt b/app/src/commands/GenerateCommand.kt index 2fa2856..2eda4de 100644 --- a/app/src/commands/GenerateCommand.kt +++ b/app/src/commands/GenerateCommand.kt @@ -3,7 +3,6 @@ package scaffold.commands import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.ProgramResult import com.github.ajalt.clikt.core.UsageError -import com.github.ajalt.clikt.output.TermUi import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.options.convert import com.github.ajalt.clikt.parameters.options.option @@ -13,6 +12,7 @@ import com.mitchellbosecke.pebble.loader.FileLoader import com.mitchellbosecke.pebble.template.PebbleTemplate import scaffold.Generator import scaffold.Generators +import scaffold.Prompt import scaffold.scripting.ScriptContext import scaffold.scripting.ScriptEngine import java.io.StringWriter @@ -37,49 +37,18 @@ class GenerateCommand(private val generators: Generators) : CliktCommand("genera val scriptContext = object : ScriptContext { override fun echo(message: String) = this@GenerateCommand.echo(message) - override fun prompt(text: String, default: String?) = this@GenerateCommand.prompt(text, default)!! + override val prompt = Prompt() - override fun promptBoolean(text: String, default: Boolean?): Boolean { - val suffix = when (default) { - true -> "[Y/n]" - false -> "[y/N]" - null -> "[y/n]" - } - - val defaultAsString = when (default) { - true -> "y" - false -> "n" - null -> null - } - - return this@GenerateCommand.prompt( - "$text $suffix", - default = defaultAsString, - showDefault = false - ) { - when (it.toLowerCase()) { - "y" -> true - "n" -> false - else -> throw UsageError("Can only be [y/n]") - } - }!! - } - - override fun promptInt(text: String, default: Int?): Int { - TODO("Not yet implemented") - } - - override fun render(name: String, ctx: Map) = pebble.getTemplate(name)(ctx) - - override fun write(content: String, output: String) { - val outputPath = outputPathRoot.resolve(output) + override fun render(template: String, ctx: Map, output: String?) { + val renderedTemplate = pebble.getTemplate(template)(ctx) + val outputPath = output?.let { outputPathRoot.resolve(it) } ?: outputPathRoot.resolve(template) Files.createDirectories(outputPath.parent) - Files.writeString(outputPath, content) + Files.writeString(outputPath, renderedTemplate) } - override fun copy(input: String, output: String) { + override fun copy(input: String, output: String?) { val inputPath = generator.treeRoot.resolve(input) - val outputPath = outputPathRoot.resolve(output) + val outputPath = output?.let { outputPathRoot.resolve(it) } ?: outputPathRoot.resolve(input) Files.createDirectories(outputPath.parent) Files.copy(inputPath, outputPath) } diff --git a/app/src/scripting/PromptProxyAdapter.kt b/app/src/scripting/PromptProxyAdapter.kt new file mode 100644 index 0000000..8b92238 --- /dev/null +++ b/app/src/scripting/PromptProxyAdapter.kt @@ -0,0 +1,26 @@ +package scaffold.scripting + +import org.graalvm.polyglot.Value +import org.graalvm.polyglot.proxy.ProxyExecutable +import org.graalvm.polyglot.proxy.ProxyObject +import scaffold.Prompt + +class PromptProxyAdapter(private val prompt: Prompt) : ProxyObject { + + override fun getMember(key: String?) = when (key) { + "string" -> ProxyExecutable { args -> + prompt.string(args[0].asString(), args.getOrNull(1)?.asString()) + } + "boolean" -> ProxyExecutable { args -> + prompt.boolean(args[0].asString(), args.getOrNull(1)?.asBoolean()) + } + else -> throw UnsupportedOperationException() + } + + override fun getMemberKeys() = arrayOf("string", "boolean") + + override fun hasMember(key: String?): Boolean = key in memberKeys + + override fun putMember(key: String?, value: Value?) = throw UnsupportedOperationException() + +} \ No newline at end of file diff --git a/app/src/scripting/ScriptContext.kt b/app/src/scripting/ScriptContext.kt index 3be343e..55da55b 100644 --- a/app/src/scripting/ScriptContext.kt +++ b/app/src/scripting/ScriptContext.kt @@ -1,14 +1,13 @@ package scaffold.scripting +import scaffold.Prompt + interface ScriptContext { fun echo(message: String) - fun prompt(text: String, default: String?): String - fun promptBoolean(text: String, default: Boolean?): Boolean - fun promptInt(text: String, default: Int?): Int + val prompt: Prompt - fun render(name: String, ctx: Map): String + fun render(template: String, ctx: Map, output: String?) - fun write(content: String, output: String) - fun copy(input: String, output: String) + fun copy(input: String, output: String?) } diff --git a/app/src/scripting/ScriptEngine.kt b/app/src/scripting/ScriptEngine.kt index b0ca168..d87bd21 100644 --- a/app/src/scripting/ScriptEngine.kt +++ b/app/src/scripting/ScriptEngine.kt @@ -13,34 +13,24 @@ class ScriptEngine(private val scriptContext: ScriptContext) { init { with(context.getBindings("js")) { - putMember("echo", ProxyExecutable { args -> - scriptContext.echo(args[0].asString()) + putMember("echo", ProxyExecutable { (message) -> + scriptContext.echo(message.asString()) null }) - putMember("prompt", ProxyExecutable { args -> - scriptContext.prompt(args[0].asString(), args.getOrNull(1)?.asString()) - }) - - putMember("promptBoolean", ProxyExecutable { args -> - scriptContext.promptBoolean(args[0].asString(), args.getOrNull(1)?.asBoolean()) - }) - - putMember("promptInt", ProxyExecutable { args -> - scriptContext.promptInt(args[0].asString(), args.getOrNull(1)?.asInt()) - }) + putMember("prompt", PromptProxyAdapter(scriptContext.prompt)) putMember("render", ProxyExecutable { args -> @Suppress("UNCHECKED_CAST") - scriptContext.render(args[0].asString(), args[1].`as`(Map::class.java) as Map) + scriptContext.render( + args[0].asString(), + args[1].`as`(Map::class.java) as Map, + args.getOrNull(2)?.asString() + ) }) - putMember("write", ProxyExecutable { (input, output) -> - scriptContext.write(input.asString(), output.asString()) - }) - - putMember("copy", ProxyExecutable { (input, output) -> - scriptContext.copy(input.asString(), output.asString()) + putMember("copy", ProxyExecutable { args -> + scriptContext.copy(args[0].asString(), args.getOrNull(1)?.asString()) }) } } diff --git a/app/test/PromptTest.kt b/app/test/PromptTest.kt new file mode 100644 index 0000000..6c7a28e --- /dev/null +++ b/app/test/PromptTest.kt @@ -0,0 +1,60 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package scaffold + +import com.github.ajalt.clikt.output.CliktConsole +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import strikt.api.expectThat +import strikt.assertions.isEqualTo + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PromptTest { + + val console = mockk() + val prompt = Prompt(console) + + @BeforeEach + fun beforeEach() = clearMocks(console) + + @CsvSource( + value = [ + "null, answer, answer", + "def, answer, answer", + "def, '', def" + ] + ) + @ParameterizedTest(name = "prompt string({argumentsWithNames})") + fun `prompt string`(default: String?, answer: String, expectedValue: String) { + every { console.promptForLine(any(), any()) } returns answer + expectThat(prompt.string("test", default)).isEqualTo(expectedValue) + } + + @CsvSource( + value = [ + "null, y, true", + "null, n, false", + + "true, '', true", + "true, y, true", + "true, n, false", + + "false, '', false", + "false, y, true", + "false, n, false", + ] + ) + @ParameterizedTest(name = "prompt boolean({argumentsWithNames})") + fun `prompt boolean`(default: Boolean?, answer: String, expectedValue: Boolean) { + every { console.promptForLine(any(), any()) } returns answer + expectThat(prompt.boolean("test", default)).isEqualTo(expectedValue) + } + +} \ No newline at end of file diff --git a/app/test/scripting/ScriptEngineTest.kt b/app/test/scripting/ScriptEngineTest.kt index 0010454..a767d4f 100644 --- a/app/test/scripting/ScriptEngineTest.kt +++ b/app/test/scripting/ScriptEngineTest.kt @@ -1,4 +1,4 @@ -@file:Suppress("MemberVisibilityCanBePrivate", "PackageDirectoryMismatch") +@file:Suppress("MemberVisibilityCanBePrivate") package scaffold.scripting @@ -10,11 +10,15 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle +import scaffold.Prompt @TestInstance(Lifecycle.PER_CLASS) class ScriptEngineTest { - val scriptContext = mockk() + val scriptContext = mockk().also { + every { it.prompt } returns Prompt() + } + val scriptEngine = ScriptEngine(scriptContext) @BeforeEach @@ -32,7 +36,7 @@ class ScriptEngineTest { @Test fun render() { - every { scriptContext.render("test", match { it["something"] == "hello" }) } returns "blah" + every { scriptContext.render("test", match { it["something"] == "hello" }, null) } returns Unit val script = """ const ctx = { @@ -44,27 +48,7 @@ class ScriptEngineTest { scriptEngine.eval(script) - verify { scriptContext.render(any(), any()) } - } - - @Test - fun prompt() { - every { scriptContext.prompt("a", null) } returns "a" - - val script = "prompt('a')" - scriptEngine.eval(script) - - verify { scriptContext.prompt(any(), null) } - } - - @Test - fun promptWithDefault(){ - every { scriptContext.prompt("a", "default") } returns "default" - - val script = "prompt('a', 'default')" - scriptEngine.eval(script) - - verify { scriptContext.prompt(any(), any()) } + verify { scriptContext.render(any(), any(), any()) } } }