Skip to content

Commit

Permalink
Picocli (#74)
Browse files Browse the repository at this point in the history
* Picocli

* Bump versions
  • Loading branch information
yschimke committed Jun 15, 2019
1 parent 6ca98ad commit 099792f
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 90 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ val jar = tasks["jar"] as org.gradle.jvm.tasks.Jar

dependencies {
implementation("javax.activation:activation")
implementation("com.github.rvesse:airline")
implementation("info.picocli:picocli")
implementation("com.jakewharton.byteunits:byteunits")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8")
Expand Down
173 changes: 93 additions & 80 deletions src/main/kotlin/io/rsocket/cli/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,13 @@ package io.rsocket.cli
import com.baulsupp.oksocial.output.ConsoleHandler
import com.baulsupp.oksocial.output.OutputHandler
import com.baulsupp.oksocial.output.UsageException
import com.github.rvesse.airline.HelpOption
import com.github.rvesse.airline.SingleCommand
import com.github.rvesse.airline.annotations.Arguments
import com.github.rvesse.airline.annotations.Command
import com.github.rvesse.airline.annotations.Option
import com.github.rvesse.airline.annotations.restrictions.AllowedRawValues
import com.github.rvesse.airline.annotations.restrictions.Required
import com.github.rvesse.airline.help.Help
import com.github.rvesse.airline.parser.errors.ParseException
import com.google.common.io.Files
import io.rsocket.AbstractRSocket
import io.rsocket.Closeable
import io.rsocket.ConnectionSetupPayload
import io.rsocket.Payload
import io.rsocket.RSocket
import io.rsocket.RSocketFactory
import io.rsocket.cli.Main.Companion.NAME
import io.rsocket.resume.PeriodicResumeStrategy
import io.rsocket.transport.ClientTransport
import io.rsocket.transport.TransportHeaderAware
Expand All @@ -47,89 +37,88 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.reactivestreams.Publisher
import org.slf4j.LoggerFactory
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.net.URI
import java.nio.charset.StandardCharsets
import java.time.Duration.ofSeconds
import java.util.ArrayList
import java.util.concurrent.TimeUnit
import java.util.function.Supplier
import javax.inject.Inject
import kotlin.system.exitProcess

/**
* Simple command line tool to make a RSocket connection and send/receive elements.
*
* Currently limited in features, only supports a text/line based approach.
*/
@Command(name = NAME, description = "CLI for RSocket.")
class Main {
@Inject
var help: HelpOption<Main>? = null

@Option(name = ["-H", "--header"], description = "Custom header to pass to server")
@Command(description = ["RSocket CLI command"],
name = "rsocket-cli", mixinStandardHelpOptions = true, version = ["dev"])
class Main : Runnable {
@Option(names = ["-H", "--header"], description = ["Custom header to pass to server"])
var headers: List<String>? = null

@Option(name = ["-T", "--transport-header"], description = "Custom header to pass to the transport")
@Option(names = ["-T", "--transport-header"],
description = ["Custom header to pass to the transport"])
var transportHeader: List<String>? = null

@Option(name = ["--stream"], description = "Request Stream")
@Option(names = ["--stream"], description = ["Request Stream"])
var stream: Boolean = false

@Option(name = ["--request"], description = "Request Response")
@Option(names = ["--request"], description = ["Request Response"])
var requestResponse: Boolean = false

@Option(name = ["--fnf"], description = "Fire and Forget")
@Option(names = ["--fnf"], description = ["Fire and Forget"])
var fireAndForget: Boolean = false

@Option(name = ["--channel"], description = "Channel")
@Option(names = ["--channel"], description = ["Channel"])
var channel: Boolean = false

@Option(name = ["--metadataPush"], description = "Metadata Push")
@Option(names = ["--metadataPush"], description = ["Metadata Push"])
var metadataPush: Boolean = false

@Option(name = ["--server"], description = "Start server instead of client")
@Option(names = ["--server"], description = ["Start server instead of client"])
var serverMode: Boolean = false

@Option(name = ["-i", "--input"], description = "String input, '-' (STDIN) or @path/to/file")
@Option(names = ["-i", "--input"], description = ["String input, '-' (STDIN) or @path/to/file"])
var input: List<String>? = null

@Option(name = ["-m", "--metadata"], description = "Metadata input string input or @path/to/file")
@Option(names = ["-m", "--metadata"],
description = ["Metadata input string input or @path/to/file"])
var metadata: String? = null

@Option(name = ["--metadataFormat"], description = "Metadata Format")
@AllowedRawValues(allowedValues = ["json", "cbor", "mime-type"])
@Option(names = ["--metadataFormat"], description = ["Metadata Format"])
var metadataFormat = "json"

@Option(name = ["--dataFormat"], description = "Data Format")
@AllowedRawValues(allowedValues = ["json", "cbor", "mime-type"])
@Option(names = ["--dataFormat"], description = ["Data Format"])
var dataFormat = "binary"

@Option(name = ["--setup"], description = "String input or @path/to/file for setup metadata")
@Option(names = ["--setup"], description = ["String input or @path/to/file for setup metadata"])
var setup: String? = null

@Option(name = ["--debug"], description = "Debug Output")
@Option(names = ["--debug"], description = ["Debug Output"])
var debug: Boolean = false

@Option(name = ["--ops"], description = "Operation Count")
@Option(names = ["--ops"], description = ["Operation Count"])
var operations = 1

@Option(name = ["--timeout"], description = "Timeout in seconds")
@Option(names = ["--timeout"], description = ["Timeout in seconds"])
var timeout: Long? = null

@Option(name = ["--keepalive"], description = "Keepalive period")
@Option(names = ["--keepalive"], description = ["Keepalive period"])
var keepalive: String? = null

@Option(name = ["--requestn", "-r"], description = "Request N credits")
@Option(names = ["--requestn", "-r"], description = ["Request N credits"])
var requestN = Integer.MAX_VALUE

@Option(name = ["--resume"], description = "resume enabled")
@Option(names = ["--resume"], description = ["resume enabled"])
var resume: Boolean = false

@Arguments(title = ["target"], description = "Endpoint URL")
@Required
var arguments: List<String> = ArrayList()
@Parameters(arity = "1", paramLabel = "target", description = ["Endpoint URL"])
var target: String = ""

lateinit var client: RSocket

Expand All @@ -139,7 +128,13 @@ class Main {

lateinit var server: Closeable

suspend fun run() {
override fun run() {
runBlocking {
exec()
}
}

private suspend fun exec(): Void? {
configureLogging(debug)

if (!this::outputHandler.isInitialized) {
Expand All @@ -150,30 +145,26 @@ class Main {
inputPublisher = LineInputPublishers(outputHandler)
}

try {
val uri = sanitizeUri(arguments[0])
val uri = sanitizeUri(target)

if (serverMode) {
server = buildServer(uri)
return if (serverMode) {
server = buildServer(uri)

server.onClose().awaitFirstOrNull()
} else {
if (!this::client.isInitialized) {
client = buildClient(uri)
}
server.onClose().awaitFirstOrNull()
} else {
if (!this::client.isInitialized) {
client = buildClient(uri)
}

val run = run(client)
val run = run(client)

if (timeout != null) {
withTimeout(TimeUnit.SECONDS.toMillis(timeout!!)) {
run.then().awaitFirstOrNull()
}
} else {
if (timeout != null) {
withTimeout(TimeUnit.SECONDS.toMillis(timeout!!)) {
run.then().awaitFirstOrNull()
}
} else {
run.then().awaitFirstOrNull()
}
} catch (e: Exception) {
outputHandler.showError("error", e)
}
}

Expand Down Expand Up @@ -242,18 +233,22 @@ class Main {

private fun parseSetupPayload(): Payload = when {
setup == null -> EmptyPayload.INSTANCE
setup!!.startsWith("@") -> DefaultPayload.create(Files.asCharSource(expectedFile(setup!!.substring(1)), StandardCharsets.UTF_8).read())
setup!!.startsWith("@") -> DefaultPayload.create(
Files.asCharSource(expectedFile(setup!!.substring(1)), StandardCharsets.UTF_8).read())
else -> DefaultPayload.create(setup!!)
}

private fun createServerRequestHandler(setupPayload: ConnectionSetupPayload, socket: RSocket): Mono<RSocket> {
private fun createServerRequestHandler(
setupPayload: ConnectionSetupPayload,
socket: RSocket
): Mono<RSocket> {
LoggerFactory.getLogger(Main::class.java).debug("setup payload $setupPayload")

// TODO chain
runAllOperations(socket).subscribe()
return Mono.just(createResponder())
}

private fun sanitizeUri(uri: String): String {
var validationUri = URI(uri)
if (validationUri.scheme == "ws" || validationUri.scheme == "wss") {
Expand All @@ -266,11 +261,13 @@ class Main {

fun createResponder(): AbstractRSocket {
return object : AbstractRSocket() {
override fun fireAndForget(payload: Payload): Mono<Void> = GlobalScope.mono(Dispatchers.Default) {
override fun fireAndForget(payload: Payload): Mono<Void> = GlobalScope.mono(
Dispatchers.Default) {
outputHandler.showOutput(payload.dataUtf8)
}.then()

override fun requestResponse(payload: Payload): Mono<Payload> = handleIncomingPayload(payload).single()
override fun requestResponse(payload: Payload): Mono<Payload> = handleIncomingPayload(
payload).single()

override fun requestStream(payload: Payload): Flux<Payload> = handleIncomingPayload(payload)

Expand All @@ -282,13 +279,15 @@ class Main {
return inputPublisherX()
}

override fun metadataPush(payload: Payload): Mono<Void> = GlobalScope.mono(Dispatchers.Default) {
override fun metadataPush(payload: Payload): Mono<Void> = GlobalScope.mono(
Dispatchers.Default) {
outputHandler.showOutput(payload.metadataUtf8)
}.then()
}
}

private fun handleIncomingPayload(payload: Payload): Flux<Payload> = GlobalScope.mono(Dispatchers.Default) {
private fun handleIncomingPayload(payload: Payload): Flux<Payload> = GlobalScope.mono(
Dispatchers.Default) {
outputHandler.showOutput(payload.dataUtf8)
}.thenMany(inputPublisherX())

Expand Down Expand Up @@ -352,23 +351,37 @@ class Main {
companion object {
const val NAME = "reactivesocket-cli"

private fun fromArgs(vararg args: String): Main {
val cmd = SingleCommand.singleCommand(Main::class.java)
return try {
cmd.parse(*args)
} catch (e: ParseException) {
System.err.println(e.message)
Help.help(cmd.commandMetadata)
exitProcess(-1)
}
}

@JvmStatic
fun main(vararg args: String) {
runBlocking {
fromArgs(*args).run()
exec(Main(), args.toList())
}

private fun exec(runnable: Runnable, args: List<String>) {
val cmd = CommandLine(runnable)
try {
val parseResult = cmd.parseArgs(*args.toTypedArray())

if (cmd.isUsageHelpRequested) {
cmd.usage(cmd.out)
exitProcess(parseResult.commandSpec().exitCodeOnUsageHelp())
} else if (cmd.isVersionHelpRequested) {
cmd.printVersionHelp(cmd.out)
exitProcess(parseResult.commandSpec().exitCodeOnVersionHelp())
}

runnable.run()

exitProcess(0)
} catch (pe: CommandLine.ParameterException) {
cmd.err.println(pe.message)
if (!CommandLine.UnmatchedArgumentException.printSuggestions(pe, cmd.err)) {
pe.commandLine.usage(cmd.err)
}
exitProcess(cmd.commandSpec.exitCodeOnInvalidInput())
} catch (ex: Exception) {
ex.printStackTrace(cmd.err)
exitProcess(cmd.commandSpec.exitCodeOnExecutionException())
}
exitProcess(0)
}
}
}
3 changes: 1 addition & 2 deletions src/main/kotlin/io/rsocket/cli/util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import com.baulsupp.oksocial.output.UsageException
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.cbor.CBORFactory
import com.github.rvesse.airline.parser.errors.ParseException
import com.google.common.base.Charsets
import com.google.common.collect.Lists
import com.google.common.io.Files
Expand Down Expand Up @@ -169,7 +168,7 @@ fun parseShortDuration(keepalive: String): Duration {
val match = DURATION_FORMAT.matcher(keepalive)

if (!match.matches()) {
throw ParseException("Unknown duration format '$keepalive'")
throw UsageException("Unknown duration format '$keepalive'")
}

val amount = java.lang.Long.valueOf(match.group(1))
Expand Down
4 changes: 2 additions & 2 deletions src/test/kotlin/io/rsocket/cli/i9n/TimeUtilTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.rsocket.cli.i9n

import com.github.rvesse.airline.parser.errors.ParseException
import com.baulsupp.oksocial.output.UsageException
import io.rsocket.cli.parseShortDuration
import org.junit.Assert.assertEquals
import org.junit.Test
Expand All @@ -22,7 +22,7 @@ class TimeUtilTest {
assertEquals(Duration.ofMinutes(0), parseShortDuration("0m"))
}

@Test(expected = ParseException::class)
@Test(expected = UsageException::class)
fun failOnBadFormat() {
parseShortDuration("-10 minutes")
}
Expand Down
6 changes: 1 addition & 5 deletions versions.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.9.9 (2 constraints: 06127
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.9 (2 constraints: 06127100)
com.fasterxml.jackson.module:jackson-module-kotlin:2.9.9 (1 constraints: 16051936)
com.fasterxml.jackson.module:jackson-module-parameter-names:2.9.9 (2 constraints: 06127100)
com.github.rvesse:airline:2.7.0 (1 constraints: 0b050a36)
com.github.rvesse:airline-io:2.7.0 (1 constraints: 750bb4de)
com.google.code.findbugs:jsr305:3.0.2 (1 constraints: 170aecb4)
com.google.errorprone:error_prone_annotations:2.2.0 (1 constraints: 160aebb4)
com.google.guava:failureaccess:1.0.1 (1 constraints: 140ae1b4)
Expand All @@ -21,6 +19,7 @@ com.jakewharton.byteunits:byteunits:0.9.1 (2 constraints: b8124bef)
com.kitfox.svg:svg-salamander:1.0 (2 constraints: 5112e2d6)
com.squareup.okio:okio:2.2.2 (2 constraints: b412b1ee)
commons-io:commons-io:1.4 (1 constraints: c10b1cde)
info.picocli:picocli:4.0.0-beta-1b (1 constraints: 8f07b669)
io.netty:netty-buffer:4.1.36.Final (10 constraints: 6a93fad7)
io.netty:netty-codec:4.1.36.Final (5 constraints: 4c452c79)
io.netty:netty-codec-http:4.1.36.Final (3 constraints: b62ffd30)
Expand All @@ -40,9 +39,6 @@ io.rsocket:rsocket-core:0.12.2-RC4 (3 constraints: ea260c2d)
io.rsocket:rsocket-transport-local:0.12.2-RC4 (1 constraints: 2d066b52)
io.rsocket:rsocket-transport-netty:0.12.2-RC4 (1 constraints: 2d066b52)
javax.activation:activation:1.1.1 (2 constraints: b1123cee)
javax.inject:javax.inject:1 (1 constraints: b10a02b2)
org.apache.commons:commons-collections4:4.2 (1 constraints: 140bfbc7)
org.apache.commons:commons-lang3:3.7 (1 constraints: 1d0c67ee)
org.checkerframework:checker-qual:2.5.2 (1 constraints: 1b0af6b4)
org.codehaus.mojo:animal-sniffer-annotations:1.17 (1 constraints: ed09d8aa)
org.eclipse.jetty:jetty-alpn-client:9.4.19.v20190610 (1 constraints: c7115d3a)
Expand Down
1 change: 1 addition & 0 deletions versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ org.jfree:jfreesvg=3.3
org.junit.jupiter:*=5.4.0
org.slf4j:*=1.8.0-beta4
org.zeroturnaround:zt-exec=1.10
info.picocli:*=4.0.0-beta-1b

0 comments on commit 099792f

Please sign in to comment.