Skip to content

Commit

Permalink
Split initialize and configure into two separate sub-commands
Browse files Browse the repository at this point in the history
  • Loading branch information
martinvisser committed Sep 11, 2023
1 parent baad546 commit bc21ee5
Show file tree
Hide file tree
Showing 29 changed files with 540 additions and 327 deletions.
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,21 +171,19 @@ class MySubCommandTest {
```

#### Test Setup for Entry Command
On top of what is written above, when you are testing the implementation of your entry command, you have to provide a custom picocli `IFacotry`, as the `PluginInitializeCommand` cannot be created automatically.
On top of what is written above, when you are testing the implementation of your entry command, you have to provide a custom picocli `IFactory`, as the `PluginInitializeCommand` cannot be created automatically.
To provide a simple mock, you can do the following:
```kotlin
// ...
private val commandLine = CommandLine(MyPluginEntryCommand(), CustomInitializationFactory())
// ...

class CustomInitializationFactory : IFactory {
private val pluginInitializeCommand: PluginInitializeCommand = mock()
override fun <K : Any?> create(cls: Class<K>?): K {
return if (cls?.isInstance(pluginInitializeCommand) == true) {
cls.cast(pluginInitializeCommand)
} else {
CommandLine.defaultFactory().create(cls)
}
override fun <K : Any> create(cls: Class<K>): K =
if (cls.isAnnotationPresent(Command::class.java)) {
mock(cls)
} else {
CommandLine.defaultFactory().create(cls)
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package io.rabobank.ret.command

import io.rabobank.ret.RetConsole
import io.rabobank.ret.autocompletion.zsh.ZshAutocompletionGenerator
import io.rabobank.ret.config.Logged
import io.rabobank.ret.plugin.Plugin
import io.rabobank.ret.util.Logged
import picocli.CommandLine.Command
import picocli.CommandLine.Model
import picocli.CommandLine.Model.CommandSpec
import picocli.CommandLine.Parameters
import picocli.CommandLine.Spec

Expand All @@ -19,9 +19,8 @@ class ConfigureCommand(
private val zshAutocompletionGenerator: ZshAutocompletionGenerator,
private val plugins: List<Plugin>,
) {

@Spec
lateinit var commandSpec: Model.CommandSpec
lateinit var commandSpec: CommandSpec

@Command(
name = "autocomplete",
Expand All @@ -42,9 +41,7 @@ class ConfigureCommand(
retConsole.out(autoCompleteScript)

plugins.forEach { plugin ->
plugin.pluginDefinition.customZshAutocompletion?.let {
retConsole.out(it)
}
plugin.pluginDefinition.customZshAutocompletion?.let(retConsole::out)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package io.rabobank.ret.command

import io.rabobank.ret.util.Logged
import picocli.CommandLine.Command

@Command(
name = "plugin",
description = ["Initialize or update RET plugins"],
subcommands = [
PluginInitializeCommand::class,
],
subcommands = [PluginInitializeCommand::class],
)
@Logged
class PluginCommand
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
package io.rabobank.ret.command

import io.quarkus.logging.Log
import io.rabobank.ret.RetConsole
import io.rabobank.ret.plugins.RetPlugin
import io.rabobank.ret.util.Logged
import picocli.CommandLine.Command
import picocli.CommandLine.Model.CommandSpec
import picocli.CommandLine.Parameters
import picocli.CommandLine.Spec

@Command(
name = "initialize",
description = ["Initialize plugin"],
)
class PluginInitializeCommand : Runnable {
@Logged
class PluginInitializeCommand(
private val retConsole: RetConsole,
) : Runnable {
@Parameters(
arity = "1",
paramLabel = "<plugin file>",
description = ["Absolute path to plugin"],
)
lateinit var pluginFile: String

@Spec
lateinit var commandSpec: CommandSpec

override fun run() {
System.load(pluginFile)
val isolateThread = RetPlugin.createIsolate()
RetPlugin.initialize(isolateThread, pluginFile)
runCatching {
System.load(pluginFile)
val isolateThread = RetPlugin.createIsolate()
RetPlugin.initialize(isolateThread, pluginFile)
}.onFailure {
retConsole.errorOut("Unable to load plugin file $pluginFile: ${it.message}")
Log.error("Unable to load plugin file $pluginFile", it)
}.getOrThrow()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import io.rabobank.ret.plugins.PluginLoader
import jakarta.enterprise.context.ApplicationScoped
import jakarta.enterprise.inject.Produces
import picocli.CommandLine
import picocli.CommandLine.Help.Ansi.AUTO
import picocli.CommandLine.Help.ColorScheme

@ApplicationScoped
class CommandLineConfiguration(private val pluginLoader: PluginLoader) {
Expand All @@ -14,9 +16,8 @@ class CommandLineConfiguration(private val pluginLoader: PluginLoader) {
fun customCommandLine(factory: PicocliCommandLineFactory, retConsole: RetConsole): CommandLine {
val commandLine = factory.create()
.setExecutionExceptionHandler(ExceptionMessageHandler(retConsole))
.setExecutionStrategy {
CommandLine.RunLast().execute(it)
}
.setExecutionStrategy { CommandLine.RunLast().execute(it) }
.setColorScheme(ColorScheme.Builder().ansi(AUTO).build())

pluginLoader.getPluginCommands(commandLine).forEach {
commandLine.addSubcommand(it.name, it.commandSpec)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import jakarta.enterprise.context.ApplicationScoped
@ApplicationScoped
class ExecutionContext {

private val gitContext: GitContext? = GitContext.create()
private val gitContext = GitContext.create()

fun repositoryName(): String? = gitContext?.repositoryName()
fun branchName(): String? = gitContext?.branchName()
fun repositoryName() = gitContext?.repositoryName()
fun branchName() = gitContext?.branchName()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.rabobank.ret.plugins

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import io.rabobank.ret.plugin.Plugin
import io.rabobank.ret.plugin.PluginDefinition
import io.rabobank.ret.util.OsUtils
Expand All @@ -20,15 +21,15 @@ class PluginConfiguration {
@OptIn(ExperimentalPathApi::class)
@Produces
@ApplicationScoped
fun plugins(osUtils: OsUtils, objectMapper: ObjectMapper): List<Plugin> {
val pluginPath = Path.of(osUtils.getHomeDirectory(), ".ret", "plugins")
fun plugins(osUtils: OsUtils, objectMapper: ObjectMapper): List<Plugin> =
osUtils.getRetPluginsDirectory().let { pluginPath ->
pluginPath.walk()
.map(Path::toFile)
.filter { it.extension == PLUGIN_EXTENSION }
.map { objectMapper.readValue<PluginDefinition>(it.readText()) }
.map { Plugin(it, pluginPath.resolve(it.dylibFile())) }
.toList()
}

return pluginPath
.walk()
.map { it.toFile() }
.filter { it.extension == PLUGIN_EXTENSION }
.map { objectMapper.readValue(it.readText(), PluginDefinition::class.java) }
.map { Plugin(it, pluginPath.resolve(it.libName)) }
.toList()
}
private fun PluginDefinition.dylibFile() = libName.takeIf { it.endsWith(".dylib") } ?: "$libName.dylib"
}
12 changes: 2 additions & 10 deletions ret-cli/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,5 @@ quarkus.log.category."io.rabobank.ret.util".level=WARN
quarkus.banner.enabled=false
quarkus.native.resources.includes=autocompletion/zsh/dynamic-functions.sh,autocompletion/zsh/footer.sh,git.properties
quarkus.application.name=ret

quarkus.log.file.enable=true
quarkus.log.file.path=${HOME}/.ret/logs/ret.log
quarkus.log.file.rotation.rotate-on-boot=false
quarkus.log.file.format=%d{yyyy-MM-dd HH:mm:ss} [version=%X{version} os=%X{os} commit=%X{commit}] %-5p [%c{2.}] %s%e%n
quarkus.log.console.enable=false
quarkus.log.console.format=${quarkus.log.file.format}

%dev.quarkus.log.console.enable=true
%dev.quarkus.log.file.enable=false
#dev profile
%dev.quarkus.log.category."io.rabobank".level=DEBUG
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,9 @@ import io.mockk.mockk
import io.mockk.verify
import io.rabobank.ret.RetConsole
import io.rabobank.ret.autocompletion.zsh.ZshAutocompletionGenerator
import io.rabobank.ret.configuration.Answer
import io.rabobank.ret.configuration.Config
import io.rabobank.ret.configuration.ConfigurationProperty
import io.rabobank.ret.configuration.Question
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import picocli.CommandLine.Model.CommandSpec
import java.nio.file.Path
import java.util.Properties

internal class ConfigureCommandTest {

Expand Down Expand Up @@ -54,28 +48,4 @@ internal class ConfigureCommandTest {
command.printZshAutocompletionScript()
verify { retConsole.out("mocked zsh autocompletion file") }
}

class TestConfig : Config {
private val configProps = listOf(
ConfigurationProperty("project", "Enter your Rabobank project"),
ConfigurationProperty("organisation", "Enter your Rabobank organisation"),
)
private val properties = Properties()

override fun get(key: String) = properties[key] as String?

override fun set(key: String, value: String) {
properties[key] = value
}

override fun configure(function: (ConfigurationProperty) -> Unit) {
configProps.forEach(function)
}

override fun prompt(function: (Question) -> Answer): List<Answer> {
TODO("Not yet implemented")
}

override fun configFile(): Path = Path.of("test-configuration")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@ package io.rabobank.ret.util
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.quarkus.test.junit.QuarkusTest
import io.rabobank.ret.RetConsole
import io.rabobank.ret.config.ExceptionMessageHandler
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import picocli.CommandLine
import picocli.CommandLine.Model.CommandSpec

@QuarkusTest
class ExceptionMessageHandlerTest {

private val retConsole: RetConsole = mockk(relaxed = true)
private val exceptionHandler: ExceptionMessageHandler = ExceptionMessageHandler(retConsole)
private val retConsole = mockk<RetConsole>(relaxed = true)
private val exceptionHandler = ExceptionMessageHandler(retConsole)

@Test
fun `another exception results in exit code 1`() {
Expand Down
8 changes: 8 additions & 0 deletions ret-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-kotlin</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
Expand Down
5 changes: 4 additions & 1 deletion ret-core/src/main/kotlin/io/rabobank/ret/RetConsole.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.rabobank.ret

import jakarta.enterprise.context.ApplicationScoped
import picocli.CommandLine
import picocli.CommandLine.Help.ColorScheme
import picocli.CommandLine.ParseResult

/**
Expand All @@ -12,6 +14,7 @@ import picocli.CommandLine.ParseResult
class RetConsole(parseResult: ParseResult) {

private val commandLine = parseResult.commandSpec().commandLine()
.setColorScheme(ColorScheme.Builder().ansi(CommandLine.Help.Ansi.AUTO).build())

/**
* Prints the [message] to the user.
Expand All @@ -33,7 +36,7 @@ class RetConsole(parseResult: ParseResult) {
*/
fun prompt(message: String, currentValue: String?): String {
val messageWithDefault = if (currentValue.isNullOrEmpty()) message else "$message [$currentValue]"
out("$messageWithDefault:")
out(messageWithDefault)
return readln()
}
}
17 changes: 3 additions & 14 deletions ret-core/src/main/kotlin/io/rabobank/ret/configuration/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,9 @@ package io.rabobank.ret.configuration
import java.nio.file.Path

interface Config {
operator fun get(key: String): String?
operator fun set(key: String, value: String)
operator fun get(key: String): Any?
operator fun set(key: String, value: Any?)
fun configure(function: (ConfigurationProperty) -> Unit)
fun configFile(): Path
fun prompt(function: (Question) -> Answer): List<Answer>
fun pluginConfigDirectory(): Path
}

data class Question(
val key: String,
val prompt: String,
val required: Boolean = false,
)

data class Answer(
val key: String,
val answer: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ package io.rabobank.ret.configuration
* Upon initializing the plugin, the user will be prompted for all provided configuration properties,
* and inputs are saved in the RET configuration file.
*/
interface Configurable {
fun properties(): List<ConfigurationProperty> = emptyList()

fun prompts(): List<Question> = emptyList()
fun interface Configurable {
fun properties(): List<ConfigurationProperty>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.rabobank.ret.configuration

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import io.rabobank.ret.util.OsUtils
import jakarta.enterprise.context.Dependent
import jakarta.inject.Inject
import org.eclipse.microprofile.config.inject.ConfigProperty

/**
* Extend from this base plugin class to help out with loading plugin specific configuration.
* This class also implements [Configurable] with a default implementation of [properties] with an empty list.
*
* @see Configurable
*/
@Dependent
open class ConfigurablePlugin : Configurable {
@ConfigProperty(name = "plugin.name", defaultValue = "ret")
lateinit var pluginName: String

@Inject
lateinit var osUtils: OsUtils

@Inject
lateinit var objectMapper: ObjectMapper

val config by lazy {
runCatching { objectMapper.readValue<Map<String, String>>(osUtils.getPluginConfig(pluginName).toFile()) }
.getOrDefault(emptyMap())
}

override fun properties() = emptyList<ConfigurationProperty>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ package io.rabobank.ret.configuration
* Used for user configurations that can be used by RET plugins. For example: user specific parts in a URL, or personal access tokens.
* @property key the name of the config property.
* @property prompt the message to the user, specifying what should be provided for this config property.
* @property required tells the user this is a mandatory field which cannot be skipped.
*/
data class ConfigurationProperty(val key: String, val prompt: String)
data class ConfigurationProperty(val key: String, val prompt: String, val required: Boolean = false)
Loading

0 comments on commit bc21ee5

Please sign in to comment.