From 3162597160445382b10392b476f6d24830b933a3 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Thu, 22 Jan 2026 17:43:56 +0300 Subject: [PATCH 01/21] Scaffold project --- .github/workflows/ci-test-build.yml | 63 ++++ .gitignore | 43 +++ streamx-cli/mvnw | 308 ++++++++++++++++++ streamx-cli/pom.xml | 180 ++++++++++ .../java/com/streamx/cli/commands/Main.java | 29 ++ .../cli/framework/AbstractCommand.java | 129 ++++++++ .../cli/framework/AbstractCommandGroup.java | 25 ++ .../cli/framework/AbstractSilentCommand.java | 16 + .../streamx/cli/framework/CommandResult.java | 49 +++ .../streamx/cli/framework/CommonOption.java | 9 + .../streamx/cli/framework/OutputFormat.java | 5 + .../framework/ShortErrorMessageHandler.java | 39 +++ .../com/streamx/cli/i18n/MessageProvider.java | 22 ++ .../src/main/resources/application.properties | 7 + .../test/java/dev/streamx/cli/MainTest.java | 43 +++ .../AbstractCommandOutputOptionTest.java | 239 ++++++++++++++ .../cli/framework/AbstractCommandTest.java | 76 +++++ .../AbstractCommandVerboseOptionTest.java | 37 +++ .../framework/AbstractSilentCommandTest.java | 28 ++ .../cli/framework/CommandResultTest.java | 188 +++++++++++ .../testing/AbstractCommandBaseTest.java | 27 ++ .../testing/AbstractSilentTestCommand.java | 36 ++ .../testing/AbstractTestCommand.java | 63 ++++ .../cli/framework/testing/TestObject.java | 37 +++ .../testing/UnserializableObject.java | 6 + 25 files changed, 1704 insertions(+) create mode 100644 .github/workflows/ci-test-build.yml create mode 100644 .gitignore create mode 100755 streamx-cli/mvnw create mode 100644 streamx-cli/pom.xml create mode 100644 streamx-cli/src/main/java/com/streamx/cli/commands/Main.java create mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java create mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java create mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java create mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java create mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/CommonOption.java create mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/OutputFormat.java create mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java create mode 100644 streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java create mode 100644 streamx-cli/src/main/resources/application.properties create mode 100644 streamx-cli/src/test/java/dev/streamx/cli/MainTest.java create mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandOutputOptionTest.java create mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandTest.java create mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandVerboseOptionTest.java create mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractSilentCommandTest.java create mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/CommandResultTest.java create mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractCommandBaseTest.java create mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractSilentTestCommand.java create mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractTestCommand.java create mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/testing/TestObject.java create mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/testing/UnserializableObject.java diff --git a/.github/workflows/ci-test-build.yml b/.github/workflows/ci-test-build.yml new file mode 100644 index 00000000..23a99874 --- /dev/null +++ b/.github/workflows/ci-test-build.yml @@ -0,0 +1,63 @@ +name: 'Test: CI build' + +on: + pull_request: + push: + branches: + - main + +permissions: + id-token: write + contents: read + +jobs: + linux-test-build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + +# - id: 'auth' +# name: 'Authenticate to Google Cloud' +# uses: 'google-github-actions/auth@v2' +# with: +# workload_identity_provider: ${{ secrets.GCP_STREAMX_RELEASES_WORKLOAD_IDENTITY_PROVIDER }} +# service_account: ${{ secrets.GCP_STREAMX_RELEASES_READ_SA }} + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + + - name: Build project + run: | + ./mvnw clean verify -P all-tests + windows-test-build: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v2' + with: + workload_identity_provider: ${{ secrets.GCP_STREAMX_RELEASES_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_STREAMX_RELEASES_READ_SA }} + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + + - name: Build project + run: | + ./mvnw clean verify "-Djacoco.skip=true" -P all-tests \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..037384fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Plugin directory +/.quarkus/cli/plugins/ + +streamx.log +streamx.* +streamx-*.log \ No newline at end of file diff --git a/streamx-cli/mvnw b/streamx-cli/mvnw new file mode 100755 index 00000000..8d937f4c --- /dev/null +++ b/streamx-cli/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/streamx-cli/pom.xml b/streamx-cli/pom.xml new file mode 100644 index 00000000..fe4c4129 --- /dev/null +++ b/streamx-cli/pom.xml @@ -0,0 +1,180 @@ + + + 4.0.0 + + dev.streamx.cli + streamx-cli + 2.0.0-SNAPSHOT + + + 21 + 21 + UTF-8 + UTF-8 + 3.30.6 + 3.2.5 + 3.13.0 + + + + + + io.quarkus.platform + quarkus-bom + ${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-picocli + + + + info.picocli + picocli-shell-jline3 + 4.7.5 + + + + org.jetbrains + annotations + 26.0.2-1 + provided + + + + io.quarkus + quarkus-arc + + + + org.jboss.logging + jboss-logging-annotations + 3.0.4.Final + provided + + + + org.jboss.logging + jboss-logging-processor + 3.0.4.Final + provided + true + + + + com.fasterxml.jackson.core + jackson-databind + 2.20.1 + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.20.1 + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.20.1 + + + + + io.quarkus + quarkus-junit5 + test + + + + org.assertj + assertj-core + 3.27.5 + test + + + + org.instancio + instancio-junit + 5.5.1 + test + + + + + + + io.quarkus.platform + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + true + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + + + org.jboss.logmanager.LogManager + + + + + + + + + native + + + native + + + + false + native + + + + \ No newline at end of file diff --git a/streamx-cli/src/main/java/com/streamx/cli/commands/Main.java b/streamx-cli/src/main/java/com/streamx/cli/commands/Main.java new file mode 100644 index 00000000..7713c1ed --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/commands/Main.java @@ -0,0 +1,29 @@ +package dev.streamx.cli.framework; + +import dev.streamx.cli.framework.cli.AbstractCommandGroup; +import dev.streamx.cli.framework.cli.CommandResult; +import dev.streamx.cli.framework.cli.ShortErrorMessageHandler; +import io.quarkus.picocli.runtime.annotations.TopCommand; +import picocli.CommandLine; + +@TopCommand +@CommandLine.Command( + name = "streamx", + mixinStandardHelpOptions = true, + description = "StreamX CLI. More info at https://streamx.dev", + subcommands = {} +) +public class Main extends AbstractCommandGroup { + @CommandLine.Spec + CommandLine.Model.CommandSpec commandSpec; + + @Override + public CommandResult runCommand() throws RuntimeException { + commandSpec + .commandLine() + .setParameterExceptionHandler(new ShortErrorMessageHandler()) + .usage(System.out); + + return new CommandResult<>(null); + } +} diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java new file mode 100644 index 00000000..94493a44 --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java @@ -0,0 +1,129 @@ +package dev.streamx.cli.framework.cli; + +import static dev.streamx.cli.framework.i18n.MessageProvider.msg; + +import org.jetbrains.annotations.Nullable; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.impl.completer.StringsCompleter; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import picocli.CommandLine; +import picocli.CommandLine.Model.CommandSpec; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; + +/** + * Each CLI command should extend this class. + * + * @param Must be serializable by Jackson (POJO, JsonSerializable, etc.) + */ +public abstract class AbstractCommand implements Runnable { + // Override this method to implement the command logic. + public abstract CommandResult runCommand() throws RuntimeException; + + // Override this method to hide specific command line options. + // May be useful to hide the "--output" option for commands that don't print anything in case of success. + public List getHiddenOptions() { + return List.of(); + } + + // Override this method to provide human-readable output. + public String getTextOutput(CommandResult result) throws RuntimeException { + return result.toText(OutputFormat.json, null); + } + + private void applyHiddenOptions() { + var options = getHiddenOptions(); + + for (String option : options) { + var optionSpec = spec.findOption(option); + if (optionSpec != null) { + spec.remove(optionSpec); + } + } + } + + @CommandLine.Spec + public CommandSpec spec; + + @CommandLine.Spec + public void setSpec(CommandSpec spec) { + this.spec = spec; + applyHiddenOptions(); + } + + @CommandLine.Option( + names = {CommonOption.VERBOSE_SHORT, CommonOption.VERBOSE_LONG}, + description = "Print debug information" + ) + public boolean verbose; + + @CommandLine.Option( + names = {CommonOption.OUTPUT_SHORT, CommonOption.OUTPUT_LONG}, + description = "Specify output format: text, json, yaml", + defaultValue = "text" + ) + // Explicitly set default value here as a fallback for commands with the hidden output option. + public OutputFormat output = OutputFormat.text; + + public void printUsage() { + spec.commandLine().usage(System.out); + } + + // For testing purposes mostly. + protected Terminal createTerminal() throws IOException { + return TerminalBuilder.builder().system(true).build(); + } + + // Use this method for asking user input in interactive commands. + public String promptForInput(String prompt, @Nullable List autocompleteOptions) throws RuntimeException { + try (Terminal terminal = createTerminal()) { + LineReaderBuilder builder = LineReaderBuilder.builder() + .terminal(terminal); + + Completer completer = null; + if (autocompleteOptions != null) { + completer = new StringsCompleter(autocompleteOptions); + builder.completer(completer); + } + + LineReader reader = builder.build(); + + return reader.readLine(completer == null ? prompt : prompt + " (TAB for autocomplete):").strip(); + } catch (IOException e) { + throw new RuntimeException(msg.failedToHandleInteractiveInput(), e); + } + } + + public int execute() { + int exitCode = 0; + + try { + var textOutput = this.runCommand().toText(output, this::getTextOutput); + if (!textOutput.isEmpty()) { + System.out.println(textOutput); + } + } catch (Exception e) { + exitCode = ShortErrorMessageHandler.shortErrorMessage(e, spec.commandLine()); + if (verbose) { + // Print exception stacktrace + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + System.err.println(sw); + } + } + + return exitCode; + } + + public void run() { + int exitCode = execute(); + System.exit(exitCode); + } +} diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java new file mode 100644 index 00000000..b8452181 --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java @@ -0,0 +1,25 @@ +package dev.streamx.cli.framework.cli; + +import java.util.List; + +// Extend this class for commands that don't do anything except display their subcommands. +public class AbstractCommandGroup extends AbstractCommand { + @Override + public CommandResult runCommand() { + this.printUsage(); + return new CommandResult<>(null); + } + + @Override + public String getTextOutput(CommandResult result) throws RuntimeException { + return ""; + } + + @Override + public List getHiddenOptions() { + return List.of( + CommonOption.OUTPUT_LONG, + CommonOption.VERBOSE_LONG + ); + } +} diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java new file mode 100644 index 00000000..725d33ec --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java @@ -0,0 +1,16 @@ +package dev.streamx.cli.framework.cli; + +import java.util.List; + +// Extend this class for commands that don't produce any output, e.g.: settings set. +public abstract class AbstractSilentCommand extends AbstractCommand { + @Override + public List getHiddenOptions() { + return List.of(CommonOption.OUTPUT_LONG); + } + + @Override + public String getTextOutput(CommandResult result) throws RuntimeException { + return ""; + } +} diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java b/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java new file mode 100644 index 00000000..f1da69ad --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java @@ -0,0 +1,49 @@ +package dev.streamx.cli.framework.cli; + +import static dev.streamx.cli.framework.i18n.MessageProvider.msg; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; + +import java.util.function.Function; + +/** + * @param Must be serializable by Jackson (POJO, JsonSerializable, etc.) + */ +public class CommandResult { + public ResultT result; + + public CommandResult(ResultT result) { + this.result = result; + } + + public String toText(OutputFormat outputFormat, Function, String> textFormatter) throws RuntimeException { + try { + switch (outputFormat) { + case OutputFormat.text -> { + return textFormatter.apply(this); + } + case OutputFormat.json -> { + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.valueToTree(result); + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode); + } + case OutputFormat.yaml -> { + var yamlFactory = YAMLFactory.builder() + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) + .build(); + ObjectMapper mapper = new ObjectMapper(yamlFactory); + JsonNode jsonNode = mapper.valueToTree(result); + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode).strip(); + } + } + + throw new RuntimeException(msg.unsupportedOutputFormat()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/CommonOption.java b/streamx-cli/src/main/java/com/streamx/cli/framework/CommonOption.java new file mode 100644 index 00000000..4c7b1cc6 --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/CommonOption.java @@ -0,0 +1,9 @@ +package dev.streamx.cli.framework.cli; + +public class CommonOption { + public static final String VERBOSE_SHORT = "-v"; + public static final String VERBOSE_LONG = "--verbose"; + + public static final String OUTPUT_SHORT = "-o"; + public static final String OUTPUT_LONG = "--output"; +} diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/OutputFormat.java b/streamx-cli/src/main/java/com/streamx/cli/framework/OutputFormat.java new file mode 100644 index 00000000..d62fcc85 --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/OutputFormat.java @@ -0,0 +1,5 @@ +package dev.streamx.cli.framework.cli; + +public enum OutputFormat { + text, json, yaml +} diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java new file mode 100644 index 00000000..9d69370f --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java @@ -0,0 +1,39 @@ +package dev.streamx.cli.framework.cli; + +import picocli.CommandLine; + +import java.io.PrintWriter; + +import picocli.CommandLine.IParameterExceptionHandler; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.UnmatchedArgumentException; + +import static dev.streamx.cli.framework.i18n.MessageProvider.msg; + +public class ShortErrorMessageHandler implements IParameterExceptionHandler { + + @Override + public int handleParseException(ParameterException ex, String[] args) { + CommandLine cmd = ex.getCommandLine(); + return shortErrorMessage(ex, cmd); + } + + static int shortErrorMessage(Exception ex, CommandLine cmd) { + PrintWriter writer = cmd.getErr(); + String errorMessage = ex.getMessage(); + + writer.println(cmd.getColorScheme().errorText(errorMessage)); + if (ex instanceof ParameterException) { + UnmatchedArgumentException.printSuggestions((ParameterException) ex, writer); + } + + if (ex instanceof ParameterException || ex instanceof IllegalArgumentException) { + CommandSpec spec = cmd.getCommandSpec(); + writer.printf(msg.tryForMoreInformationOnAvailableOptions(spec.qualifiedName(), "help".equals(spec.name()) ? "" : " --help")); + return cmd.getCommandSpec().exitCodeOnInvalidInput(); + } + return cmd.getCommandSpec().exitCodeOnExecutionException(); + } + +} \ No newline at end of file diff --git a/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java b/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java new file mode 100644 index 00000000..9ec43a44 --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java @@ -0,0 +1,22 @@ +package dev.streamx.cli.framework.i18n; + +import org.jboss.logging.Messages; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageBundle; + +import java.lang.invoke.MethodHandles; + +@MessageBundle(projectCode = "STREAMXCLI") +public interface MessageProvider { + + MessageProvider msg = Messages.getBundle(MethodHandles.lookup(), MessageProvider.class); + + @Message(id = 100, value = "Unsupported output format") + String unsupportedOutputFormat(); + + @Message(id = 101, value = "Try '%s%s' for more information on the available options.%n") + String tryForMoreInformationOnAvailableOptions(String qualifiedCommandName, String helpOptionName); + + @Message(id = 102, value = "Failed to handle interactive input") + String failedToHandleInteractiveInput(); +} \ No newline at end of file diff --git a/streamx-cli/src/main/resources/application.properties b/streamx-cli/src/main/resources/application.properties new file mode 100644 index 00000000..184395a7 --- /dev/null +++ b/streamx-cli/src/main/resources/application.properties @@ -0,0 +1,7 @@ +quarkus.banner.enabled=false +quarkus.log.level=ERROR +quarkus.log.console.enable=true +quarkus.log.console.level=ERROR + +quarkus.log.category."io.quarkus".level=OFF +quarkus.log.category."org.jboss".level=OFF \ No newline at end of file diff --git a/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java b/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java new file mode 100644 index 00000000..42e7b8d5 --- /dev/null +++ b/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java @@ -0,0 +1,43 @@ +package dev.streamx.cli.framework; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.streamx.cli.framework.cli.AbstractCommand; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +@QuarkusTest +class MainTest { + + @Inject + CommandLine.IFactory factory; + + @Test + void allCommandsAndSubcommandsShouldExtendAbstractCommand() { + CommandLine commandLine = new CommandLine(Main.class, factory); + + Set> allCommandClasses = new HashSet<>(); + collectAllCommands(commandLine.getCommandSpec(), allCommandClasses); + + assertThat(allCommandClasses) + .as("All commands and subcommands should extend AbstractCommand") + .allSatisfy(commandClass -> + assertThat(AbstractCommand.class.isAssignableFrom(commandClass)) + .as("Command %s should extend AbstractCommand", commandClass.getName()) + .isTrue() + ); + } + + private void collectAllCommands(CommandLine.Model.CommandSpec commandSpec, Set> commands) { + Class userObject = commandSpec.userObject().getClass(); + commands.add(userObject); + + for (CommandLine subcommand : commandSpec.subcommands().values()) { + collectAllCommands(subcommand.getCommandSpec(), commands); + } + } +} \ No newline at end of file diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandOutputOptionTest.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandOutputOptionTest.java new file mode 100644 index 00000000..45328835 --- /dev/null +++ b/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandOutputOptionTest.java @@ -0,0 +1,239 @@ +package dev.streamx.cli.framework.cli; + +import static org.junit.jupiter.api.Assertions.*; + +import dev.streamx.cli.framework.cli.testing.AbstractCommandBaseTest; +import dev.streamx.cli.framework.cli.testing.AbstractTestCommand; +import dev.streamx.cli.framework.cli.testing.TestObject; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +import java.util.List; + +class AbstractCommandOutputOptionTest extends AbstractCommandBaseTest { + private final TestObject result = new TestObject( + null, + true, + 100500, + 42.42, + "Test string", + new TestObject( + null, + false, + 7, + 3.14, + "Nested object test string", + null, + null + ), + List.of( + new TestObject( + null, + true, + 15, + 100.42, + "Nested list object 1 test string", + null, + null + ), + new TestObject( + null, + false, + 18, + 0.42, + "Nested list object 2 test string", + null, + null + ) + ) + ); + + @Test + void textOutputFlag_FormatsOutputAsJsonIfNoCustomFormatterProvided() { + var command = new AbstractTestCommand<>(); + CommandLine commandLine = new CommandLine(command); + commandLine.parseArgs(CommonOption.OUTPUT_LONG, "text"); + command.setRunCommandHandler(() -> new CommandResult<>(result)); + command.execute(); + + var expectedStdOutOutput = """ + { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 100500, + "floatValue" : 42.42, + "stringValue" : "Test string", + "nestedObject" : { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 7, + "floatValue" : 3.14, + "stringValue" : "Nested object test string", + "nestedObject" : null, + "nestedObjects" : null + }, + "nestedObjects" : [ { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 15, + "floatValue" : 100.42, + "stringValue" : "Nested list object 1 test string", + "nestedObject" : null, + "nestedObjects" : null + }, { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 18, + "floatValue" : 0.42, + "stringValue" : "Nested list object 2 test string", + "nestedObject" : null, + "nestedObjects" : null + } ] + } + """.strip() + "\n"; + + assertEquals(expectedStdOutOutput, outStream.toString()); + assertEquals("", errStream.toString()); + } + + @Test + void textOutputFlag_FormatsWithCustomFormatter() { + var command = new AbstractTestCommand(); + CommandLine commandLine = new CommandLine(command); + commandLine.parseArgs(CommonOption.OUTPUT_LONG, "text"); + command.setRunCommandHandler(() -> new CommandResult<>(result)); + command.setGetTextOutputHandler((cr) -> """ + String value: %s, + Total nested objects: %d + """.formatted(cr.result.stringValue, cr.result.nestedObjects.size()).strip()); + + command.execute(); + + var expectedStdOutOutput = """ + String value: Test string, + Total nested objects: 2 + """.strip() + "\n"; + + assertEquals(expectedStdOutOutput, outStream.toString()); + assertEquals("", errStream.toString()); + } + + @Test + void jsonOutputFlag() { + var command = new AbstractTestCommand<>(); + CommandLine commandLine = new CommandLine(command); + commandLine.parseArgs(CommonOption.OUTPUT_LONG, "json"); + command.setRunCommandHandler(() -> new CommandResult<>(result)); + command.execute(); + + var expectedStdOutOutput = """ + { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 100500, + "floatValue" : 42.42, + "stringValue" : "Test string", + "nestedObject" : { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 7, + "floatValue" : 3.14, + "stringValue" : "Nested object test string", + "nestedObject" : null, + "nestedObjects" : null + }, + "nestedObjects" : [ { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 15, + "floatValue" : 100.42, + "stringValue" : "Nested list object 1 test string", + "nestedObject" : null, + "nestedObjects" : null + }, { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 18, + "floatValue" : 0.42, + "stringValue" : "Nested list object 2 test string", + "nestedObject" : null, + "nestedObjects" : null + } ] + } + """.strip() + "\n"; + + assertEquals(expectedStdOutOutput, outStream.toString()); + assertEquals("", errStream.toString()); + } + + @Test + void yamlOutputFlag() { + var command = new AbstractTestCommand(); + CommandLine commandLine = new CommandLine(command); + commandLine.parseArgs(CommonOption.OUTPUT_LONG, "yaml"); + command.setRunCommandHandler(() -> new CommandResult<>(result)); + command.execute(); + + var expectedStdOutOutput = """ + voidValue: null + booleanValue: true + longValue: 100500 + floatValue: 42.42 + stringValue: "Test string" + nestedObject: + voidValue: null + booleanValue: false + longValue: 7 + floatValue: 3.14 + stringValue: "Nested object test string" + nestedObject: null + nestedObjects: null + nestedObjects: + - voidValue: null + booleanValue: true + longValue: 15 + floatValue: 100.42 + stringValue: "Nested list object 1 test string" + nestedObject: null + nestedObjects: null + - voidValue: null + booleanValue: false + longValue: 18 + floatValue: 0.42 + stringValue: "Nested list object 2 test string" + nestedObject: null + nestedObjects: null + """.strip() + "\n"; + + assertEquals(expectedStdOutOutput, outStream.toString()); + assertEquals("", errStream.toString()); + } + + @Test + void voidResult() { + var command = new AbstractTestCommand(); + command.setRunCommandHandler(() -> new CommandResult<>(null)); + + command.output = OutputFormat.text; + command.execute(); + + assertEquals("null\n", outStream.toString()); + assertEquals("", errStream.toString()); + + restoreStreams(); + redirectStreams(); + command.output = OutputFormat.json; + command.execute(); + + assertEquals("null\n", outStream.toString()); + assertEquals("", errStream.toString()); + + restoreStreams(); + redirectStreams(); + command.output = OutputFormat.yaml; + command.execute(); + + assertEquals("null\n", outStream.toString()); + assertEquals("", errStream.toString()); + } +} \ No newline at end of file diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandTest.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandTest.java new file mode 100644 index 00000000..33633008 --- /dev/null +++ b/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandTest.java @@ -0,0 +1,76 @@ +package dev.streamx.cli.framework.cli; + +import dev.streamx.cli.framework.cli.testing.AbstractCommandBaseTest; +import dev.streamx.cli.framework.cli.testing.AbstractTestCommand; +import dev.streamx.cli.framework.cli.testing.TestObject; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +import java.io.ByteArrayInputStream; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/* +--enable-native-access=ALL-UNNAMED -Dorg.jline.terminal.dumb=true + */ +class AbstractCommandTest extends AbstractCommandBaseTest { + @Test + void execute_success() { + var command = new AbstractTestCommand(); + command.setRunCommandHandler(() -> new CommandResult<>(TestObject.random())); + new CommandLine(command); + var exitCode = command.execute(); + + assertEquals(0, exitCode); + assertFalse(outStream.toString().isEmpty()); + assertTrue(errStream.toString().isEmpty()); + } + + @Test + void execute_fail() { + var command = new AbstractTestCommand(); + command.setRunCommandHandler(() -> { + throw new RuntimeException("Test exception"); + }); + new CommandLine(command); + var exitCode = command.execute(); + + assertEquals(1, exitCode); + assertTrue(outStream.toString().isEmpty()); + assertTrue(errStream.toString().contains("Test exception")); + } + + @Test + void getHiddenOptionsOverride() { + var command1 = new AbstractTestCommand<>(); + new CommandLine(command1); + + assertNotNull(command1.spec.findOption(CommonOption.OUTPUT_LONG)); + + var command2 = new AbstractTestCommand<>(); + command2.setHiddenOptionsHandler(() -> List.of(CommonOption.OUTPUT_LONG)); + new CommandLine(command2); + + assertNull(command2.spec.findOption(CommonOption.OUTPUT_LONG)); + } + + @Test + void testPromptForInputMultipleCalls() { + var command = new AbstractTestCommand<>(); + CommandLine commandLine = new CommandLine(command); + command.setSpec(commandLine.getCommandSpec()); + + String input1 = "first\n"; + System.setIn(new ByteArrayInputStream(input1.getBytes())); + String result1 = command.promptForInput("Enter first:", null); + + assertEquals("first", result1); + + String input2 = "second\n"; + System.setIn(new ByteArrayInputStream(input2.getBytes())); + String result2 = command.promptForInput("Enter second:", null); + + assertEquals("second", result2); + } +} \ No newline at end of file diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandVerboseOptionTest.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandVerboseOptionTest.java new file mode 100644 index 00000000..20a47cf9 --- /dev/null +++ b/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandVerboseOptionTest.java @@ -0,0 +1,37 @@ +package dev.streamx.cli.framework.cli; + +import dev.streamx.cli.framework.cli.testing.AbstractCommandBaseTest; +import dev.streamx.cli.framework.cli.testing.AbstractTestCommand; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AbstractCommandVerboseOptionTest extends AbstractCommandBaseTest { + @Test + void ifProvided_printsStackTrace() { + var command = new AbstractTestCommand<>(); + command.setRunCommandHandler(() -> { + throw new RuntimeException("Test exception"); + }); + var commandLine = new CommandLine(command); + commandLine.parseArgs(CommonOption.VERBOSE_LONG); + + command.execute(); + + assertTrue(errStream.toString().contains("java.lang.RuntimeException: Test exception")); + } + + @Test + void ifNotProvided_doesntPrintStackTrace() { + var command = new AbstractTestCommand<>(); + command.setRunCommandHandler(() -> { + throw new RuntimeException("Test exception"); + }); + new CommandLine(command); + command.execute(); + + assertFalse(errStream.toString().contains("java.lang.RuntimeException: Test exception")); + } +} \ No newline at end of file diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractSilentCommandTest.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractSilentCommandTest.java new file mode 100644 index 00000000..45645ec1 --- /dev/null +++ b/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractSilentCommandTest.java @@ -0,0 +1,28 @@ +package dev.streamx.cli.framework.cli; + +import dev.streamx.cli.framework.cli.testing.AbstractCommandBaseTest; +import dev.streamx.cli.framework.cli.testing.AbstractSilentTestCommand; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +import static org.junit.jupiter.api.Assertions.*; + +class AbstractSilentCommandTest extends AbstractCommandBaseTest { + @Test + void outputFlagIsAbsent() { + var command = new AbstractSilentTestCommand(); + new CommandLine(command); // Trigger all PicocLi initialization + + assertNull(command.spec.findOption(CommonOption.OUTPUT_LONG)); + } + + @Test + void outputIsEmpty() { + var command = new AbstractSilentTestCommand(); + command.setRunCommandHandler(() -> new CommandResult<>(null)); + command.execute(); + + assertEquals("", outStream.toString()); + assertEquals("", errStream.toString()); + } +} \ No newline at end of file diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/CommandResultTest.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/CommandResultTest.java new file mode 100644 index 00000000..06e22891 --- /dev/null +++ b/streamx-cli/src/test/java/dev/streamx/cli/framework/CommandResultTest.java @@ -0,0 +1,188 @@ +package dev.streamx.cli.framework.cli; + +import static org.junit.jupiter.api.Assertions.*; + +import dev.streamx.cli.framework.cli.testing.TestObject; +import dev.streamx.cli.framework.cli.testing.UnserializableObject; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.function.Function; + +class CommandResultTest { + private final TestObject result = new TestObject( + null, + true, + 100500, + 42.42, + "Test string", + new TestObject( + null, + false, + 7, + 3.14, + "Nested object test string", + null, + null + ), + List.of( + new TestObject( + null, + true, + 15, + 100.42, + "Nested list object 1 test string", + null, + null + ), + new TestObject( + null, + false, + 18, + 0.42, + "Nested list object 2 test string", + null, + null + ) + ) + ); + private final CommandResult commandResult = new CommandResult<>(result); + + @Test + void toText_withTextFormat_shouldUseTextFormatter() { + var expectedOutput = """ + Void Value: null + Boolean Value: true + Long Value: 100500 + Float Value: 42.42 + String Value: Test string + Nested Object Long Value: 7 + Nested Object String Value: Nested object test string + Total Nested Objects: 2 + """; + + Function, String> textFormatter = + cr -> """ + Void Value: %s + Boolean Value: %b + Long Value: %d + Float Value: %.2f + String Value: %s + Nested Object Long Value: %d + Nested Object String Value: %s + Total Nested Objects: %d + """.formatted( + cr.result.voidValue, + cr.result.booleanValue, + cr.result.longValue, + cr.result.floatValue, + cr.result.stringValue, + cr.result.nestedObject.longValue, + cr.result.nestedObject.stringValue, + cr.result.nestedObjects.size() + ); + + var output = commandResult.toText(OutputFormat.text, textFormatter); + + assertEquals(expectedOutput, output); + } + + @Test + void toText_withJsonFormat_shouldReturnPrettyPrintedJson() { + var expectedOutput = """ + { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 100500, + "floatValue" : 42.42, + "stringValue" : "Test string", + "nestedObject" : { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 7, + "floatValue" : 3.14, + "stringValue" : "Nested object test string", + "nestedObject" : null, + "nestedObjects" : null + }, + "nestedObjects" : [ { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 15, + "floatValue" : 100.42, + "stringValue" : "Nested list object 1 test string", + "nestedObject" : null, + "nestedObjects" : null + }, { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 18, + "floatValue" : 0.42, + "stringValue" : "Nested list object 2 test string", + "nestedObject" : null, + "nestedObjects" : null + } ] + } + """.strip(); + + var output = commandResult.toText(OutputFormat.json, null); + + assertEquals(expectedOutput, output); + } + + @Test + void toText_withYamlFormat_shouldReturnYaml() { + var expectedOutput = """ + voidValue: null + booleanValue: true + longValue: 100500 + floatValue: 42.42 + stringValue: "Test string" + nestedObject: + voidValue: null + booleanValue: false + longValue: 7 + floatValue: 3.14 + stringValue: "Nested object test string" + nestedObject: null + nestedObjects: null + nestedObjects: + - voidValue: null + booleanValue: true + longValue: 15 + floatValue: 100.42 + stringValue: "Nested list object 1 test string" + nestedObject: null + nestedObjects: null + - voidValue: null + booleanValue: false + longValue: 18 + floatValue: 0.42 + stringValue: "Nested list object 2 test string" + nestedObject: null + nestedObjects: null + """.strip(); + + var output = commandResult.toText(OutputFormat.yaml, null); + + assertEquals(expectedOutput, output); + } + + @Test + void toText_withNullResult_shouldHandleGracefully() { + CommandResult commandResult = new CommandResult<>(null); + + assertEquals("null", commandResult.toText(OutputFormat.json, null)); + assertEquals("null", commandResult.toText(OutputFormat.yaml, null)); + } + + @Test + void toText_shouldThrowRuntimeExceptionForUnserializableObject() { + UnserializableObject unserializable = new UnserializableObject(); + CommandResult commandResult = new CommandResult<>(unserializable); + + assertThrows(RuntimeException.class, () -> + commandResult.toText(OutputFormat.json, null) + ); + } +} \ No newline at end of file diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractCommandBaseTest.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractCommandBaseTest.java new file mode 100644 index 00000000..ed6d33f5 --- /dev/null +++ b/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractCommandBaseTest.java @@ -0,0 +1,27 @@ +package dev.streamx.cli.framework.cli.testing; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +public class AbstractCommandBaseTest { + public final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + public final ByteArrayOutputStream errStream = new ByteArrayOutputStream(); + + @BeforeEach + public void redirectStreams() { + System.setOut(new PrintStream(outStream)); + System.setErr(new PrintStream(errStream)); + } + + @AfterEach + public void restoreStreams() { + outStream.reset(); + errStream.reset(); + + System.setOut(System.out); + System.setErr(System.err); + } +} diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractSilentTestCommand.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractSilentTestCommand.java new file mode 100644 index 00000000..929a9da3 --- /dev/null +++ b/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractSilentTestCommand.java @@ -0,0 +1,36 @@ +package dev.streamx.cli.framework.cli.testing; + +import dev.streamx.cli.framework.cli.AbstractSilentCommand; +import dev.streamx.cli.framework.cli.CommandResult; + +import java.util.List; +import java.util.function.Supplier; + +// Helper class for testing SilentAbstractCommand +public class AbstractSilentTestCommand extends AbstractSilentCommand { + public Supplier> runCommandHandler; + public Supplier> hiddenOptionsHandler; + + public void setRunCommandHandler(Supplier> handler) { + this.runCommandHandler = handler; + } + public void setHiddenOptionsHandler(Supplier> handler) { + this.hiddenOptionsHandler = handler; + } + + @Override + public CommandResult runCommand() throws RuntimeException { + if (runCommandHandler != null) { + return runCommandHandler.get(); + } + throw new IllegalStateException("No run command handler set"); + } + + @Override + public List getHiddenOptions() { + if (hiddenOptionsHandler != null) { + return hiddenOptionsHandler.get(); + } + return super.getHiddenOptions(); + } +} diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractTestCommand.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractTestCommand.java new file mode 100644 index 00000000..c9e80285 --- /dev/null +++ b/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractTestCommand.java @@ -0,0 +1,63 @@ +package dev.streamx.cli.framework.cli.testing; + +import dev.streamx.cli.framework.cli.AbstractCommand; +import dev.streamx.cli.framework.cli.CommandResult; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; + +import java.io.IOException; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +// Helper class for testing AbstractCommand +public class AbstractTestCommand extends AbstractCommand { + public Supplier> runCommandHandler; + public Supplier> hiddenOptionsHandler; + public Function, String> getTextOutputHandler; + + public void setRunCommandHandler(Supplier> handler) { + this.runCommandHandler = handler; + } + + public void setHiddenOptionsHandler(Supplier> handler) { + this.hiddenOptionsHandler = handler; + } + + public void setGetTextOutputHandler(Function, String> handler) { + this.getTextOutputHandler = handler; + } + + @Override + public CommandResult runCommand() throws RuntimeException { + if (runCommandHandler != null) { + return runCommandHandler.get(); + } + throw new IllegalStateException("No run command handler set"); + } + + @Override + public List getHiddenOptions() { + if (hiddenOptionsHandler != null) { + return hiddenOptionsHandler.get(); + } + return super.getHiddenOptions(); + } + + @Override + public String getTextOutput(CommandResult result) throws RuntimeException { + if (getTextOutputHandler != null) { + return getTextOutputHandler.apply(result); + } + return super.getTextOutput(result); + } + + @Override + protected Terminal createTerminal() throws IOException { + return TerminalBuilder.builder() + .system(false) + .streams(System.in, System.out) + .dumb(true) + .build(); + } +} diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/TestObject.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/TestObject.java new file mode 100644 index 00000000..ca5b50e1 --- /dev/null +++ b/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/TestObject.java @@ -0,0 +1,37 @@ +package dev.streamx.cli.framework.cli.testing; + +import org.instancio.Instancio; + +import java.util.List; + +public class TestObject { + public Void voidValue; + public boolean booleanValue; + public long longValue; + public double floatValue; + public String stringValue; + public TestObject nestedObject; + public List nestedObjects; + + public TestObject( + Void voidValue, + boolean booleanValue, + long longValue, + double floatValue, + String stringValue, + TestObject nestedObject, + List nestedObjects + ) { + this.voidValue = voidValue; + this.booleanValue = booleanValue; + this.longValue = longValue; + this.floatValue = floatValue; + this.stringValue = stringValue; + this.nestedObject = nestedObject; + this.nestedObjects = nestedObjects; + } + + public static TestObject random() { + return Instancio.create(TestObject.class); + } +} \ No newline at end of file diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/UnserializableObject.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/UnserializableObject.java new file mode 100644 index 00000000..865b634a --- /dev/null +++ b/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/UnserializableObject.java @@ -0,0 +1,6 @@ +package dev.streamx.cli.framework.cli.testing; + +public class UnserializableObject { + // Object with circular reference to make it unserializable + public UnserializableObject self = this; +} \ No newline at end of file From f4f1f8a985a8d64599dcada4d9c3c448321c71b2 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Thu, 22 Jan 2026 17:54:11 +0300 Subject: [PATCH 02/21] Update README.md --- README.md | 68 +++---------- streamx-cli/.mvn/wrapper/.gitignore | 1 + .../.mvn/wrapper/MavenWrapperDownloader.java | 98 +++++++++++++++++++ .../.mvn/wrapper/maven-wrapper.properties | 18 ++++ .../cli/framework/AbstractCommand.java | 3 +- 5 files changed, 133 insertions(+), 55 deletions(-) create mode 100644 streamx-cli/.mvn/wrapper/.gitignore create mode 100644 streamx-cli/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 streamx-cli/.mvn/wrapper/maven-wrapper.properties diff --git a/README.md b/README.md index 8fb21fae..4471f961 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,25 @@ -# **StreamX Dev Repository Template** +# **StreamX CLI v2.x** -This repository serves as a **template** for creating new repositories within the **streamx-dev** organization. It includes pre-configured settings, templates, and best practices to ensure consistency and compliance across all projects. +This project provides utilities for managing the mesh: +* It allows to run a defined mesh from commands, +* It allows to ingest data into mesh. -## **Included Configurations** +For more information, see the [StreamX CLI Reference](https://www.streamx.dev/guides/main/streamx-command-line-interface-reference.html). -1. **EULA License:** - - A sample **End-User License Agreement (EULA)** is included for repositories that require it. - - Located in the `LICENSE.md` file. - -2. **Issue Templates:** - - Standardized templates for reporting bugs, requesting features, and general contacts links. - - Located in the `.github/ISSUE_TEMPLATE/` directory. +## Development -3. **Pull Request (PR) Templates:** - - A default PR template is provided to maintain quality and structure in submissions. - - Located in the `.github/PULL_REQUEST_TEMPLATE.md` file. +- Enter Quarkus development console. -4. **Codeowners:** - - Defines the responsible individuals or teams for reviewing changes. - - Located in the `.github/CODEOWNERS` file. - -5. **Dependabot:** - - It is a tool that automates dependency updates, ensuring your project stays up-to-date with the latest security patches and version improvements. - - Located in the `.github/dependabot.yml` file. - - The provided Dependabot setup performs a monthly scan of the subfolders in your repository for Maven projects, checking dependencies for outdated versions. If any outdated dependencies are detected, Dependabot automatically creates a new Pull Request (PR) with the updates. - - It is often necessary to establish a CI/CD pipeline to validate and test updates whenever a new PR is opened. These pipelines require specific credentials to operate effectively. However, it is important to note that pipelines triggered by Dependabot use a different set of variables and secrets than those triggered by GitHub users manually opening a PR. -Please contact the Principal Engineer responisble for Infrastructure to provide the appropriate secrets for Dependabot-triggered pipelines. - - You can learn more about managing secrets for Dependabot-triggered pipelines in the [GitHub documentation on Accessing Secrets in Dependabot](https://docs.github.com/en/code-security/dependabot/troubleshooting-dependabot/troubleshooting-dependabot-on-github-actions#accessing-secrets). +`cd streamx-cli && ./mvnw quarkus:dev` -## **Repository Settings Standards** +- Use `e` button to edit CLI arguments. -1. **Main Branch Protection Rules** - - The main branch is protected to maintain code quality and stability. - - Merging to `main` requires: a review with approval from at least one **code owner**. - -3. **Default reposiotry settings**: - - Wikis - disabled - - Issues - enabled - - for PRs only squash merging are allowed - - Automatically delete head branches - allowed +## Running tests -## **How to Use This Template** -1. **Clone or Use as Template:** - - Create by cloning the repository. - - Create a new repository based on this template. See example below: -image +`cd streamx-cli && ./mvnw test` -2. **Update Configurations:** - - **Adjust the License:** If needed, replace the default license with one that matches your project’s requirements. When there are any dobts, check the [decision log](https://teamds.atlassian.net/wiki/x/AYA1KQ) related to Licensing Policy. - - **Modify codeowners:** Ensure the responsible teams or individuals are correctly listed in `.github/CODEOWNERS`. It's importnant to avoid merge bottlenecks. - - **Adjust Dependabot configuration:** Adjust dependabot config to fit the project needs. Provide CI/CD pipelines to validate and test updates, as well. - - **Provide a JIRA release GitHub action:** [here](https://github.com/streamx-dev/streamx-common-github-actions) you can find more details on how and why to create it. - - **Update README.md:** Add specific details about your project to replace this template content. +## Configuration -3. **Start Developing:** - - Get familiar with [Contribution Policy](https://github.com/streamx-dev/streamx/blob/main/CONTRIBUTING.md). - - Push your code, create issues, and submit PRs following the provided templates. +There are several ways of configuring and several properties to configure. -## **Best Practices** -- Regularly review the codeowners file to ensure it reflects the correct reviewers. -- Keep your license file up to date with the project’s purpose and legal requirements. -- Ensure all team members are familiar with the repository’s templates and protection rules. - -By using this template, you’re setting up your repository for success with clear structure, strong protections, and organizational consistency. +For details refer to [StreamX CLI Reference](https://www.streamx.dev/guides/streamx-command-line-interface-reference.html). diff --git a/streamx-cli/.mvn/wrapper/.gitignore b/streamx-cli/.mvn/wrapper/.gitignore new file mode 100644 index 00000000..e72f5e8b --- /dev/null +++ b/streamx-cli/.mvn/wrapper/.gitignore @@ -0,0 +1 @@ +maven-wrapper.jar diff --git a/streamx-cli/.mvn/wrapper/MavenWrapperDownloader.java b/streamx-cli/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..84d1e60d --- /dev/null +++ b/streamx-cli/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +public final class MavenWrapperDownloader +{ + private static final String WRAPPER_VERSION = "3.2.0"; + + private static final boolean VERBOSE = Boolean.parseBoolean( System.getenv( "MVNW_VERBOSE" ) ); + + public static void main( String[] args ) + { + log( "Apache Maven Wrapper Downloader " + WRAPPER_VERSION ); + + if ( args.length != 2 ) + { + System.err.println( " - ERROR wrapperUrl or wrapperJarPath parameter missing" ); + System.exit( 1 ); + } + + try + { + log( " - Downloader started" ); + final URL wrapperUrl = new URL( args[0] ); + final String jarPath = args[1].replace( "..", "" ); // Sanitize path + final Path wrapperJarPath = Paths.get( jarPath ).toAbsolutePath().normalize(); + downloadFileFromURL( wrapperUrl, wrapperJarPath ); + log( "Done" ); + } + catch ( IOException e ) + { + System.err.println( "- Error downloading: " + e.getMessage() ); + if ( VERBOSE ) + { + e.printStackTrace(); + } + System.exit( 1 ); + } + } + + private static void downloadFileFromURL( URL wrapperUrl, Path wrapperJarPath ) + throws IOException + { + log( " - Downloading to: " + wrapperJarPath ); + if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null ) + { + final String username = System.getenv( "MVNW_USERNAME" ); + final char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray(); + Authenticator.setDefault( new Authenticator() + { + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication( username, password ); + } + } ); + } + try ( InputStream inStream = wrapperUrl.openStream() ) + { + Files.copy( inStream, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING ); + } + log( " - Downloader complete" ); + } + + private static void log( String msg ) + { + if ( VERBOSE ) + { + System.out.println( msg ); + } + } + +} diff --git a/streamx-cli/.mvn/wrapper/maven-wrapper.properties b/streamx-cli/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..346d645f --- /dev/null +++ b/streamx-cli/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java index 94493a44..4329390e 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java @@ -11,6 +11,7 @@ import org.jline.terminal.TerminalBuilder; import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; +import io.quarkus.runtime.Quarkus; import java.io.IOException; import java.io.PrintWriter; @@ -124,6 +125,6 @@ public int execute() { public void run() { int exitCode = execute(); - System.exit(exitCode); + Quarkus.asyncExit(exitCode); } } From 03a1295604232fc32c3e6db5d72c9fce34cb3460 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Thu, 22 Jan 2026 17:58:06 +0300 Subject: [PATCH 03/21] Adjust test and build GitHub action --- .github/workflows/ci-test-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-test-build.yml b/.github/workflows/ci-test-build.yml index 23a99874..9c1e7fa5 100644 --- a/.github/workflows/ci-test-build.yml +++ b/.github/workflows/ci-test-build.yml @@ -33,9 +33,9 @@ jobs: distribution: 'temurin' cache: 'maven' - - name: Build project + - name: Run tests run: | - ./mvnw clean verify -P all-tests + cd ./streamx-cli && ./mvnw clean test windows-test-build: runs-on: windows-latest steps: @@ -58,6 +58,6 @@ jobs: distribution: 'temurin' cache: 'maven' - - name: Build project + - name: Run tests run: | - ./mvnw clean verify "-Djacoco.skip=true" -P all-tests \ No newline at end of file + cd ./streamx-cli && ./mvnw clean test \ No newline at end of file From ee37aa82719102ecd90e2d2f88d215f32f36dfca Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Thu, 22 Jan 2026 17:59:32 +0300 Subject: [PATCH 04/21] Fix build --- .github/workflows/ci-test-build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-test-build.yml b/.github/workflows/ci-test-build.yml index 9c1e7fa5..85c3b8d5 100644 --- a/.github/workflows/ci-test-build.yml +++ b/.github/workflows/ci-test-build.yml @@ -44,12 +44,12 @@ jobs: with: fetch-depth: 0 - - id: 'auth' - name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v2' - with: - workload_identity_provider: ${{ secrets.GCP_STREAMX_RELEASES_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ secrets.GCP_STREAMX_RELEASES_READ_SA }} +# - id: 'auth' +# name: 'Authenticate to Google Cloud' +# uses: 'google-github-actions/auth@v2' +# with: +# workload_identity_provider: ${{ secrets.GCP_STREAMX_RELEASES_WORKLOAD_IDENTITY_PROVIDER }} +# service_account: ${{ secrets.GCP_STREAMX_RELEASES_READ_SA }} - name: Set up JDK 21 uses: actions/setup-java@v3 From b2c7fe2022b9959cae60d4120012f4e02f719cb3 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Thu, 22 Jan 2026 17:59:58 +0300 Subject: [PATCH 05/21] Remove unused code --- .github/workflows/ci-test-build.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/ci-test-build.yml b/.github/workflows/ci-test-build.yml index 85c3b8d5..49073674 100644 --- a/.github/workflows/ci-test-build.yml +++ b/.github/workflows/ci-test-build.yml @@ -19,13 +19,6 @@ jobs: with: fetch-depth: 0 -# - id: 'auth' -# name: 'Authenticate to Google Cloud' -# uses: 'google-github-actions/auth@v2' -# with: -# workload_identity_provider: ${{ secrets.GCP_STREAMX_RELEASES_WORKLOAD_IDENTITY_PROVIDER }} -# service_account: ${{ secrets.GCP_STREAMX_RELEASES_READ_SA }} - - name: Set up JDK 21 uses: actions/setup-java@v3 with: @@ -44,13 +37,6 @@ jobs: with: fetch-depth: 0 -# - id: 'auth' -# name: 'Authenticate to Google Cloud' -# uses: 'google-github-actions/auth@v2' -# with: -# workload_identity_provider: ${{ secrets.GCP_STREAMX_RELEASES_WORKLOAD_IDENTITY_PROVIDER }} -# service_account: ${{ secrets.GCP_STREAMX_RELEASES_READ_SA }} - - name: Set up JDK 21 uses: actions/setup-java@v3 with: From 0b041f95e4ae0edc5afaff0d11cd11aa6ea09665 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Thu, 22 Jan 2026 18:04:41 +0300 Subject: [PATCH 06/21] upd --- .../src/main/java/com/streamx/cli/{commands => }/Main.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename streamx-cli/src/main/java/com/streamx/cli/{commands => }/Main.java (100%) diff --git a/streamx-cli/src/main/java/com/streamx/cli/commands/Main.java b/streamx-cli/src/main/java/com/streamx/cli/Main.java similarity index 100% rename from streamx-cli/src/main/java/com/streamx/cli/commands/Main.java rename to streamx-cli/src/main/java/com/streamx/cli/Main.java From 6a9fbd09cc16013dd79379c245df884544231faa Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Thu, 22 Jan 2026 18:08:09 +0300 Subject: [PATCH 07/21] Remove unused code --- .../java/dev/streamx/cli/framework/AbstractCommandTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandTest.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandTest.java index 33633008..da9f40c0 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandTest.java +++ b/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandTest.java @@ -11,9 +11,6 @@ import static org.junit.jupiter.api.Assertions.*; -/* ---enable-native-access=ALL-UNNAMED -Dorg.jline.terminal.dumb=true - */ class AbstractCommandTest extends AbstractCommandBaseTest { @Test void execute_success() { From 5ee523786ebc92c33b463c18ead3e513174b94b7 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Thu, 22 Jan 2026 18:14:11 +0300 Subject: [PATCH 08/21] Add .editorconfig, apply endfile newline formatting --- .editorconfig | 8 ++++++++ .github/workflows/ci-test-build.yml | 2 +- streamx-cli/pom.xml | 2 +- .../streamx/cli/framework/ShortErrorMessageHandler.java | 2 +- .../main/java/com/streamx/cli/i18n/MessageProvider.java | 2 +- streamx-cli/src/main/resources/application.properties | 2 +- streamx-cli/src/test/java/dev/streamx/cli/MainTest.java | 2 +- .../cli/framework/AbstractCommandOutputOptionTest.java | 2 +- .../dev/streamx/cli/framework/AbstractCommandTest.java | 2 +- .../cli/framework/AbstractCommandVerboseOptionTest.java | 2 +- .../streamx/cli/framework/AbstractSilentCommandTest.java | 2 +- .../java/dev/streamx/cli/framework/CommandResultTest.java | 2 +- .../dev/streamx/cli/framework/testing/TestObject.java | 2 +- .../cli/framework/testing/UnserializableObject.java | 2 +- 14 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7ebee5de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 100 +tab_width = 2 \ No newline at end of file diff --git a/.github/workflows/ci-test-build.yml b/.github/workflows/ci-test-build.yml index 49073674..54e92e05 100644 --- a/.github/workflows/ci-test-build.yml +++ b/.github/workflows/ci-test-build.yml @@ -46,4 +46,4 @@ jobs: - name: Run tests run: | - cd ./streamx-cli && ./mvnw clean test \ No newline at end of file + cd ./streamx-cli && ./mvnw clean test diff --git a/streamx-cli/pom.xml b/streamx-cli/pom.xml index fe4c4129..9003174c 100644 --- a/streamx-cli/pom.xml +++ b/streamx-cli/pom.xml @@ -177,4 +177,4 @@ - \ No newline at end of file + diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java index 9d69370f..4b5886cf 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java @@ -36,4 +36,4 @@ static int shortErrorMessage(Exception ex, CommandLine cmd) { return cmd.getCommandSpec().exitCodeOnExecutionException(); } -} \ No newline at end of file +} diff --git a/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java b/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java index 9ec43a44..759fe609 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java +++ b/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java @@ -19,4 +19,4 @@ public interface MessageProvider { @Message(id = 102, value = "Failed to handle interactive input") String failedToHandleInteractiveInput(); -} \ No newline at end of file +} diff --git a/streamx-cli/src/main/resources/application.properties b/streamx-cli/src/main/resources/application.properties index 184395a7..cb541572 100644 --- a/streamx-cli/src/main/resources/application.properties +++ b/streamx-cli/src/main/resources/application.properties @@ -4,4 +4,4 @@ quarkus.log.console.enable=true quarkus.log.console.level=ERROR quarkus.log.category."io.quarkus".level=OFF -quarkus.log.category."org.jboss".level=OFF \ No newline at end of file +quarkus.log.category."org.jboss".level=OFF diff --git a/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java b/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java index 42e7b8d5..9bb1a0c0 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java +++ b/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java @@ -40,4 +40,4 @@ private void collectAllCommands(CommandLine.Model.CommandSpec commandSpec, Set Date: Thu, 22 Jan 2026 18:25:15 +0300 Subject: [PATCH 09/21] Add Checkstyle plugin --- .../codestyle/checkstyle-suppressions.xml | 9 + .../codestyle/intellij-java-google-style.xml | 598 ++++++++++++++++++ streamx-cli/pom.xml | 373 ++++++----- .../test/java/dev/streamx/cli/MainTest.java | 2 + 4 files changed, 810 insertions(+), 172 deletions(-) create mode 100644 streamx-cli/codestyle/checkstyle-suppressions.xml create mode 100644 streamx-cli/codestyle/intellij-java-google-style.xml diff --git a/streamx-cli/codestyle/checkstyle-suppressions.xml b/streamx-cli/codestyle/checkstyle-suppressions.xml new file mode 100644 index 00000000..04ffb895 --- /dev/null +++ b/streamx-cli/codestyle/checkstyle-suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/streamx-cli/codestyle/intellij-java-google-style.xml b/streamx-cli/codestyle/intellij-java-google-style.xml new file mode 100644 index 00000000..f3a6743e --- /dev/null +++ b/streamx-cli/codestyle/intellij-java-google-style.xml @@ -0,0 +1,598 @@ + + + + + + diff --git a/streamx-cli/pom.xml b/streamx-cli/pom.xml index 9003174c..236fc1ff 100644 --- a/streamx-cli/pom.xml +++ b/streamx-cli/pom.xml @@ -3,178 +3,207 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - 4.0.0 - - dev.streamx.cli - streamx-cli - 2.0.0-SNAPSHOT - - - 21 - 21 - UTF-8 - UTF-8 - 3.30.6 - 3.2.5 - 3.13.0 - - - - - - io.quarkus.platform - quarkus-bom - ${quarkus.platform.version} - pom - import - - - - + 4.0.0 + + dev.streamx.cli + streamx-cli + 2.0.0-SNAPSHOT + + + 21 + 21 + UTF-8 + UTF-8 + 3.30.6 + 3.2.5 + 3.13.0 + 3.6.0 + + + - - io.quarkus - quarkus-picocli - - - - info.picocli - picocli-shell-jline3 - 4.7.5 - - - - org.jetbrains - annotations - 26.0.2-1 - provided - - - - io.quarkus - quarkus-arc - - - - org.jboss.logging - jboss-logging-annotations - 3.0.4.Final - provided - - - - org.jboss.logging - jboss-logging-processor - 3.0.4.Final - provided - true - - - - com.fasterxml.jackson.core - jackson-databind - 2.20.1 - - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.20.1 - - - - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - 2.20.1 - - - - - io.quarkus - quarkus-junit5 - test - - - - org.assertj - assertj-core - 3.27.5 - test - - - - org.instancio - instancio-junit - 5.5.1 - test - + + io.quarkus.platform + quarkus-bom + ${quarkus.platform.version} + pom + import + - - - - - io.quarkus.platform - quarkus-maven-plugin - ${quarkus.platform.version} - true - - - - build - generate-code - generate-code-tests - - - - - - maven-compiler-plugin - ${compiler-plugin.version} - - true - - - - maven-surefire-plugin - ${surefire-plugin.version} - - - org.jboss.logmanager.LogManager - - - - - maven-failsafe-plugin - ${surefire-plugin.version} - - - - integration-test - verify - - - - - - org.jboss.logmanager.LogManager - - - - - - - - - native - - - native - - - - false - native - - - + + + + + io.quarkus + quarkus-picocli + + + + info.picocli + picocli-shell-jline3 + 4.7.5 + + + + org.jetbrains + annotations + 26.0.2-1 + provided + + + + io.quarkus + quarkus-arc + + + + org.jboss.logging + jboss-logging-annotations + 3.0.4.Final + provided + + + + org.jboss.logging + jboss-logging-processor + 3.0.4.Final + provided + true + + + + com.fasterxml.jackson.core + jackson-databind + 2.20.1 + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.20.1 + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.20.1 + + + + + io.quarkus + quarkus-junit5 + test + + + + org.assertj + assertj-core + 3.27.5 + test + + + + org.instancio + instancio-junit + 5.5.1 + test + + + + + + + io.quarkus.platform + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle-plugin.version} + + google_checks.xml + true + true + true + true + warning + true + codestyle/checkstyle-suppressions.xml + + ${project.build.sourceDirectory} + ${project.build.testSourceDirectory} + + + + + checkstyle + + check + + validate + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + true + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + + + org.jboss.logmanager.LogManager + + + + + + + + + native + + + native + + + + false + native + + + diff --git a/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java b/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java index 9bb1a0c0..4323a03a 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java +++ b/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java @@ -5,8 +5,10 @@ import dev.streamx.cli.framework.cli.AbstractCommand; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; + import java.util.HashSet; import java.util.Set; + import org.junit.jupiter.api.Test; import picocli.CommandLine; From 364ed79c582c8c1be2c08d526150b43f1937a0ca Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Thu, 22 Jan 2026 19:32:44 +0300 Subject: [PATCH 10/21] Add and apply checkstyle linter --- streamx-cli/pom.xml | 2 +- .../src/main/java/com/streamx/cli/Main.java | 18 +- .../cli/framework/AbstractCommand.java | 38 +-- .../cli/framework/AbstractCommandGroup.java | 2 +- .../cli/framework/AbstractSilentCommand.java | 2 +- .../streamx/cli/framework/CommandResult.java | 17 +- .../streamx/cli/framework/CommonOption.java | 2 +- .../streamx/cli/framework/OutputFormat.java | 2 +- .../framework/ShortErrorMessageHandler.java | 13 +- .../com/streamx/cli/i18n/MessageProvider.java | 10 +- .../{dev => com}/streamx/cli/MainTest.java | 23 +- .../AbstractCommandOutputOptionTest.java | 238 +++++++++++++++++ .../cli/framework/AbstractCommandTest.java | 19 +- .../AbstractCommandVerboseOptionTest.java | 12 +- .../framework/AbstractSilentCommandTest.java | 11 +- .../cli/framework/CommandResultTest.java | 188 ++++++++++++++ .../testing/AbstractCommandBaseTest.java | 7 +- .../testing/AbstractSilentTestCommand.java | 8 +- .../testing/AbstractTestCommand.java | 11 +- .../cli/framework/testing/TestObject.java | 19 +- .../testing/UnserializableObject.java | 2 +- .../AbstractCommandOutputOptionTest.java | 239 ------------------ .../cli/framework/CommandResultTest.java | 188 -------------- 23 files changed, 540 insertions(+), 531 deletions(-) rename streamx-cli/src/test/java/{dev => com}/streamx/cli/MainTest.java (63%) create mode 100644 streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java rename streamx-cli/src/test/java/{dev => com}/streamx/cli/framework/AbstractCommandTest.java (79%) rename streamx-cli/src/test/java/{dev => com}/streamx/cli/framework/AbstractCommandVerboseOptionTest.java (85%) rename streamx-cli/src/test/java/{dev => com}/streamx/cli/framework/AbstractSilentCommandTest.java (68%) create mode 100644 streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java rename streamx-cli/src/test/java/{dev => com}/streamx/cli/framework/testing/AbstractCommandBaseTest.java (93%) rename streamx-cli/src/test/java/{dev => com}/streamx/cli/framework/testing/AbstractSilentTestCommand.java (85%) rename streamx-cli/src/test/java/{dev => com}/streamx/cli/framework/testing/AbstractTestCommand.java (91%) rename streamx-cli/src/test/java/{dev => com}/streamx/cli/framework/testing/TestObject.java (74%) rename streamx-cli/src/test/java/{dev => com}/streamx/cli/framework/testing/UnserializableObject.java (75%) delete mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandOutputOptionTest.java delete mode 100644 streamx-cli/src/test/java/dev/streamx/cli/framework/CommandResultTest.java diff --git a/streamx-cli/pom.xml b/streamx-cli/pom.xml index 236fc1ff..b04a62cd 100644 --- a/streamx-cli/pom.xml +++ b/streamx-cli/pom.xml @@ -5,7 +5,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - dev.streamx.cli + com.streamx.cli streamx-cli 2.0.0-SNAPSHOT diff --git a/streamx-cli/src/main/java/com/streamx/cli/Main.java b/streamx-cli/src/main/java/com/streamx/cli/Main.java index 7713c1ed..d3984860 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/Main.java +++ b/streamx-cli/src/main/java/com/streamx/cli/Main.java @@ -1,17 +1,17 @@ -package dev.streamx.cli.framework; +package com.streamx.cli; -import dev.streamx.cli.framework.cli.AbstractCommandGroup; -import dev.streamx.cli.framework.cli.CommandResult; -import dev.streamx.cli.framework.cli.ShortErrorMessageHandler; +import com.streamx.cli.framework.AbstractCommandGroup; +import com.streamx.cli.framework.CommandResult; +import com.streamx.cli.framework.ShortErrorMessageHandler; import io.quarkus.picocli.runtime.annotations.TopCommand; import picocli.CommandLine; @TopCommand @CommandLine.Command( - name = "streamx", - mixinStandardHelpOptions = true, - description = "StreamX CLI. More info at https://streamx.dev", - subcommands = {} + name = "streamx", + mixinStandardHelpOptions = true, + description = "StreamX CLI. More info at https://streamx.dev", + subcommands = {} ) public class Main extends AbstractCommandGroup { @CommandLine.Spec @@ -22,7 +22,7 @@ public CommandResult runCommand() throws RuntimeException { commandSpec .commandLine() .setParameterExceptionHandler(new ShortErrorMessageHandler()) - .usage(System.out); + .usage(System.out); return new CommandResult<>(null); } diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java index 4329390e..9b37640d 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java @@ -1,7 +1,12 @@ -package dev.streamx.cli.framework.cli; +package com.streamx.cli.framework; -import static dev.streamx.cli.framework.i18n.MessageProvider.msg; +import static com.streamx.cli.i18n.MessageProvider.msg; +import io.quarkus.runtime.Quarkus; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; import org.jetbrains.annotations.Nullable; import org.jline.reader.Completer; import org.jline.reader.LineReader; @@ -11,12 +16,6 @@ import org.jline.terminal.TerminalBuilder; import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; -import io.quarkus.runtime.Quarkus; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.List; /** * Each CLI command should extend this class. @@ -28,7 +27,8 @@ public abstract class AbstractCommand implements Runnable { public abstract CommandResult runCommand() throws RuntimeException; // Override this method to hide specific command line options. - // May be useful to hide the "--output" option for commands that don't print anything in case of success. + // May be useful to hide the "--output" option for + // commands that don't print anything in case of success. public List getHiddenOptions() { return List.of(); } @@ -59,15 +59,15 @@ public void setSpec(CommandSpec spec) { } @CommandLine.Option( - names = {CommonOption.VERBOSE_SHORT, CommonOption.VERBOSE_LONG}, - description = "Print debug information" + names = {CommonOption.VERBOSE_SHORT, CommonOption.VERBOSE_LONG}, + description = "Print debug information" ) public boolean verbose; @CommandLine.Option( - names = {CommonOption.OUTPUT_SHORT, CommonOption.OUTPUT_LONG}, - description = "Specify output format: text, json, yaml", - defaultValue = "text" + names = {CommonOption.OUTPUT_SHORT, CommonOption.OUTPUT_LONG}, + description = "Specify output format: text, json, yaml", + defaultValue = "text" ) // Explicitly set default value here as a fallback for commands with the hidden output option. public OutputFormat output = OutputFormat.text; @@ -82,10 +82,13 @@ protected Terminal createTerminal() throws IOException { } // Use this method for asking user input in interactive commands. - public String promptForInput(String prompt, @Nullable List autocompleteOptions) throws RuntimeException { + public String promptForInput( + String prompt, + @Nullable List autocompleteOptions + ) throws RuntimeException { try (Terminal terminal = createTerminal()) { LineReaderBuilder builder = LineReaderBuilder.builder() - .terminal(terminal); + .terminal(terminal); Completer completer = null; if (autocompleteOptions != null) { @@ -95,7 +98,8 @@ public String promptForInput(String prompt, @Nullable List autocompleteO LineReader reader = builder.build(); - return reader.readLine(completer == null ? prompt : prompt + " (TAB for autocomplete):").strip(); + return reader.readLine(completer == null ? prompt : prompt + " (TAB for autocomplete):") + .strip(); } catch (IOException e) { throw new RuntimeException(msg.failedToHandleInteractiveInput(), e); } diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java index b8452181..c52844e6 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java @@ -1,4 +1,4 @@ -package dev.streamx.cli.framework.cli; +package com.streamx.cli.framework; import java.util.List; diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java index 725d33ec..9672a689 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java @@ -1,4 +1,4 @@ -package dev.streamx.cli.framework.cli; +package com.streamx.cli.framework; import java.util.List; diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java b/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java index f1da69ad..59bbcad0 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java @@ -1,13 +1,12 @@ -package dev.streamx.cli.framework.cli; +package com.streamx.cli.framework; -import static dev.streamx.cli.framework.i18n.MessageProvider.msg; +import static com.streamx.cli.i18n.MessageProvider.msg; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; - import java.util.function.Function; /** @@ -20,7 +19,10 @@ public CommandResult(ResultT result) { this.result = result; } - public String toText(OutputFormat outputFormat, Function, String> textFormatter) throws RuntimeException { + public String toText( + OutputFormat outputFormat, + Function, String> textFormatter + ) throws RuntimeException { try { switch (outputFormat) { case OutputFormat.text -> { @@ -33,15 +35,14 @@ public String toText(OutputFormat outputFormat, Function, } case OutputFormat.yaml -> { var yamlFactory = YAMLFactory.builder() - .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) - .build(); + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) + .build(); ObjectMapper mapper = new ObjectMapper(yamlFactory); JsonNode jsonNode = mapper.valueToTree(result); return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode).strip(); } + default -> throw new RuntimeException(msg.unsupportedOutputFormat()); } - - throw new RuntimeException(msg.unsupportedOutputFormat()); } catch (JsonProcessingException e) { throw new RuntimeException(e); } diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/CommonOption.java b/streamx-cli/src/main/java/com/streamx/cli/framework/CommonOption.java index 4c7b1cc6..24b49fd8 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/CommonOption.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/CommonOption.java @@ -1,4 +1,4 @@ -package dev.streamx.cli.framework.cli; +package com.streamx.cli.framework; public class CommonOption { public static final String VERBOSE_SHORT = "-v"; diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/OutputFormat.java b/streamx-cli/src/main/java/com/streamx/cli/framework/OutputFormat.java index d62fcc85..d811ada5 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/OutputFormat.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/OutputFormat.java @@ -1,4 +1,4 @@ -package dev.streamx.cli.framework.cli; +package com.streamx.cli.framework; public enum OutputFormat { text, json, yaml diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java index 4b5886cf..8d5487b0 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java @@ -1,16 +1,14 @@ -package dev.streamx.cli.framework.cli; +package com.streamx.cli.framework; -import picocli.CommandLine; +import static com.streamx.cli.i18n.MessageProvider.msg; import java.io.PrintWriter; - +import picocli.CommandLine; import picocli.CommandLine.IParameterExceptionHandler; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.ParameterException; import picocli.CommandLine.UnmatchedArgumentException; -import static dev.streamx.cli.framework.i18n.MessageProvider.msg; - public class ShortErrorMessageHandler implements IParameterExceptionHandler { @Override @@ -30,7 +28,10 @@ static int shortErrorMessage(Exception ex, CommandLine cmd) { if (ex instanceof ParameterException || ex instanceof IllegalArgumentException) { CommandSpec spec = cmd.getCommandSpec(); - writer.printf(msg.tryForMoreInformationOnAvailableOptions(spec.qualifiedName(), "help".equals(spec.name()) ? "" : " --help")); + writer.printf(msg.tryForMoreInformationOnAvailableOptions( + spec.qualifiedName(), + "help".equals(spec.name()) ? "" : " --help" + )); return cmd.getCommandSpec().exitCodeOnInvalidInput(); } return cmd.getCommandSpec().exitCodeOnExecutionException(); diff --git a/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java b/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java index 759fe609..3b9e5bbb 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java +++ b/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java @@ -1,11 +1,10 @@ -package dev.streamx.cli.framework.i18n; +package com.streamx.cli.i18n; +import java.lang.invoke.MethodHandles; import org.jboss.logging.Messages; import org.jboss.logging.annotations.Message; import org.jboss.logging.annotations.MessageBundle; -import java.lang.invoke.MethodHandles; - @MessageBundle(projectCode = "STREAMXCLI") public interface MessageProvider { @@ -15,7 +14,10 @@ public interface MessageProvider { String unsupportedOutputFormat(); @Message(id = 101, value = "Try '%s%s' for more information on the available options.%n") - String tryForMoreInformationOnAvailableOptions(String qualifiedCommandName, String helpOptionName); + String tryForMoreInformationOnAvailableOptions( + String qualifiedCommandName, + String helpOptionName + ); @Message(id = 102, value = "Failed to handle interactive input") String failedToHandleInteractiveInput(); diff --git a/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java b/streamx-cli/src/test/java/com/streamx/cli/MainTest.java similarity index 63% rename from streamx-cli/src/test/java/dev/streamx/cli/MainTest.java rename to streamx-cli/src/test/java/com/streamx/cli/MainTest.java index 4323a03a..3d223bcd 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/MainTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/MainTest.java @@ -1,14 +1,12 @@ -package dev.streamx.cli.framework; +package com.streamx.cli; import static org.assertj.core.api.Assertions.assertThat; -import dev.streamx.cli.framework.cli.AbstractCommand; +import com.streamx.cli.framework.AbstractCommand; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; - import java.util.HashSet; import java.util.Set; - import org.junit.jupiter.api.Test; import picocli.CommandLine; @@ -26,15 +24,18 @@ void allCommandsAndSubcommandsShouldExtendAbstractCommand() { collectAllCommands(commandLine.getCommandSpec(), allCommandClasses); assertThat(allCommandClasses) - .as("All commands and subcommands should extend AbstractCommand") - .allSatisfy(commandClass -> - assertThat(AbstractCommand.class.isAssignableFrom(commandClass)) - .as("Command %s should extend AbstractCommand", commandClass.getName()) - .isTrue() - ); + .as("All commands and subcommands should extend AbstractCommand") + .allSatisfy(commandClass -> + assertThat(AbstractCommand.class.isAssignableFrom(commandClass)) + .as("Command %s should extend AbstractCommand", commandClass.getName()) + .isTrue() + ); } - private void collectAllCommands(CommandLine.Model.CommandSpec commandSpec, Set> commands) { + private void collectAllCommands( + CommandLine.Model.CommandSpec commandSpec, + Set> commands + ) { Class userObject = commandSpec.userObject().getClass(); commands.add(userObject); diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java new file mode 100644 index 00000000..6bb8779f --- /dev/null +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java @@ -0,0 +1,238 @@ +package com.streamx.cli.framework; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.streamx.cli.framework.testing.AbstractCommandBaseTest; +import com.streamx.cli.framework.testing.AbstractTestCommand; +import com.streamx.cli.framework.testing.TestObject; +import java.util.List; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +class AbstractCommandOutputOptionTest extends AbstractCommandBaseTest { + private final TestObject result = new TestObject( + null, + true, + 100500, + 42.42, + "Test string", + new TestObject( + null, + false, + 7, + 3.14, + "Nested object test string", + null, + null + ), + List.of( + new TestObject( + null, + true, + 15, + 100.42, + "Nested list object 1 test string", + null, + null + ), + new TestObject( + null, + false, + 18, + 0.42, + "Nested list object 2 test string", + null, + null + ) + ) + ); + + @Test + void textOutputFlag_FormatsOutputAsJsonIfNoCustomFormatterProvided() { + var command = new AbstractTestCommand<>(); + CommandLine commandLine = new CommandLine(command); + commandLine.parseArgs(CommonOption.OUTPUT_LONG, "text"); + command.setRunCommandHandler(() -> new CommandResult<>(result)); + command.execute(); + + var expectedStdOutOutput = """ + { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 100500, + "floatValue" : 42.42, + "stringValue" : "Test string", + "nestedObject" : { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 7, + "floatValue" : 3.14, + "stringValue" : "Nested object test string", + "nestedObject" : null, + "nestedObjects" : null + }, + "nestedObjects" : [ { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 15, + "floatValue" : 100.42, + "stringValue" : "Nested list object 1 test string", + "nestedObject" : null, + "nestedObjects" : null + }, { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 18, + "floatValue" : 0.42, + "stringValue" : "Nested list object 2 test string", + "nestedObject" : null, + "nestedObjects" : null + } ] + } + """.strip() + "\n"; + + assertEquals(expectedStdOutOutput, outStream.toString()); + assertEquals("", errStream.toString()); + } + + @Test + void textOutputFlag_FormatsWithCustomFormatter() { + var command = new AbstractTestCommand(); + CommandLine commandLine = new CommandLine(command); + commandLine.parseArgs(CommonOption.OUTPUT_LONG, "text"); + command.setRunCommandHandler(() -> new CommandResult<>(result)); + command.setGetTextOutputHandler((cr) -> """ + String value: %s, + Total nested objects: %d + """.formatted(cr.result.stringValue, cr.result.nestedObjects.size()).strip()); + + command.execute(); + + var expectedStdOutOutput = """ + String value: Test string, + Total nested objects: 2 + """.strip() + "\n"; + + assertEquals(expectedStdOutOutput, outStream.toString()); + assertEquals("", errStream.toString()); + } + + @Test + void jsonOutputFlag() { + var command = new AbstractTestCommand<>(); + CommandLine commandLine = new CommandLine(command); + commandLine.parseArgs(CommonOption.OUTPUT_LONG, "json"); + command.setRunCommandHandler(() -> new CommandResult<>(result)); + command.execute(); + + var expectedStdOutOutput = """ + { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 100500, + "floatValue" : 42.42, + "stringValue" : "Test string", + "nestedObject" : { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 7, + "floatValue" : 3.14, + "stringValue" : "Nested object test string", + "nestedObject" : null, + "nestedObjects" : null + }, + "nestedObjects" : [ { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 15, + "floatValue" : 100.42, + "stringValue" : "Nested list object 1 test string", + "nestedObject" : null, + "nestedObjects" : null + }, { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 18, + "floatValue" : 0.42, + "stringValue" : "Nested list object 2 test string", + "nestedObject" : null, + "nestedObjects" : null + } ] + } + """.strip() + "\n"; + + assertEquals(expectedStdOutOutput, outStream.toString()); + assertEquals("", errStream.toString()); + } + + @Test + void yamlOutputFlag() { + var command = new AbstractTestCommand(); + CommandLine commandLine = new CommandLine(command); + commandLine.parseArgs(CommonOption.OUTPUT_LONG, "yaml"); + command.setRunCommandHandler(() -> new CommandResult<>(result)); + command.execute(); + + var expectedStdOutOutput = """ + voidValue: null + booleanValue: true + longValue: 100500 + floatValue: 42.42 + stringValue: "Test string" + nestedObject: + voidValue: null + booleanValue: false + longValue: 7 + floatValue: 3.14 + stringValue: "Nested object test string" + nestedObject: null + nestedObjects: null + nestedObjects: + - voidValue: null + booleanValue: true + longValue: 15 + floatValue: 100.42 + stringValue: "Nested list object 1 test string" + nestedObject: null + nestedObjects: null + - voidValue: null + booleanValue: false + longValue: 18 + floatValue: 0.42 + stringValue: "Nested list object 2 test string" + nestedObject: null + nestedObjects: null + """.strip() + "\n"; + + assertEquals(expectedStdOutOutput, outStream.toString()); + assertEquals("", errStream.toString()); + } + + @Test + void voidResult() { + var command = new AbstractTestCommand(); + command.setRunCommandHandler(() -> new CommandResult<>(null)); + + command.output = OutputFormat.text; + command.execute(); + + assertEquals("null\n", outStream.toString()); + assertEquals("", errStream.toString()); + + restoreStreams(); + redirectStreams(); + command.output = OutputFormat.json; + command.execute(); + + assertEquals("null\n", outStream.toString()); + assertEquals("", errStream.toString()); + + restoreStreams(); + redirectStreams(); + command.output = OutputFormat.yaml; + command.execute(); + + assertEquals("null\n", outStream.toString()); + assertEquals("", errStream.toString()); + } +} diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java similarity index 79% rename from streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandTest.java rename to streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java index 7ba46da5..fa14d5aa 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java @@ -1,15 +1,18 @@ -package dev.streamx.cli.framework.cli; +package com.streamx.cli.framework; -import dev.streamx.cli.framework.cli.testing.AbstractCommandBaseTest; -import dev.streamx.cli.framework.cli.testing.AbstractTestCommand; -import dev.streamx.cli.framework.cli.testing.TestObject; -import org.junit.jupiter.api.Test; -import picocli.CommandLine; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.streamx.cli.framework.testing.AbstractCommandBaseTest; +import com.streamx.cli.framework.testing.AbstractTestCommand; +import com.streamx.cli.framework.testing.TestObject; import java.io.ByteArrayInputStream; import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; class AbstractCommandTest extends AbstractCommandBaseTest { @Test diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandVerboseOptionTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java similarity index 85% rename from streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandVerboseOptionTest.java rename to streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java index 7ed6ce9d..6d8bd10f 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandVerboseOptionTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java @@ -1,13 +1,13 @@ -package dev.streamx.cli.framework.cli; - -import dev.streamx.cli.framework.cli.testing.AbstractCommandBaseTest; -import dev.streamx.cli.framework.cli.testing.AbstractTestCommand; -import org.junit.jupiter.api.Test; -import picocli.CommandLine; +package com.streamx.cli.framework; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.streamx.cli.framework.testing.AbstractCommandBaseTest; +import com.streamx.cli.framework.testing.AbstractTestCommand; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + class AbstractCommandVerboseOptionTest extends AbstractCommandBaseTest { @Test void ifProvided_printsStackTrace() { diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractSilentCommandTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractSilentCommandTest.java similarity index 68% rename from streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractSilentCommandTest.java rename to streamx-cli/src/test/java/com/streamx/cli/framework/AbstractSilentCommandTest.java index cf458a16..f6cd35b9 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractSilentCommandTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractSilentCommandTest.java @@ -1,12 +1,13 @@ -package dev.streamx.cli.framework.cli; +package com.streamx.cli.framework; -import dev.streamx.cli.framework.cli.testing.AbstractCommandBaseTest; -import dev.streamx.cli.framework.cli.testing.AbstractSilentTestCommand; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.streamx.cli.framework.testing.AbstractCommandBaseTest; +import com.streamx.cli.framework.testing.AbstractSilentTestCommand; import org.junit.jupiter.api.Test; import picocli.CommandLine; -import static org.junit.jupiter.api.Assertions.*; - class AbstractSilentCommandTest extends AbstractCommandBaseTest { @Test void outputFlagIsAbsent() { diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java new file mode 100644 index 00000000..933547b0 --- /dev/null +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java @@ -0,0 +1,188 @@ +package com.streamx.cli.framework; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.streamx.cli.framework.testing.TestObject; +import com.streamx.cli.framework.testing.UnserializableObject; +import java.util.List; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +class CommandResultTest { + private final TestObject result = new TestObject( + null, + true, + 100500, + 42.42, + "Test string", + new TestObject( + null, + false, + 7, + 3.14, + "Nested object test string", + null, + null + ), + List.of( + new TestObject( + null, + true, + 15, + 100.42, + "Nested list object 1 test string", + null, + null + ), + new TestObject( + null, + false, + 18, + 0.42, + "Nested list object 2 test string", + null, + null + ) + ) + ); + private final CommandResult commandResult = new CommandResult<>(result); + + @Test + void toText_withTextFormat_shouldUseTextFormatter() { + var expectedOutput = """ + Void Value: null + Boolean Value: true + Long Value: 100500 + Float Value: 42.42 + String Value: Test string + Nested Object Long Value: 7 + Nested Object String Value: Nested object test string + Total Nested Objects: 2 + """; + + Function, String> textFormatter = + cr -> """ + Void Value: %s + Boolean Value: %b + Long Value: %d + Float Value: %.2f + String Value: %s + Nested Object Long Value: %d + Nested Object String Value: %s + Total Nested Objects: %d + """.formatted( + cr.result.voidValue, + cr.result.booleanValue, + cr.result.longValue, + cr.result.floatValue, + cr.result.stringValue, + cr.result.nestedObject.longValue, + cr.result.nestedObject.stringValue, + cr.result.nestedObjects.size() + ); + + var output = commandResult.toText(OutputFormat.text, textFormatter); + + assertEquals(expectedOutput, output); + } + + @Test + void toText_withJsonFormat_shouldReturnPrettyPrintedJson() { + var expectedOutput = """ + { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 100500, + "floatValue" : 42.42, + "stringValue" : "Test string", + "nestedObject" : { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 7, + "floatValue" : 3.14, + "stringValue" : "Nested object test string", + "nestedObject" : null, + "nestedObjects" : null + }, + "nestedObjects" : [ { + "voidValue" : null, + "booleanValue" : true, + "longValue" : 15, + "floatValue" : 100.42, + "stringValue" : "Nested list object 1 test string", + "nestedObject" : null, + "nestedObjects" : null + }, { + "voidValue" : null, + "booleanValue" : false, + "longValue" : 18, + "floatValue" : 0.42, + "stringValue" : "Nested list object 2 test string", + "nestedObject" : null, + "nestedObjects" : null + } ] + } + """.strip(); + + var output = commandResult.toText(OutputFormat.json, null); + + assertEquals(expectedOutput, output); + } + + @Test + void toText_withYamlFormat_shouldReturnYaml() { + var expectedOutput = """ + voidValue: null + booleanValue: true + longValue: 100500 + floatValue: 42.42 + stringValue: "Test string" + nestedObject: + voidValue: null + booleanValue: false + longValue: 7 + floatValue: 3.14 + stringValue: "Nested object test string" + nestedObject: null + nestedObjects: null + nestedObjects: + - voidValue: null + booleanValue: true + longValue: 15 + floatValue: 100.42 + stringValue: "Nested list object 1 test string" + nestedObject: null + nestedObjects: null + - voidValue: null + booleanValue: false + longValue: 18 + floatValue: 0.42 + stringValue: "Nested list object 2 test string" + nestedObject: null + nestedObjects: null + """.strip(); + + var output = commandResult.toText(OutputFormat.yaml, null); + + assertEquals(expectedOutput, output); + } + + @Test + void toText_withNullResult_shouldHandleGracefully() { + CommandResult commandResult = new CommandResult<>(null); + + assertEquals("null", commandResult.toText(OutputFormat.json, null)); + assertEquals("null", commandResult.toText(OutputFormat.yaml, null)); + } + + @Test + void toText_shouldThrowRuntimeExceptionForUnserializableObject() { + UnserializableObject unserializable = new UnserializableObject(); + CommandResult commandResult = new CommandResult<>(unserializable); + + assertThrows(RuntimeException.class, () -> + commandResult.toText(OutputFormat.json, null) + ); + } +} diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractCommandBaseTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractCommandBaseTest.java similarity index 93% rename from streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractCommandBaseTest.java rename to streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractCommandBaseTest.java index ed6d33f5..641c530a 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractCommandBaseTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractCommandBaseTest.java @@ -1,10 +1,9 @@ -package dev.streamx.cli.framework.cli.testing; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; +package com.streamx.cli.framework.testing; import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; public class AbstractCommandBaseTest { public final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractSilentTestCommand.java b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractSilentTestCommand.java similarity index 85% rename from streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractSilentTestCommand.java rename to streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractSilentTestCommand.java index 929a9da3..15998982 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractSilentTestCommand.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractSilentTestCommand.java @@ -1,8 +1,7 @@ -package dev.streamx.cli.framework.cli.testing; - -import dev.streamx.cli.framework.cli.AbstractSilentCommand; -import dev.streamx.cli.framework.cli.CommandResult; +package com.streamx.cli.framework.testing; +import com.streamx.cli.framework.AbstractSilentCommand; +import com.streamx.cli.framework.CommandResult; import java.util.List; import java.util.function.Supplier; @@ -14,6 +13,7 @@ public class AbstractSilentTestCommand extends AbstractSilentCommand { public void setRunCommandHandler(Supplier> handler) { this.runCommandHandler = handler; } + public void setHiddenOptionsHandler(Supplier> handler) { this.hiddenOptionsHandler = handler; } diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractTestCommand.java b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractTestCommand.java similarity index 91% rename from streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractTestCommand.java rename to streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractTestCommand.java index c9e80285..f8d467c6 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/AbstractTestCommand.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractTestCommand.java @@ -1,14 +1,13 @@ -package dev.streamx.cli.framework.cli.testing; - -import dev.streamx.cli.framework.cli.AbstractCommand; -import dev.streamx.cli.framework.cli.CommandResult; -import org.jline.terminal.Terminal; -import org.jline.terminal.TerminalBuilder; +package com.streamx.cli.framework.testing; +import com.streamx.cli.framework.AbstractCommand; +import com.streamx.cli.framework.CommandResult; import java.io.IOException; import java.util.List; import java.util.function.Function; import java.util.function.Supplier; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; // Helper class for testing AbstractCommand public class AbstractTestCommand extends AbstractCommand { diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/TestObject.java b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/TestObject.java similarity index 74% rename from streamx-cli/src/test/java/dev/streamx/cli/framework/testing/TestObject.java rename to streamx-cli/src/test/java/com/streamx/cli/framework/testing/TestObject.java index 0bd2bb8e..cf46569d 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/TestObject.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/TestObject.java @@ -1,8 +1,7 @@ -package dev.streamx.cli.framework.cli.testing; - -import org.instancio.Instancio; +package com.streamx.cli.framework.testing; import java.util.List; +import org.instancio.Instancio; public class TestObject { public Void voidValue; @@ -14,13 +13,13 @@ public class TestObject { public List nestedObjects; public TestObject( - Void voidValue, - boolean booleanValue, - long longValue, - double floatValue, - String stringValue, - TestObject nestedObject, - List nestedObjects + Void voidValue, + boolean booleanValue, + long longValue, + double floatValue, + String stringValue, + TestObject nestedObject, + List nestedObjects ) { this.voidValue = voidValue; this.booleanValue = booleanValue; diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/UnserializableObject.java b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/UnserializableObject.java similarity index 75% rename from streamx-cli/src/test/java/dev/streamx/cli/framework/testing/UnserializableObject.java rename to streamx-cli/src/test/java/com/streamx/cli/framework/testing/UnserializableObject.java index 5b09a958..35dde7bc 100644 --- a/streamx-cli/src/test/java/dev/streamx/cli/framework/testing/UnserializableObject.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/UnserializableObject.java @@ -1,4 +1,4 @@ -package dev.streamx.cli.framework.cli.testing; +package com.streamx.cli.framework.testing; public class UnserializableObject { // Object with circular reference to make it unserializable diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandOutputOptionTest.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandOutputOptionTest.java deleted file mode 100644 index 62ea958c..00000000 --- a/streamx-cli/src/test/java/dev/streamx/cli/framework/AbstractCommandOutputOptionTest.java +++ /dev/null @@ -1,239 +0,0 @@ -package dev.streamx.cli.framework.cli; - -import static org.junit.jupiter.api.Assertions.*; - -import dev.streamx.cli.framework.cli.testing.AbstractCommandBaseTest; -import dev.streamx.cli.framework.cli.testing.AbstractTestCommand; -import dev.streamx.cli.framework.cli.testing.TestObject; -import org.junit.jupiter.api.Test; -import picocli.CommandLine; - -import java.util.List; - -class AbstractCommandOutputOptionTest extends AbstractCommandBaseTest { - private final TestObject result = new TestObject( - null, - true, - 100500, - 42.42, - "Test string", - new TestObject( - null, - false, - 7, - 3.14, - "Nested object test string", - null, - null - ), - List.of( - new TestObject( - null, - true, - 15, - 100.42, - "Nested list object 1 test string", - null, - null - ), - new TestObject( - null, - false, - 18, - 0.42, - "Nested list object 2 test string", - null, - null - ) - ) - ); - - @Test - void textOutputFlag_FormatsOutputAsJsonIfNoCustomFormatterProvided() { - var command = new AbstractTestCommand<>(); - CommandLine commandLine = new CommandLine(command); - commandLine.parseArgs(CommonOption.OUTPUT_LONG, "text"); - command.setRunCommandHandler(() -> new CommandResult<>(result)); - command.execute(); - - var expectedStdOutOutput = """ - { - "voidValue" : null, - "booleanValue" : true, - "longValue" : 100500, - "floatValue" : 42.42, - "stringValue" : "Test string", - "nestedObject" : { - "voidValue" : null, - "booleanValue" : false, - "longValue" : 7, - "floatValue" : 3.14, - "stringValue" : "Nested object test string", - "nestedObject" : null, - "nestedObjects" : null - }, - "nestedObjects" : [ { - "voidValue" : null, - "booleanValue" : true, - "longValue" : 15, - "floatValue" : 100.42, - "stringValue" : "Nested list object 1 test string", - "nestedObject" : null, - "nestedObjects" : null - }, { - "voidValue" : null, - "booleanValue" : false, - "longValue" : 18, - "floatValue" : 0.42, - "stringValue" : "Nested list object 2 test string", - "nestedObject" : null, - "nestedObjects" : null - } ] - } - """.strip() + "\n"; - - assertEquals(expectedStdOutOutput, outStream.toString()); - assertEquals("", errStream.toString()); - } - - @Test - void textOutputFlag_FormatsWithCustomFormatter() { - var command = new AbstractTestCommand(); - CommandLine commandLine = new CommandLine(command); - commandLine.parseArgs(CommonOption.OUTPUT_LONG, "text"); - command.setRunCommandHandler(() -> new CommandResult<>(result)); - command.setGetTextOutputHandler((cr) -> """ - String value: %s, - Total nested objects: %d - """.formatted(cr.result.stringValue, cr.result.nestedObjects.size()).strip()); - - command.execute(); - - var expectedStdOutOutput = """ - String value: Test string, - Total nested objects: 2 - """.strip() + "\n"; - - assertEquals(expectedStdOutOutput, outStream.toString()); - assertEquals("", errStream.toString()); - } - - @Test - void jsonOutputFlag() { - var command = new AbstractTestCommand<>(); - CommandLine commandLine = new CommandLine(command); - commandLine.parseArgs(CommonOption.OUTPUT_LONG, "json"); - command.setRunCommandHandler(() -> new CommandResult<>(result)); - command.execute(); - - var expectedStdOutOutput = """ - { - "voidValue" : null, - "booleanValue" : true, - "longValue" : 100500, - "floatValue" : 42.42, - "stringValue" : "Test string", - "nestedObject" : { - "voidValue" : null, - "booleanValue" : false, - "longValue" : 7, - "floatValue" : 3.14, - "stringValue" : "Nested object test string", - "nestedObject" : null, - "nestedObjects" : null - }, - "nestedObjects" : [ { - "voidValue" : null, - "booleanValue" : true, - "longValue" : 15, - "floatValue" : 100.42, - "stringValue" : "Nested list object 1 test string", - "nestedObject" : null, - "nestedObjects" : null - }, { - "voidValue" : null, - "booleanValue" : false, - "longValue" : 18, - "floatValue" : 0.42, - "stringValue" : "Nested list object 2 test string", - "nestedObject" : null, - "nestedObjects" : null - } ] - } - """.strip() + "\n"; - - assertEquals(expectedStdOutOutput, outStream.toString()); - assertEquals("", errStream.toString()); - } - - @Test - void yamlOutputFlag() { - var command = new AbstractTestCommand(); - CommandLine commandLine = new CommandLine(command); - commandLine.parseArgs(CommonOption.OUTPUT_LONG, "yaml"); - command.setRunCommandHandler(() -> new CommandResult<>(result)); - command.execute(); - - var expectedStdOutOutput = """ - voidValue: null - booleanValue: true - longValue: 100500 - floatValue: 42.42 - stringValue: "Test string" - nestedObject: - voidValue: null - booleanValue: false - longValue: 7 - floatValue: 3.14 - stringValue: "Nested object test string" - nestedObject: null - nestedObjects: null - nestedObjects: - - voidValue: null - booleanValue: true - longValue: 15 - floatValue: 100.42 - stringValue: "Nested list object 1 test string" - nestedObject: null - nestedObjects: null - - voidValue: null - booleanValue: false - longValue: 18 - floatValue: 0.42 - stringValue: "Nested list object 2 test string" - nestedObject: null - nestedObjects: null - """.strip() + "\n"; - - assertEquals(expectedStdOutOutput, outStream.toString()); - assertEquals("", errStream.toString()); - } - - @Test - void voidResult() { - var command = new AbstractTestCommand(); - command.setRunCommandHandler(() -> new CommandResult<>(null)); - - command.output = OutputFormat.text; - command.execute(); - - assertEquals("null\n", outStream.toString()); - assertEquals("", errStream.toString()); - - restoreStreams(); - redirectStreams(); - command.output = OutputFormat.json; - command.execute(); - - assertEquals("null\n", outStream.toString()); - assertEquals("", errStream.toString()); - - restoreStreams(); - redirectStreams(); - command.output = OutputFormat.yaml; - command.execute(); - - assertEquals("null\n", outStream.toString()); - assertEquals("", errStream.toString()); - } -} diff --git a/streamx-cli/src/test/java/dev/streamx/cli/framework/CommandResultTest.java b/streamx-cli/src/test/java/dev/streamx/cli/framework/CommandResultTest.java deleted file mode 100644 index 8ba3518a..00000000 --- a/streamx-cli/src/test/java/dev/streamx/cli/framework/CommandResultTest.java +++ /dev/null @@ -1,188 +0,0 @@ -package dev.streamx.cli.framework.cli; - -import static org.junit.jupiter.api.Assertions.*; - -import dev.streamx.cli.framework.cli.testing.TestObject; -import dev.streamx.cli.framework.cli.testing.UnserializableObject; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.function.Function; - -class CommandResultTest { - private final TestObject result = new TestObject( - null, - true, - 100500, - 42.42, - "Test string", - new TestObject( - null, - false, - 7, - 3.14, - "Nested object test string", - null, - null - ), - List.of( - new TestObject( - null, - true, - 15, - 100.42, - "Nested list object 1 test string", - null, - null - ), - new TestObject( - null, - false, - 18, - 0.42, - "Nested list object 2 test string", - null, - null - ) - ) - ); - private final CommandResult commandResult = new CommandResult<>(result); - - @Test - void toText_withTextFormat_shouldUseTextFormatter() { - var expectedOutput = """ - Void Value: null - Boolean Value: true - Long Value: 100500 - Float Value: 42.42 - String Value: Test string - Nested Object Long Value: 7 - Nested Object String Value: Nested object test string - Total Nested Objects: 2 - """; - - Function, String> textFormatter = - cr -> """ - Void Value: %s - Boolean Value: %b - Long Value: %d - Float Value: %.2f - String Value: %s - Nested Object Long Value: %d - Nested Object String Value: %s - Total Nested Objects: %d - """.formatted( - cr.result.voidValue, - cr.result.booleanValue, - cr.result.longValue, - cr.result.floatValue, - cr.result.stringValue, - cr.result.nestedObject.longValue, - cr.result.nestedObject.stringValue, - cr.result.nestedObjects.size() - ); - - var output = commandResult.toText(OutputFormat.text, textFormatter); - - assertEquals(expectedOutput, output); - } - - @Test - void toText_withJsonFormat_shouldReturnPrettyPrintedJson() { - var expectedOutput = """ - { - "voidValue" : null, - "booleanValue" : true, - "longValue" : 100500, - "floatValue" : 42.42, - "stringValue" : "Test string", - "nestedObject" : { - "voidValue" : null, - "booleanValue" : false, - "longValue" : 7, - "floatValue" : 3.14, - "stringValue" : "Nested object test string", - "nestedObject" : null, - "nestedObjects" : null - }, - "nestedObjects" : [ { - "voidValue" : null, - "booleanValue" : true, - "longValue" : 15, - "floatValue" : 100.42, - "stringValue" : "Nested list object 1 test string", - "nestedObject" : null, - "nestedObjects" : null - }, { - "voidValue" : null, - "booleanValue" : false, - "longValue" : 18, - "floatValue" : 0.42, - "stringValue" : "Nested list object 2 test string", - "nestedObject" : null, - "nestedObjects" : null - } ] - } - """.strip(); - - var output = commandResult.toText(OutputFormat.json, null); - - assertEquals(expectedOutput, output); - } - - @Test - void toText_withYamlFormat_shouldReturnYaml() { - var expectedOutput = """ - voidValue: null - booleanValue: true - longValue: 100500 - floatValue: 42.42 - stringValue: "Test string" - nestedObject: - voidValue: null - booleanValue: false - longValue: 7 - floatValue: 3.14 - stringValue: "Nested object test string" - nestedObject: null - nestedObjects: null - nestedObjects: - - voidValue: null - booleanValue: true - longValue: 15 - floatValue: 100.42 - stringValue: "Nested list object 1 test string" - nestedObject: null - nestedObjects: null - - voidValue: null - booleanValue: false - longValue: 18 - floatValue: 0.42 - stringValue: "Nested list object 2 test string" - nestedObject: null - nestedObjects: null - """.strip(); - - var output = commandResult.toText(OutputFormat.yaml, null); - - assertEquals(expectedOutput, output); - } - - @Test - void toText_withNullResult_shouldHandleGracefully() { - CommandResult commandResult = new CommandResult<>(null); - - assertEquals("null", commandResult.toText(OutputFormat.json, null)); - assertEquals("null", commandResult.toText(OutputFormat.yaml, null)); - } - - @Test - void toText_shouldThrowRuntimeExceptionForUnserializableObject() { - UnserializableObject unserializable = new UnserializableObject(); - CommandResult commandResult = new CommandResult<>(unserializable); - - assertThrows(RuntimeException.class, () -> - commandResult.toText(OutputFormat.json, null) - ); - } -} From 574e198fb17d16be2d6da26d3d9df1da1994ebb1 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Fri, 23 Jan 2026 13:04:04 +0300 Subject: [PATCH 11/21] Avoid using var keyword accrding to PR review --- .../streamx/cli/framework/AbstractCommand.java | 7 ++++--- .../streamx/cli/framework/CommandResult.java | 2 +- .../AbstractCommandOutputOptionTest.java | 18 +++++++++--------- .../cli/framework/AbstractCommandTest.java | 14 +++++++------- .../AbstractCommandVerboseOptionTest.java | 6 +++--- .../framework/AbstractSilentCommandTest.java | 4 ++-- .../cli/framework/CommandResultTest.java | 12 ++++++------ 7 files changed, 32 insertions(+), 31 deletions(-) diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java index 9b37640d..5b81c21f 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java @@ -16,6 +16,7 @@ import org.jline.terminal.TerminalBuilder; import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Model.OptionSpec; /** * Each CLI command should extend this class. @@ -39,10 +40,10 @@ public String getTextOutput(CommandResult result) throws RuntimeExcepti } private void applyHiddenOptions() { - var options = getHiddenOptions(); + List options = getHiddenOptions(); for (String option : options) { - var optionSpec = spec.findOption(option); + OptionSpec optionSpec = spec.findOption(option); if (optionSpec != null) { spec.remove(optionSpec); } @@ -109,7 +110,7 @@ public int execute() { int exitCode = 0; try { - var textOutput = this.runCommand().toText(output, this::getTextOutput); + String textOutput = this.runCommand().toText(output, this::getTextOutput); if (!textOutput.isEmpty()) { System.out.println(textOutput); } diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java b/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java index 59bbcad0..9614a0ac 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java @@ -34,7 +34,7 @@ public String toText( return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode); } case OutputFormat.yaml -> { - var yamlFactory = YAMLFactory.builder() + YAMLFactory yamlFactory = YAMLFactory.builder() .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) .build(); ObjectMapper mapper = new ObjectMapper(yamlFactory); diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java index 6bb8779f..83e9e286 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java @@ -49,13 +49,13 @@ class AbstractCommandOutputOptionTest extends AbstractCommandBaseTest { @Test void textOutputFlag_FormatsOutputAsJsonIfNoCustomFormatterProvided() { - var command = new AbstractTestCommand<>(); + AbstractTestCommand command = new AbstractTestCommand<>(); CommandLine commandLine = new CommandLine(command); commandLine.parseArgs(CommonOption.OUTPUT_LONG, "text"); command.setRunCommandHandler(() -> new CommandResult<>(result)); command.execute(); - var expectedStdOutOutput = """ + String expectedStdOutOutput = """ { "voidValue" : null, "booleanValue" : true, @@ -97,7 +97,7 @@ void textOutputFlag_FormatsOutputAsJsonIfNoCustomFormatterProvided() { @Test void textOutputFlag_FormatsWithCustomFormatter() { - var command = new AbstractTestCommand(); + AbstractTestCommand command = new AbstractTestCommand(); CommandLine commandLine = new CommandLine(command); commandLine.parseArgs(CommonOption.OUTPUT_LONG, "text"); command.setRunCommandHandler(() -> new CommandResult<>(result)); @@ -108,7 +108,7 @@ void textOutputFlag_FormatsWithCustomFormatter() { command.execute(); - var expectedStdOutOutput = """ + String expectedStdOutOutput = """ String value: Test string, Total nested objects: 2 """.strip() + "\n"; @@ -119,13 +119,13 @@ void textOutputFlag_FormatsWithCustomFormatter() { @Test void jsonOutputFlag() { - var command = new AbstractTestCommand<>(); + AbstractTestCommand command = new AbstractTestCommand<>(); CommandLine commandLine = new CommandLine(command); commandLine.parseArgs(CommonOption.OUTPUT_LONG, "json"); command.setRunCommandHandler(() -> new CommandResult<>(result)); command.execute(); - var expectedStdOutOutput = """ + String expectedStdOutOutput = """ { "voidValue" : null, "booleanValue" : true, @@ -167,13 +167,13 @@ void jsonOutputFlag() { @Test void yamlOutputFlag() { - var command = new AbstractTestCommand(); + AbstractTestCommand command = new AbstractTestCommand<>(); CommandLine commandLine = new CommandLine(command); commandLine.parseArgs(CommonOption.OUTPUT_LONG, "yaml"); command.setRunCommandHandler(() -> new CommandResult<>(result)); command.execute(); - var expectedStdOutOutput = """ + String expectedStdOutOutput = """ voidValue: null booleanValue: true longValue: 100500 @@ -210,7 +210,7 @@ void yamlOutputFlag() { @Test void voidResult() { - var command = new AbstractTestCommand(); + AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> new CommandResult<>(null)); command.output = OutputFormat.text; diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java index fa14d5aa..1320e19d 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java @@ -17,10 +17,10 @@ class AbstractCommandTest extends AbstractCommandBaseTest { @Test void execute_success() { - var command = new AbstractTestCommand(); + AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> new CommandResult<>(TestObject.random())); new CommandLine(command); - var exitCode = command.execute(); + int exitCode = command.execute(); assertEquals(0, exitCode); assertFalse(outStream.toString().isEmpty()); @@ -29,12 +29,12 @@ void execute_success() { @Test void execute_fail() { - var command = new AbstractTestCommand(); + AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> { throw new RuntimeException("Test exception"); }); new CommandLine(command); - var exitCode = command.execute(); + int exitCode = command.execute(); assertEquals(1, exitCode); assertTrue(outStream.toString().isEmpty()); @@ -43,12 +43,12 @@ void execute_fail() { @Test void getHiddenOptionsOverride() { - var command1 = new AbstractTestCommand<>(); + AbstractTestCommand command1 = new AbstractTestCommand<>(); new CommandLine(command1); assertNotNull(command1.spec.findOption(CommonOption.OUTPUT_LONG)); - var command2 = new AbstractTestCommand<>(); + AbstractTestCommand command2 = new AbstractTestCommand<>(); command2.setHiddenOptionsHandler(() -> List.of(CommonOption.OUTPUT_LONG)); new CommandLine(command2); @@ -57,7 +57,7 @@ void getHiddenOptionsOverride() { @Test void testPromptForInputMultipleCalls() { - var command = new AbstractTestCommand<>(); + AbstractTestCommand command = new AbstractTestCommand<>(); CommandLine commandLine = new CommandLine(command); command.setSpec(commandLine.getCommandSpec()); diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java index 6d8bd10f..42444552 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java @@ -11,11 +11,11 @@ class AbstractCommandVerboseOptionTest extends AbstractCommandBaseTest { @Test void ifProvided_printsStackTrace() { - var command = new AbstractTestCommand<>(); + AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> { throw new RuntimeException("Test exception"); }); - var commandLine = new CommandLine(command); + CommandLine commandLine = new CommandLine(command); commandLine.parseArgs(CommonOption.VERBOSE_LONG); command.execute(); @@ -25,7 +25,7 @@ void ifProvided_printsStackTrace() { @Test void ifNotProvided_doesntPrintStackTrace() { - var command = new AbstractTestCommand<>(); + AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> { throw new RuntimeException("Test exception"); }); diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractSilentCommandTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractSilentCommandTest.java index f6cd35b9..5cc428c5 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractSilentCommandTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractSilentCommandTest.java @@ -11,7 +11,7 @@ class AbstractSilentCommandTest extends AbstractCommandBaseTest { @Test void outputFlagIsAbsent() { - var command = new AbstractSilentTestCommand(); + AbstractSilentTestCommand command = new AbstractSilentTestCommand(); new CommandLine(command); // Trigger all PicocLi initialization assertNull(command.spec.findOption(CommonOption.OUTPUT_LONG)); @@ -19,7 +19,7 @@ void outputFlagIsAbsent() { @Test void outputIsEmpty() { - var command = new AbstractSilentTestCommand(); + AbstractSilentTestCommand command = new AbstractSilentTestCommand(); command.setRunCommandHandler(() -> new CommandResult<>(null)); command.execute(); diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java index 933547b0..458c67d7 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java @@ -50,7 +50,7 @@ class CommandResultTest { @Test void toText_withTextFormat_shouldUseTextFormatter() { - var expectedOutput = """ + String expectedOutput = """ Void Value: null Boolean Value: true Long Value: 100500 @@ -82,14 +82,14 @@ void toText_withTextFormat_shouldUseTextFormatter() { cr.result.nestedObjects.size() ); - var output = commandResult.toText(OutputFormat.text, textFormatter); + String output = commandResult.toText(OutputFormat.text, textFormatter); assertEquals(expectedOutput, output); } @Test void toText_withJsonFormat_shouldReturnPrettyPrintedJson() { - var expectedOutput = """ + String expectedOutput = """ { "voidValue" : null, "booleanValue" : true, @@ -125,14 +125,14 @@ void toText_withJsonFormat_shouldReturnPrettyPrintedJson() { } """.strip(); - var output = commandResult.toText(OutputFormat.json, null); + String output = commandResult.toText(OutputFormat.json, null); assertEquals(expectedOutput, output); } @Test void toText_withYamlFormat_shouldReturnYaml() { - var expectedOutput = """ + String expectedOutput = """ voidValue: null booleanValue: true longValue: 100500 @@ -163,7 +163,7 @@ void toText_withYamlFormat_shouldReturnYaml() { nestedObjects: null """.strip(); - var output = commandResult.toText(OutputFormat.yaml, null); + String output = commandResult.toText(OutputFormat.yaml, null); assertEquals(expectedOutput, output); } From 3bfded465675b0d372afd4950c7641774a2795c6 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Fri, 23 Jan 2026 13:05:55 +0300 Subject: [PATCH 12/21] Remove TAB to autocomplete message according to PR review --- .../main/java/com/streamx/cli/framework/AbstractCommand.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java index 5b81c21f..d8afdf73 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java @@ -99,8 +99,7 @@ public String promptForInput( LineReader reader = builder.build(); - return reader.readLine(completer == null ? prompt : prompt + " (TAB for autocomplete):") - .strip(); + return reader.readLine(prompt).strip(); } catch (IOException e) { throw new RuntimeException(msg.failedToHandleInteractiveInput(), e); } From 69b4ce205f8f812a0d3d4567ce223b1453f32da8 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Fri, 23 Jan 2026 13:10:26 +0300 Subject: [PATCH 13/21] Move AbstractCommand fields to the top according to PR review --- .../cli/framework/AbstractCommand.java | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java index d8afdf73..618167f2 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java @@ -24,6 +24,29 @@ * @param Must be serializable by Jackson (POJO, JsonSerializable, etc.) */ public abstract class AbstractCommand implements Runnable { + @CommandLine.Spec + public CommandSpec spec; + + @CommandLine.Spec + public void setSpec(CommandSpec spec) { + this.spec = spec; + applyHiddenOptions(); + } + + @CommandLine.Option( + names = {CommonOption.VERBOSE_SHORT, CommonOption.VERBOSE_LONG}, + description = "Print debug information" + ) + public boolean verbose; + + @CommandLine.Option( + names = {CommonOption.OUTPUT_SHORT, CommonOption.OUTPUT_LONG}, + description = "Specify output format: text, json, yaml", + defaultValue = "text" + ) + // Explicitly set default value here as a fallback for commands with the hidden output option. + public OutputFormat output = OutputFormat.text; + // Override this method to implement the command logic. public abstract CommandResult runCommand() throws RuntimeException; @@ -50,38 +73,10 @@ private void applyHiddenOptions() { } } - @CommandLine.Spec - public CommandSpec spec; - - @CommandLine.Spec - public void setSpec(CommandSpec spec) { - this.spec = spec; - applyHiddenOptions(); - } - - @CommandLine.Option( - names = {CommonOption.VERBOSE_SHORT, CommonOption.VERBOSE_LONG}, - description = "Print debug information" - ) - public boolean verbose; - - @CommandLine.Option( - names = {CommonOption.OUTPUT_SHORT, CommonOption.OUTPUT_LONG}, - description = "Specify output format: text, json, yaml", - defaultValue = "text" - ) - // Explicitly set default value here as a fallback for commands with the hidden output option. - public OutputFormat output = OutputFormat.text; - public void printUsage() { spec.commandLine().usage(System.out); } - // For testing purposes mostly. - protected Terminal createTerminal() throws IOException { - return TerminalBuilder.builder().system(true).build(); - } - // Use this method for asking user input in interactive commands. public String promptForInput( String prompt, @@ -131,4 +126,9 @@ public void run() { int exitCode = execute(); Quarkus.asyncExit(exitCode); } + + // For testing purposes mostly. + protected Terminal createTerminal() throws IOException { + return TerminalBuilder.builder().system(true).build(); + } } From 7a50cef995810fa46303734198e8c3b6bd433db1 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Fri, 23 Jan 2026 13:11:23 +0300 Subject: [PATCH 14/21] Remove .editorconfig according to PR review --- .editorconfig | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 7ebee5de..00000000 --- a/.editorconfig +++ /dev/null @@ -1,8 +0,0 @@ -[*] -charset = utf-8 -end_of_line = lf -indent_size = 2 -indent_style = space -insert_final_newline = true -max_line_length = 100 -tab_width = 2 \ No newline at end of file From 0d969becb922c10bac120c006994be93c3722938 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Mon, 26 Jan 2026 09:16:44 +0300 Subject: [PATCH 15/21] Show friendly error message on unchecked exceptions --- .../src/main/java/com/streamx/cli/Main.java | 3 +- .../cli/framework/AbstractCommand.java | 11 ++++--- .../cli/framework/AbstractCommandGroup.java | 2 +- .../cli/framework/AbstractSilentCommand.java | 2 +- .../streamx/cli/framework/CliException.java | 11 +++++++ .../streamx/cli/framework/CommandResult.java | 11 +++---- .../framework/ShortErrorMessageHandler.java | 19 +++++++++-- .../cli/framework/ThrowingFunction.java | 6 ++++ .../cli/framework/ThrowingFunction1.java | 6 ++++ .../com/streamx/cli/i18n/MessageProvider.java | 3 ++ .../cli/framework/AbstractCommandTest.java | 16 ++++++++-- .../AbstractCommandVerboseOptionTest.java | 13 +++++--- .../cli/framework/CommandResultTest.java | 32 +++++++++++++++---- .../testing/AbstractSilentTestCommand.java | 3 +- .../testing/AbstractTestCommand.java | 22 +++++++------ 15 files changed, 120 insertions(+), 40 deletions(-) create mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/CliException.java create mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction.java create mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction1.java diff --git a/streamx-cli/src/main/java/com/streamx/cli/Main.java b/streamx-cli/src/main/java/com/streamx/cli/Main.java index d3984860..f07b0c50 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/Main.java +++ b/streamx-cli/src/main/java/com/streamx/cli/Main.java @@ -1,6 +1,7 @@ package com.streamx.cli; import com.streamx.cli.framework.AbstractCommandGroup; +import com.streamx.cli.framework.CliException; import com.streamx.cli.framework.CommandResult; import com.streamx.cli.framework.ShortErrorMessageHandler; import io.quarkus.picocli.runtime.annotations.TopCommand; @@ -18,7 +19,7 @@ public class Main extends AbstractCommandGroup { CommandLine.Model.CommandSpec commandSpec; @Override - public CommandResult runCommand() throws RuntimeException { + public CommandResult runCommand() { commandSpec .commandLine() .setParameterExceptionHandler(new ShortErrorMessageHandler()) diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java index 618167f2..d53cae26 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java @@ -48,7 +48,7 @@ public void setSpec(CommandSpec spec) { public OutputFormat output = OutputFormat.text; // Override this method to implement the command logic. - public abstract CommandResult runCommand() throws RuntimeException; + public abstract CommandResult runCommand() throws CliException; // Override this method to hide specific command line options. // May be useful to hide the "--output" option for @@ -58,7 +58,7 @@ public List getHiddenOptions() { } // Override this method to provide human-readable output. - public String getTextOutput(CommandResult result) throws RuntimeException { + public String getTextOutput(CommandResult result) throws CliException { return result.toText(OutputFormat.json, null); } @@ -81,7 +81,7 @@ public void printUsage() { public String promptForInput( String prompt, @Nullable List autocompleteOptions - ) throws RuntimeException { + ) throws CliException { try (Terminal terminal = createTerminal()) { LineReaderBuilder builder = LineReaderBuilder.builder() .terminal(terminal); @@ -96,7 +96,7 @@ public String promptForInput( return reader.readLine(prompt).strip(); } catch (IOException e) { - throw new RuntimeException(msg.failedToHandleInteractiveInput(), e); + throw new CliException(msg.failedToHandleInteractiveInput(), e); } } @@ -104,7 +104,8 @@ public int execute() { int exitCode = 0; try { - String textOutput = this.runCommand().toText(output, this::getTextOutput); + CommandResult result = this.runCommand(); + String textOutput = result.toText(output, this::getTextOutput); if (!textOutput.isEmpty()) { System.out.println(textOutput); } diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java index c52844e6..64536801 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommandGroup.java @@ -11,7 +11,7 @@ public CommandResult runCommand() { } @Override - public String getTextOutput(CommandResult result) throws RuntimeException { + public String getTextOutput(CommandResult result) { return ""; } diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java index 9672a689..73253eca 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractSilentCommand.java @@ -10,7 +10,7 @@ public List getHiddenOptions() { } @Override - public String getTextOutput(CommandResult result) throws RuntimeException { + public String getTextOutput(CommandResult result) { return ""; } } diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/CliException.java b/streamx-cli/src/main/java/com/streamx/cli/framework/CliException.java new file mode 100644 index 00000000..347d9dd6 --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/CliException.java @@ -0,0 +1,11 @@ +package com.streamx.cli.framework; + +public class CliException extends Exception { + public CliException(String userFriendlyMessage) { + super(userFriendlyMessage); + } + + public CliException(String userFriendlyMessage, Throwable cause) { + super(userFriendlyMessage, cause); + } +} diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java b/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java index 9614a0ac..6f942074 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; -import java.util.function.Function; /** * @param Must be serializable by Jackson (POJO, JsonSerializable, etc.) @@ -21,12 +20,12 @@ public CommandResult(ResultT result) { public String toText( OutputFormat outputFormat, - Function, String> textFormatter - ) throws RuntimeException { + ThrowingFunction1, String, CliException> textFormatter + ) throws CliException { try { switch (outputFormat) { case OutputFormat.text -> { - return textFormatter.apply(this); + return textFormatter.get(this); } case OutputFormat.json -> { ObjectMapper mapper = new ObjectMapper(); @@ -41,10 +40,10 @@ public String toText( JsonNode jsonNode = mapper.valueToTree(result); return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode).strip(); } - default -> throw new RuntimeException(msg.unsupportedOutputFormat()); + default -> throw new CliException(msg.unsupportedOutputFormat()); } } catch (JsonProcessingException e) { - throw new RuntimeException(e); + throw new CliException(msg.unableToSerializeJson(), e); } } } diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java index 8d5487b0..5969cae2 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java @@ -19,9 +19,24 @@ public int handleParseException(ParameterException ex, String[] args) { static int shortErrorMessage(Exception ex, CommandLine cmd) { PrintWriter writer = cmd.getErr(); - String errorMessage = ex.getMessage(); - writer.println(cmd.getColorScheme().errorText(errorMessage)); + if (ex instanceof ParameterException || ex instanceof IllegalArgumentException || ex instanceof CliException) { + writer.println(cmd.getColorScheme().errorText(ex.getMessage())); + } else { + String errorMessage = """ + ❌ Something went wrong while running the command. + + Please try again with `--verbose` for more details. + If the problem persists, report it here: + https://github.com/streamx-com/streamx-cli/issues + + You can also contact us at: + https://www.streamx.dev/contact-us.html + """.strip(); + + writer.println(cmd.getColorScheme().errorText(errorMessage)); + } + if (ex instanceof ParameterException) { UnmatchedArgumentException.printSuggestions((ParameterException) ex, writer); } diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction.java b/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction.java new file mode 100644 index 00000000..7bec16d2 --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction.java @@ -0,0 +1,6 @@ +package com.streamx.cli.framework; + +@FunctionalInterface +public interface ThrowingFunction { + ResultT get() throws ExceptionT; +} \ No newline at end of file diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction1.java b/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction1.java new file mode 100644 index 00000000..df083d6b --- /dev/null +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction1.java @@ -0,0 +1,6 @@ +package com.streamx.cli.framework; + +@FunctionalInterface +public interface ThrowingFunction1 { + ResultT get(InputT input) throws ExceptionT; +} \ No newline at end of file diff --git a/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java b/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java index 3b9e5bbb..c24ba929 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java +++ b/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java @@ -21,4 +21,7 @@ String tryForMoreInformationOnAvailableOptions( @Message(id = 102, value = "Failed to handle interactive input") String failedToHandleInteractiveInput(); + + @Message(id = 103, value = "Unable to serialize JSON") + String unableToSerializeJson(); } diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java index 1320e19d..3593bbe3 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java @@ -31,7 +31,7 @@ void execute_success() { void execute_fail() { AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> { - throw new RuntimeException("Test exception"); + throw new CliException("Test exception"); }); new CommandLine(command); int exitCode = command.execute(); @@ -63,13 +63,23 @@ void testPromptForInputMultipleCalls() { String input1 = "first\n"; System.setIn(new ByteArrayInputStream(input1.getBytes())); - String result1 = command.promptForInput("Enter first:", null); + String result1; + try { + result1 = command.promptForInput("Enter first:", null); + } catch (CliException e) { + throw new RuntimeException(e); + } assertEquals("first", result1); String input2 = "second\n"; System.setIn(new ByteArrayInputStream(input2.getBytes())); - String result2 = command.promptForInput("Enter second:", null); + String result2; + try { + result2 = command.promptForInput("Enter second:", null); + } catch (CliException e) { + throw new RuntimeException(e); + } assertEquals("second", result2); } diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java index 42444552..4816c72e 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java @@ -13,25 +13,30 @@ class AbstractCommandVerboseOptionTest extends AbstractCommandBaseTest { void ifProvided_printsStackTrace() { AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> { - throw new RuntimeException("Test exception"); + throw new CliException("Test exception"); }); CommandLine commandLine = new CommandLine(command); commandLine.parseArgs(CommonOption.VERBOSE_LONG); command.execute(); - assertTrue(errStream.toString().contains("java.lang.RuntimeException: Test exception")); + assertTrue( + errStream.toString().contains("com.streamx.cli.framework.CliException: Test exception") + ); } @Test void ifNotProvided_doesntPrintStackTrace() { AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> { - throw new RuntimeException("Test exception"); + throw new CliException("Test exception"); }); new CommandLine(command); command.execute(); - assertFalse(errStream.toString().contains("java.lang.RuntimeException: Test exception")); + assertTrue(errStream.toString().contains("Test exception")); + assertFalse( + errStream.toString().contains("com.streamx.cli.framework.CliException: Test exception") + ); } } diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java index 458c67d7..35188dca 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java @@ -6,7 +6,6 @@ import com.streamx.cli.framework.testing.TestObject; import com.streamx.cli.framework.testing.UnserializableObject; import java.util.List; -import java.util.function.Function; import org.junit.jupiter.api.Test; class CommandResultTest { @@ -49,7 +48,7 @@ class CommandResultTest { private final CommandResult commandResult = new CommandResult<>(result); @Test - void toText_withTextFormat_shouldUseTextFormatter() { + void toText_withTextFormat_shouldUseTextFormatter() throws CliException { String expectedOutput = """ Void Value: null Boolean Value: true @@ -61,7 +60,7 @@ void toText_withTextFormat_shouldUseTextFormatter() { Total Nested Objects: 2 """; - Function, String> textFormatter = + ThrowingFunction1, String, CliException> textFormatter = cr -> """ Void Value: %s Boolean Value: %b @@ -125,7 +124,12 @@ void toText_withJsonFormat_shouldReturnPrettyPrintedJson() { } """.strip(); - String output = commandResult.toText(OutputFormat.json, null); + String output; + try { + output = commandResult.toText(OutputFormat.json, null); + } catch (CliException e) { + throw new RuntimeException(e); + } assertEquals(expectedOutput, output); } @@ -163,7 +167,12 @@ void toText_withYamlFormat_shouldReturnYaml() { nestedObjects: null """.strip(); - String output = commandResult.toText(OutputFormat.yaml, null); + String output; + try { + output = commandResult.toText(OutputFormat.yaml, null); + } catch (CliException e) { + throw new RuntimeException(e); + } assertEquals(expectedOutput, output); } @@ -172,8 +181,17 @@ void toText_withYamlFormat_shouldReturnYaml() { void toText_withNullResult_shouldHandleGracefully() { CommandResult commandResult = new CommandResult<>(null); - assertEquals("null", commandResult.toText(OutputFormat.json, null)); - assertEquals("null", commandResult.toText(OutputFormat.yaml, null)); + String jsonOutput; + String yamlOutput; + try { + jsonOutput = commandResult.toText(OutputFormat.json, null); + yamlOutput = commandResult.toText(OutputFormat.yaml, null); + } catch (CliException e) { + throw new RuntimeException(e); + } + + assertEquals("null", jsonOutput); + assertEquals("null", yamlOutput); } @Test diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractSilentTestCommand.java b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractSilentTestCommand.java index 15998982..449daeda 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractSilentTestCommand.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractSilentTestCommand.java @@ -1,6 +1,7 @@ package com.streamx.cli.framework.testing; import com.streamx.cli.framework.AbstractSilentCommand; +import com.streamx.cli.framework.CliException; import com.streamx.cli.framework.CommandResult; import java.util.List; import java.util.function.Supplier; @@ -19,7 +20,7 @@ public void setHiddenOptionsHandler(Supplier> handler) { } @Override - public CommandResult runCommand() throws RuntimeException { + public CommandResult runCommand() throws CliException { if (runCommandHandler != null) { return runCommandHandler.get(); } diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractTestCommand.java b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractTestCommand.java index f8d467c6..405c9024 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractTestCommand.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractTestCommand.java @@ -1,21 +1,23 @@ package com.streamx.cli.framework.testing; import com.streamx.cli.framework.AbstractCommand; +import com.streamx.cli.framework.CliException; import com.streamx.cli.framework.CommandResult; +import com.streamx.cli.framework.ThrowingFunction; +import com.streamx.cli.framework.ThrowingFunction1; import java.io.IOException; import java.util.List; -import java.util.function.Function; import java.util.function.Supplier; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; // Helper class for testing AbstractCommand public class AbstractTestCommand extends AbstractCommand { - public Supplier> runCommandHandler; + public ThrowingFunction, CliException> runCommandHandler; public Supplier> hiddenOptionsHandler; - public Function, String> getTextOutputHandler; + public ThrowingFunction1, String, CliException> getTextOutputHandler; - public void setRunCommandHandler(Supplier> handler) { + public void setRunCommandHandler(ThrowingFunction, CliException> handler) { this.runCommandHandler = handler; } @@ -23,16 +25,18 @@ public void setHiddenOptionsHandler(Supplier> handler) { this.hiddenOptionsHandler = handler; } - public void setGetTextOutputHandler(Function, String> handler) { + public void setGetTextOutputHandler( + ThrowingFunction1, String, CliException> handler + ) { this.getTextOutputHandler = handler; } @Override - public CommandResult runCommand() throws RuntimeException { + public CommandResult runCommand() throws CliException { if (runCommandHandler != null) { return runCommandHandler.get(); } - throw new IllegalStateException("No run command handler set"); + throw new CliException("No run command handler set"); } @Override @@ -44,9 +48,9 @@ public List getHiddenOptions() { } @Override - public String getTextOutput(CommandResult result) throws RuntimeException { + public String getTextOutput(CommandResult result) throws CliException { if (getTextOutputHandler != null) { - return getTextOutputHandler.apply(result); + return getTextOutputHandler.get(result); } return super.getTextOutput(result); } From 841b5fd3f33415d2076bfcd56f64cadc501888f8 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Mon, 26 Jan 2026 09:23:03 +0300 Subject: [PATCH 16/21] Fix build --- .../framework/ShortErrorMessageHandler.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java index 5969cae2..b6ed2379 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java @@ -20,19 +20,23 @@ public int handleParseException(ParameterException ex, String[] args) { static int shortErrorMessage(Exception ex, CommandLine cmd) { PrintWriter writer = cmd.getErr(); - if (ex instanceof ParameterException || ex instanceof IllegalArgumentException || ex instanceof CliException) { + if ( + ex instanceof ParameterException + || ex instanceof IllegalArgumentException + || ex instanceof CliException + ) { writer.println(cmd.getColorScheme().errorText(ex.getMessage())); } else { String errorMessage = """ - ❌ Something went wrong while running the command. - - Please try again with `--verbose` for more details. - If the problem persists, report it here: - https://github.com/streamx-com/streamx-cli/issues - - You can also contact us at: - https://www.streamx.dev/contact-us.html - """.strip(); + ❌ Something went wrong while running the command. + + Please try again with `--verbose` for more details. + If the problem persists, report it here: + https://github.com/streamx-com/streamx-cli/issues + + You can also contact us at: + https://www.streamx.dev/contact-us.html + """.strip(); writer.println(cmd.getColorScheme().errorText(errorMessage)); } From d2d10be09dbec8a50d427f89885fa70b7b43a6db Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Mon, 26 Jan 2026 12:34:36 +0300 Subject: [PATCH 17/21] Make runCommand and some other methods to throw unchecked exception --- .../streamx/cli/framework/AbstractCommand.java | 8 ++++---- .../com/streamx/cli/framework/CliException.java | 2 +- .../streamx/cli/framework/CommandResult.java | 5 +++-- .../streamx/cli/framework/ThrowingFunction.java | 6 ------ .../cli/framework/ThrowingFunction1.java | 6 ------ .../cli/framework/CommandResultTest.java | 3 ++- .../framework/testing/AbstractTestCommand.java | 17 ++++++++--------- 7 files changed, 18 insertions(+), 29 deletions(-) delete mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction.java delete mode 100644 streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction1.java diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java index d53cae26..9be99ccd 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/AbstractCommand.java @@ -48,7 +48,7 @@ public void setSpec(CommandSpec spec) { public OutputFormat output = OutputFormat.text; // Override this method to implement the command logic. - public abstract CommandResult runCommand() throws CliException; + public abstract CommandResult runCommand(); // Override this method to hide specific command line options. // May be useful to hide the "--output" option for @@ -58,7 +58,7 @@ public List getHiddenOptions() { } // Override this method to provide human-readable output. - public String getTextOutput(CommandResult result) throws CliException { + public String getTextOutput(CommandResult result) { return result.toText(OutputFormat.json, null); } @@ -81,12 +81,12 @@ public void printUsage() { public String promptForInput( String prompt, @Nullable List autocompleteOptions - ) throws CliException { + ) { try (Terminal terminal = createTerminal()) { LineReaderBuilder builder = LineReaderBuilder.builder() .terminal(terminal); - Completer completer = null; + Completer completer; if (autocompleteOptions != null) { completer = new StringsCompleter(autocompleteOptions); builder.completer(completer); diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/CliException.java b/streamx-cli/src/main/java/com/streamx/cli/framework/CliException.java index 347d9dd6..5d3ea4d9 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/CliException.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/CliException.java @@ -1,6 +1,6 @@ package com.streamx.cli.framework; -public class CliException extends Exception { +public class CliException extends RuntimeException { public CliException(String userFriendlyMessage) { super(userFriendlyMessage); } diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java b/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java index 6f942074..a0ebb583 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/CommandResult.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import java.util.function.Function; /** * @param Must be serializable by Jackson (POJO, JsonSerializable, etc.) @@ -20,12 +21,12 @@ public CommandResult(ResultT result) { public String toText( OutputFormat outputFormat, - ThrowingFunction1, String, CliException> textFormatter + Function, String> textFormatter ) throws CliException { try { switch (outputFormat) { case OutputFormat.text -> { - return textFormatter.get(this); + return textFormatter.apply(this); } case OutputFormat.json -> { ObjectMapper mapper = new ObjectMapper(); diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction.java b/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction.java deleted file mode 100644 index 7bec16d2..00000000 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.streamx.cli.framework; - -@FunctionalInterface -public interface ThrowingFunction { - ResultT get() throws ExceptionT; -} \ No newline at end of file diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction1.java b/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction1.java deleted file mode 100644 index df083d6b..00000000 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/ThrowingFunction1.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.streamx.cli.framework; - -@FunctionalInterface -public interface ThrowingFunction1 { - ResultT get(InputT input) throws ExceptionT; -} \ No newline at end of file diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java index 35188dca..cbef70f1 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java @@ -6,6 +6,7 @@ import com.streamx.cli.framework.testing.TestObject; import com.streamx.cli.framework.testing.UnserializableObject; import java.util.List; +import java.util.function.Function; import org.junit.jupiter.api.Test; class CommandResultTest { @@ -60,7 +61,7 @@ void toText_withTextFormat_shouldUseTextFormatter() throws CliException { Total Nested Objects: 2 """; - ThrowingFunction1, String, CliException> textFormatter = + Function, String> textFormatter = cr -> """ Void Value: %s Boolean Value: %b diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractTestCommand.java b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractTestCommand.java index 405c9024..af50ec4f 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractTestCommand.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/testing/AbstractTestCommand.java @@ -3,21 +3,20 @@ import com.streamx.cli.framework.AbstractCommand; import com.streamx.cli.framework.CliException; import com.streamx.cli.framework.CommandResult; -import com.streamx.cli.framework.ThrowingFunction; -import com.streamx.cli.framework.ThrowingFunction1; import java.io.IOException; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; // Helper class for testing AbstractCommand public class AbstractTestCommand extends AbstractCommand { - public ThrowingFunction, CliException> runCommandHandler; + public Supplier> runCommandHandler; public Supplier> hiddenOptionsHandler; - public ThrowingFunction1, String, CliException> getTextOutputHandler; + public Function, String> getTextOutputHandler; - public void setRunCommandHandler(ThrowingFunction, CliException> handler) { + public void setRunCommandHandler(Supplier> handler) { this.runCommandHandler = handler; } @@ -26,13 +25,13 @@ public void setHiddenOptionsHandler(Supplier> handler) { } public void setGetTextOutputHandler( - ThrowingFunction1, String, CliException> handler + Function, String> handler ) { this.getTextOutputHandler = handler; } @Override - public CommandResult runCommand() throws CliException { + public CommandResult runCommand() { if (runCommandHandler != null) { return runCommandHandler.get(); } @@ -48,9 +47,9 @@ public List getHiddenOptions() { } @Override - public String getTextOutput(CommandResult result) throws CliException { + public String getTextOutput(CommandResult result) { if (getTextOutputHandler != null) { - return getTextOutputHandler.get(result); + return getTextOutputHandler.apply(result); } return super.getTextOutput(result); } From 961d9ec7c9ab10dc28bf2635529e49776eef1432 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Mon, 26 Jan 2026 12:47:57 +0300 Subject: [PATCH 18/21] Remove git repo mention in user-faced error message --- .../com/streamx/cli/framework/ShortErrorMessageHandler.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java index b6ed2379..617f1aa6 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java @@ -32,9 +32,6 @@ static int shortErrorMessage(Exception ex, CommandLine cmd) { Please try again with `--verbose` for more details. If the problem persists, report it here: - https://github.com/streamx-com/streamx-cli/issues - - You can also contact us at: https://www.streamx.dev/contact-us.html """.strip(); From 047484e4bdc198df8423a38d6a3766f5da238110 Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Mon, 26 Jan 2026 12:55:14 +0300 Subject: [PATCH 19/21] Move Something went wrong message to MessageProvider --- .../cli/framework/ShortErrorMessageHandler.java | 8 +------- .../java/com/streamx/cli/i18n/MessageProvider.java | 14 +++++++++++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java index 617f1aa6..7634b427 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java +++ b/streamx-cli/src/main/java/com/streamx/cli/framework/ShortErrorMessageHandler.java @@ -27,13 +27,7 @@ static int shortErrorMessage(Exception ex, CommandLine cmd) { ) { writer.println(cmd.getColorScheme().errorText(ex.getMessage())); } else { - String errorMessage = """ - ❌ Something went wrong while running the command. - - Please try again with `--verbose` for more details. - If the problem persists, report it here: - https://www.streamx.dev/contact-us.html - """.strip(); + String errorMessage = msg.somethingWentWrong().strip(); writer.println(cmd.getColorScheme().errorText(errorMessage)); } diff --git a/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java b/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java index c24ba929..7d11a641 100644 --- a/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java +++ b/streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java @@ -5,7 +5,7 @@ import org.jboss.logging.annotations.Message; import org.jboss.logging.annotations.MessageBundle; -@MessageBundle(projectCode = "STREAMXCLI") +@MessageBundle(projectCode = "") public interface MessageProvider { MessageProvider msg = Messages.getBundle(MethodHandles.lookup(), MessageProvider.class); @@ -24,4 +24,16 @@ String tryForMoreInformationOnAvailableOptions( @Message(id = 103, value = "Unable to serialize JSON") String unableToSerializeJson(); + + @Message( + id = 104, + value = """ + ❌ Something went wrong while running the command. + + Please try again with `--verbose` for more details. + If the problem persists, report it here: + https://www.streamx.dev/contact-us.html + """ + ) + String somethingWentWrong(); } From c88ae0cab58189dea5e83bea5cb4bbcb7e11f73a Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Mon, 26 Jan 2026 13:48:28 +0300 Subject: [PATCH 20/21] Add contributing guidelines --- CONTRIBUTING.md | 20 ++++++++++++++++++++ README.md | 12 +----------- 2 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..a17cf63a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# Contributing + +- All commands should extend `AbstractCommand` from [this package](./streamx-cli/src/main/java/com/streamx/cli/framework). +- There are helper classes which extend the `AbstractCommand` class: + - Use `AbstractCommandGroup` for commands which only contain subcommands and don't do anything else, e.g. `streamx settings`. + - Use `AbstractSilentCommand` for commands which don't print any user-faced output, e.g. `streamx settings set`. +- Commands should throw only the [`CliException`](./streamx-cli/src/main/java/com/streamx/cli/framework/CliException.java). +- All user facing messages should be provided by [`MessageProvider`](./streamx-cli/src/main/java/com/streamx/cli/i18n/MessageProvider.java). + +## Development + +- Enter Quarkus development console. + +`cd streamx-cli && ./mvnw quarkus:dev` + +- Use `e` button to edit CLI arguments. + +## Running tests + +`cd streamx-cli && ./mvnw test` \ No newline at end of file diff --git a/README.md b/README.md index 4471f961..68bca310 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,7 @@ This project provides utilities for managing the mesh: For more information, see the [StreamX CLI Reference](https://www.streamx.dev/guides/main/streamx-command-line-interface-reference.html). -## Development - -- Enter Quarkus development console. - -`cd streamx-cli && ./mvnw quarkus:dev` - -- Use `e` button to edit CLI arguments. - -## Running tests - -`cd streamx-cli && ./mvnw test` +⚠️ Please read the [contributing guidelines](./CONTRIBUTING.md) if you're a developer and wish to contribute to the project. ## Configuration From 30205622768e72f16a180e876bc1b49615d6826b Mon Sep 17 00:00:00 2001 From: Kiryl Valkovich Date: Mon, 26 Jan 2026 14:27:27 +0300 Subject: [PATCH 21/21] Make test method names follow the company coding conventions --- .../AbstractCommandOutputOptionTest.java | 10 +-- .../cli/framework/AbstractCommandTest.java | 8 +-- .../AbstractCommandVerboseOptionTest.java | 4 +- .../framework/AbstractSilentCommandTest.java | 4 +- .../cli/framework/CommandResultTest.java | 66 +++++++++---------- 5 files changed, 46 insertions(+), 46 deletions(-) diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java index 83e9e286..dc7ddbf1 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandOutputOptionTest.java @@ -48,7 +48,7 @@ class AbstractCommandOutputOptionTest extends AbstractCommandBaseTest { ); @Test - void textOutputFlag_FormatsOutputAsJsonIfNoCustomFormatterProvided() { + void shouldFormatResultAsJsonIfNoCustomFormatterProvided() { AbstractTestCommand command = new AbstractTestCommand<>(); CommandLine commandLine = new CommandLine(command); commandLine.parseArgs(CommonOption.OUTPUT_LONG, "text"); @@ -96,7 +96,7 @@ void textOutputFlag_FormatsOutputAsJsonIfNoCustomFormatterProvided() { } @Test - void textOutputFlag_FormatsWithCustomFormatter() { + void shouldFormatResultWithCustomFormatter() { AbstractTestCommand command = new AbstractTestCommand(); CommandLine commandLine = new CommandLine(command); commandLine.parseArgs(CommonOption.OUTPUT_LONG, "text"); @@ -118,7 +118,7 @@ void textOutputFlag_FormatsWithCustomFormatter() { } @Test - void jsonOutputFlag() { + void shouldFormatResultAsJson() { AbstractTestCommand command = new AbstractTestCommand<>(); CommandLine commandLine = new CommandLine(command); commandLine.parseArgs(CommonOption.OUTPUT_LONG, "json"); @@ -166,7 +166,7 @@ void jsonOutputFlag() { } @Test - void yamlOutputFlag() { + void shouldFormatResultAsYaml() { AbstractTestCommand command = new AbstractTestCommand<>(); CommandLine commandLine = new CommandLine(command); commandLine.parseArgs(CommonOption.OUTPUT_LONG, "yaml"); @@ -209,7 +209,7 @@ void yamlOutputFlag() { } @Test - void voidResult() { + void shouldHandleVoidResult() { AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> new CommandResult<>(null)); diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java index 3593bbe3..88885b0b 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandTest.java @@ -16,7 +16,7 @@ class AbstractCommandTest extends AbstractCommandBaseTest { @Test - void execute_success() { + void shouldExecuteSuccessfully() { AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> new CommandResult<>(TestObject.random())); new CommandLine(command); @@ -28,7 +28,7 @@ void execute_success() { } @Test - void execute_fail() { + void shouldHandleExceptionGracefully() { AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> { throw new CliException("Test exception"); @@ -42,7 +42,7 @@ void execute_fail() { } @Test - void getHiddenOptionsOverride() { + void shouldHideOptionsBasedOnHandler() { AbstractTestCommand command1 = new AbstractTestCommand<>(); new CommandLine(command1); @@ -56,7 +56,7 @@ void getHiddenOptionsOverride() { } @Test - void testPromptForInputMultipleCalls() { + void shouldBeAbleToPromptForInput() { AbstractTestCommand command = new AbstractTestCommand<>(); CommandLine commandLine = new CommandLine(command); command.setSpec(commandLine.getCommandSpec()); diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java index 4816c72e..c4f38498 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractCommandVerboseOptionTest.java @@ -10,7 +10,7 @@ class AbstractCommandVerboseOptionTest extends AbstractCommandBaseTest { @Test - void ifProvided_printsStackTrace() { + void shouldPrintStackTraceIfProvided() { AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> { throw new CliException("Test exception"); @@ -26,7 +26,7 @@ void ifProvided_printsStackTrace() { } @Test - void ifNotProvided_doesntPrintStackTrace() { + void shouldNotPrintStackTraceByDefault() { AbstractTestCommand command = new AbstractTestCommand<>(); command.setRunCommandHandler(() -> { throw new CliException("Test exception"); diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractSilentCommandTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractSilentCommandTest.java index 5cc428c5..f8454182 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractSilentCommandTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/AbstractSilentCommandTest.java @@ -10,7 +10,7 @@ class AbstractSilentCommandTest extends AbstractCommandBaseTest { @Test - void outputFlagIsAbsent() { + void shouldNotHaveOutputOption() { AbstractSilentTestCommand command = new AbstractSilentTestCommand(); new CommandLine(command); // Trigger all PicocLi initialization @@ -18,7 +18,7 @@ void outputFlagIsAbsent() { } @Test - void outputIsEmpty() { + void shouldReturnEmptyOutput() { AbstractSilentTestCommand command = new AbstractSilentTestCommand(); command.setRunCommandHandler(() -> new CommandResult<>(null)); command.execute(); diff --git a/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java b/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java index cbef70f1..2c06de54 100644 --- a/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java +++ b/streamx-cli/src/test/java/com/streamx/cli/framework/CommandResultTest.java @@ -26,30 +26,30 @@ class CommandResultTest { null ), List.of( - new TestObject( - null, - true, - 15, - 100.42, - "Nested list object 1 test string", - null, - null - ), - new TestObject( - null, - false, - 18, - 0.42, - "Nested list object 2 test string", - null, - null - ) + new TestObject( + null, + true, + 15, + 100.42, + "Nested list object 1 test string", + null, + null + ), + new TestObject( + null, + false, + 18, + 0.42, + "Nested list object 2 test string", + null, + null + ) ) ); private final CommandResult commandResult = new CommandResult<>(result); @Test - void toText_withTextFormat_shouldUseTextFormatter() throws CliException { + void shouldUseTextFormatterWhenTextFormatProvided() throws CliException { String expectedOutput = """ Void Value: null Boolean Value: true @@ -72,15 +72,15 @@ void toText_withTextFormat_shouldUseTextFormatter() throws CliException { Nested Object String Value: %s Total Nested Objects: %d """.formatted( - cr.result.voidValue, - cr.result.booleanValue, - cr.result.longValue, - cr.result.floatValue, - cr.result.stringValue, - cr.result.nestedObject.longValue, - cr.result.nestedObject.stringValue, - cr.result.nestedObjects.size() - ); + cr.result.voidValue, + cr.result.booleanValue, + cr.result.longValue, + cr.result.floatValue, + cr.result.stringValue, + cr.result.nestedObject.longValue, + cr.result.nestedObject.stringValue, + cr.result.nestedObjects.size() + ); String output = commandResult.toText(OutputFormat.text, textFormatter); @@ -88,7 +88,7 @@ void toText_withTextFormat_shouldUseTextFormatter() throws CliException { } @Test - void toText_withJsonFormat_shouldReturnPrettyPrintedJson() { + void shouldReturnPrettyPrintedJsonWhenJsonFormatProvided() { String expectedOutput = """ { "voidValue" : null, @@ -136,7 +136,7 @@ void toText_withJsonFormat_shouldReturnPrettyPrintedJson() { } @Test - void toText_withYamlFormat_shouldReturnYaml() { + void shouldReturnYamlWhenYamlFormatProvided() { String expectedOutput = """ voidValue: null booleanValue: true @@ -179,7 +179,7 @@ void toText_withYamlFormat_shouldReturnYaml() { } @Test - void toText_withNullResult_shouldHandleGracefully() { + void shouldHandleNullResultGracefully() { CommandResult commandResult = new CommandResult<>(null); String jsonOutput; @@ -196,7 +196,7 @@ void toText_withNullResult_shouldHandleGracefully() { } @Test - void toText_shouldThrowRuntimeExceptionForUnserializableObject() { + void shouldThrowRuntimeExceptionForUnserializableObject() { UnserializableObject unserializable = new UnserializableObject(); CommandResult commandResult = new CommandResult<>(unserializable); @@ -204,4 +204,4 @@ void toText_shouldThrowRuntimeExceptionForUnserializableObject() { commandResult.toText(OutputFormat.json, null) ); } -} +} \ No newline at end of file