diff --git a/build.gradle.kts b/build.gradle.kts index 61aa646..2b5f4b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,8 @@ dependencies { implementation("info.picocli:picocli") implementation("info.picocli:picocli-jansi-graalvm:1.2.0") implementation("org.fusesource.jansi:jansi:2.4.2") + implementation("info.picocli:picocli-shell-jline3:4.7.6") + implementation("org.jline:jline:3.30.5") implementation("io.micronaut:micronaut-http-client") implementation("io.micronaut.picocli:micronaut-picocli") implementation("io.micronaut.serde:micronaut-serde-jackson") @@ -36,7 +38,7 @@ dependencies { application { - mainClass = "org.justserve.BaseCommand" + mainClass = "org.justserve.cli.JustServeCommand" } java { diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..41073ae --- /dev/null +++ b/compose.yml @@ -0,0 +1,26 @@ +services: + linux-shell: + image: ghcr.io/graalvm/native-image-community:21-muslib + container_name: justserve-linux-shell + working_dir: /app + volumes: + - .:/app + env_file: .env + entrypoint: /bin/sh + command: + - -c + - "mkdir -p /temp_app && cp -r . /temp_app/ && cd /temp_app && microdnf install -y findutils && sed -i 's/\r$//' ./gradlew && if [ -t 0 ]; then /bin/bash; else tail -f /dev/null; fi" + stdin_open: true + tty: true + + linux-test: + image: ghcr.io/graalvm/native-image-community:21-muslib + container_name: justserve-linux-test + working_dir: /app + volumes: + - .:/app + env_file: .env + entrypoint: /bin/sh + command: + - -c + - "mkdir -p /temp_app && cp -r . /temp_app/ && cd /temp_app && microdnf install -y findutils && sed -i 's/\r$//' ./gradlew && ./gradlew test" \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index bd03fdd..b1b5a5a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ micronautVersion=4.8.3 -justserveCliVersion=0.0.5-SNAPSHOT \ No newline at end of file +justserveCliVersion=0.0.5-SNAPSHOT +org.gradle.console=rich \ No newline at end of file diff --git a/src/main/java/org/justserve/cli/JustServeCommand.java b/src/main/java/org/justserve/cli/JustServeCommand.java new file mode 100644 index 0000000..67a472f --- /dev/null +++ b/src/main/java/org/justserve/cli/JustServeCommand.java @@ -0,0 +1,28 @@ +package org.justserve.cli; + +import io.micronaut.configuration.picocli.PicocliRunner; +import org.justserve.cli.command.BaseCommand; +import org.justserve.cli.command.GetTempPassword; +import org.justserve.cli.util.JustServeVersionProvider; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParameterException; +import picocli.jansi.graalvm.AnsiConsole; + +@Command(subcommands = GetTempPassword.class, + mixinStandardHelpOptions = true, + name = "justserve", versionProvider = JustServeVersionProvider.class, + description = "justserve-cli is a terminal tool to help specialists and admin using JustServe") +public class JustServeCommand extends BaseCommand implements Runnable { + + public static void main(String[] args) { + try (AnsiConsole ignored = AnsiConsole.windowsInstall()) { + PicocliRunner.run(JustServeCommand.class, args); + } + + } + + @Override + public void run() { + throw new ParameterException(spec.commandLine(), "No command specified"); + } +} diff --git a/src/main/java/org/justserve/cli/command/BaseCommand.java b/src/main/java/org/justserve/cli/command/BaseCommand.java new file mode 100644 index 0000000..29da8f6 --- /dev/null +++ b/src/main/java/org/justserve/cli/command/BaseCommand.java @@ -0,0 +1,70 @@ +package org.justserve.cli.command; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.ReflectiveAccess; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Spec; + +import java.io.PrintWriter; +import java.util.Optional; + +import static picocli.CommandLine.Help.Ansi.AUTO; + +@Command +public class BaseCommand implements ConsoleOutput{ + + @Spec + @ReflectiveAccess + protected CommandSpec spec; + + + @Override + public void out(String message) { + outWriter().ifPresent(writer -> writer.println(AUTO.string(message))); + } + + public void err(String message) { + errWriter().ifPresent(writer -> writer.println(AUTO.string("@|bold,red | Error|@ " + message))); + } + + public void warning(String message) { + outWriter().ifPresent(writer -> writer.println(AUTO.string("@|bold,red | Warning|@ " + message))); + } + + @Override + public void green(String message) { + outWriter().ifPresent(writer -> writer.println(AUTO.string("@|bold,green " + message + "|@"))); + } + + @Override + public void red(String message) { + outWriter().ifPresent(writer -> writer.println(AUTO.string("@|bold,red " + message + "|@"))); + } + + @Override + public boolean showStacktrace() { + return false; + } + + @Override + public boolean verbose() { + return false; + } + + + @NonNull + public Optional outWriter() { + return getSpec().map(spec -> spec.commandLine().getOut()); + } + + @NonNull + public Optional errWriter() { + return getSpec().map(spec -> spec.commandLine().getErr()); + } + + @NonNull + public Optional getSpec() { + return Optional.ofNullable(spec); + } +} diff --git a/src/main/java/org/justserve/cli/command/CommonOptionsMixin.java b/src/main/java/org/justserve/cli/command/CommonOptionsMixin.java new file mode 100644 index 0000000..527e5c0 --- /dev/null +++ b/src/main/java/org/justserve/cli/command/CommonOptionsMixin.java @@ -0,0 +1,34 @@ +package org.justserve.cli.command; + +import io.micronaut.core.annotation.ReflectiveAccess; +import org.justserve.cli.util.JustServeVersionProvider; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +/** + * Mixin that adds help, version and other common options to a command. Example usage: + *
+ * @Command(name = "command")
+ * class App {
+ *     @Mixin
+ *     CommonOptionsMixin commonOptions // adds common options to the command
+ *
+ *     // ...
+ * }
+ * 
+ * + * @author Remko Popma + * @version 1.0 + */ +@Command(mixinStandardHelpOptions = true, versionProvider = JustServeVersionProvider.class) +@SuppressWarnings("checkstyle:VisibilityModifier") +public class CommonOptionsMixin { + + @Option(names = {"-x", "--stacktrace"}, defaultValue = "false", description = "Show full stack trace when exceptions occur.") + @ReflectiveAccess + public boolean showStacktrace; + + @Option(names = {"-v", "--verbose"}, defaultValue = "false", description = "Create verbose output.") + @ReflectiveAccess + public boolean verbose; +} \ No newline at end of file diff --git a/src/main/java/org/justserve/cli/command/ConsoleOutput.java b/src/main/java/org/justserve/cli/command/ConsoleOutput.java new file mode 100644 index 0000000..00c03b0 --- /dev/null +++ b/src/main/java/org/justserve/cli/command/ConsoleOutput.java @@ -0,0 +1,43 @@ +package org.justserve.cli.command; + +public interface ConsoleOutput { + ConsoleOutput NOOP = new ConsoleOutput() { + @Override + public void out(String message) { } + + @Override + public void err(String message) { } + + @Override + public void warning(String message) { } + + @Override + public boolean showStacktrace() { + return false; + } + + @Override + public boolean verbose() { + return false; + } + + }; + + void out(String message); + + void err(String message); + + void warning(String message); + + boolean showStacktrace(); + + boolean verbose(); + + default void green(String message) { + out(message); + } + + default void red(String message) { + out(message); + } +} diff --git a/src/main/java/org/justserve/BaseCommand.java b/src/main/java/org/justserve/cli/command/GetTempPassword.java similarity index 67% rename from src/main/java/org/justserve/BaseCommand.java rename to src/main/java/org/justserve/cli/command/GetTempPassword.java index 746f510..399d28a 100644 --- a/src/main/java/org/justserve/BaseCommand.java +++ b/src/main/java/org/justserve/cli/command/GetTempPassword.java @@ -1,44 +1,34 @@ -package org.justserve; +package org.justserve.cli.command; -import io.micronaut.configuration.picocli.PicocliRunner; import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.ReflectiveAccess; import io.micronaut.http.HttpResponse; import io.micronaut.http.client.exceptions.HttpClientResponseException; import jakarta.inject.Inject; import jakarta.inject.Provider; import org.justserve.client.UserClient; import org.justserve.model.UserHashRequestByEmail; -import org.justserve.util.VersionProvider; import picocli.CommandLine.Command; import picocli.CommandLine.Option; -import picocli.jansi.graalvm.AnsiConsole; -import static org.justserve.util.JustServePrinter.printError; -import static org.justserve.util.JustServePrinter.printNormal; +import static org.justserve.cli.util.JustServePrinter.printError; +import static org.justserve.cli.util.JustServePrinter.printNormal; -@Command(name = "justserve", versionProvider = VersionProvider.class, - description = "justserve-cli is a terminal tool to help specialists and admin using JustServe") -public class BaseCommand implements Runnable { +@Command(name = "getTempPassword", description = "get a temporary password for a user") +public class GetTempPassword extends BaseCommand implements Runnable { + @ReflectiveAccess @Option(names = {"-e", "--email"}, description = "email for the user whose temporary password will be generated") String email; - @Option(names = {"version", "--version", "-v"}, versionHelp = true, description = "print version info and exit") - boolean version = false; - @Inject + @ReflectiveAccess Provider userClientProvider; @Value("${justserve.token}") String token; - public static void main(String[] args) { - try (AnsiConsole ignored = AnsiConsole.windowsInstall()) { - PicocliRunner.run(BaseCommand.class, args); - } - - } - + @Override public void run() { HttpResponse response; if ("i-need-to-be-defined".equals(token) || null == token) { diff --git a/src/main/java/org/justserve/util/JustServePrinter.java b/src/main/java/org/justserve/cli/util/JustServePrinter.java similarity index 99% rename from src/main/java/org/justserve/util/JustServePrinter.java rename to src/main/java/org/justserve/cli/util/JustServePrinter.java index 6a01c62..643431d 100644 --- a/src/main/java/org/justserve/util/JustServePrinter.java +++ b/src/main/java/org/justserve/cli/util/JustServePrinter.java @@ -1,4 +1,4 @@ -package org.justserve.util; +package org.justserve.cli.util; import org.fusesource.jansi.Ansi; diff --git a/src/main/java/org/justserve/util/VersionProvider.java b/src/main/java/org/justserve/cli/util/JustServeVersionProvider.java similarity index 73% rename from src/main/java/org/justserve/util/VersionProvider.java rename to src/main/java/org/justserve/cli/util/JustServeVersionProvider.java index 082db41..f67d90a 100644 --- a/src/main/java/org/justserve/util/VersionProvider.java +++ b/src/main/java/org/justserve/cli/util/JustServeVersionProvider.java @@ -1,12 +1,12 @@ -package org.justserve.util; +package org.justserve.cli.util; import io.micronaut.context.annotation.Value; import picocli.CommandLine; -import static org.justserve.util.JustServePrinter.styleEmphasis; -import static org.justserve.util.JustServePrinter.styleTitle; +import static org.justserve.cli.util.JustServePrinter.styleEmphasis; +import static org.justserve.cli.util.JustServePrinter.styleTitle; -public class VersionProvider implements CommandLine.IVersionProvider { +public class JustServeVersionProvider implements CommandLine.IVersionProvider { @Value("${micronaut.application.version}") String justserveCliVersion; diff --git a/src/test/groovy/org/justserve/client/UserClientSpec.groovy b/src/test/groovy/org/justserve/client/UserClientSpec.groovy index f868993..5f0ffbb 100644 --- a/src/test/groovy/org/justserve/client/UserClientSpec.groovy +++ b/src/test/groovy/org/justserve/client/UserClientSpec.groovy @@ -19,7 +19,7 @@ class UserClientSpec extends JustServeSpec { userClient = ctx.getBean(UserClient) } - def "get tempPassword for #email and "() { + def "get tempPassword for #email"() { when: HttpResponse response = null def caughtException = null diff --git a/src/test/groovy/org/justserve/BaseCommandSpec.groovy b/src/test/groovy/org/justserve/command/BaseCommandSpec.groovy similarity index 64% rename from src/test/groovy/org/justserve/BaseCommandSpec.groovy rename to src/test/groovy/org/justserve/command/BaseCommandSpec.groovy index 7d54604..200a627 100644 --- a/src/test/groovy/org/justserve/BaseCommandSpec.groovy +++ b/src/test/groovy/org/justserve/command/BaseCommandSpec.groovy @@ -1,7 +1,9 @@ -package org.justserve +package org.justserve.command import io.micronaut.configuration.picocli.PicocliRunner import io.micronaut.context.ApplicationContext +import org.justserve.JustServeSpec +import org.justserve.cli.JustServeCommand import spock.lang.Shared import spock.lang.Unroll @@ -10,7 +12,7 @@ import java.util.regex.Pattern class BaseCommandSpec extends JustServeSpec { @Shared - Pattern cliVersion, ansiRegex, blankRegex, successRegex, errorRegex, tokenNotSetRegex + Pattern cliVersion, blankRegex, successRegex, errorRegex, tokenNotSetRegex def setupSpec() { def props = new Properties() @@ -23,7 +25,7 @@ class BaseCommandSpec extends JustServeSpec { | |_ _ ___| |_| (___ ___ _ ____ _____ | | | | / __| __|\\___ \\ / _ \\ '__\\ \\ / / _ \\ ___| | |_| \\__ \\ |_ ____) | __/ | \\ V / __/ -|____/ \\__,_|___/\\__|_____/ \\___|_| \\_/ \\___|"""; +|____/ \\__,_|___/\\__|_____/ \\___|_| \\_/ \\___|""" cliVersion = Pattern.compile("^${ansi}${Pattern.quote(staticText)}${ansi}\\s*${ansi}" + Pattern.quote(props.getProperty('justserveCliVersion')) + "${ansi}\\s*\$") blankRegex = Pattern.compile "^\\s*\$" @@ -34,31 +36,6 @@ class BaseCommandSpec extends JustServeSpec { "${ansi}Please define the environment variable \"JUSTSERVE_TOKEN\" and try again\\.${ansi}\\s*\$") } - @Unroll("command with args: calling 'justserve #flag #email' works as expected") - def "commands to query temporary password should behave as expected with or without authentication"() { - when: - def (outputStream, errorStream) = executeCommand(context as ApplicationContext, [flag, email] as String[]) - - then: - if (context == noAuthCtx) { - verifyAll { - outputStream.matches(blankRegex) - errorStream.matches(tokenNotSetRegex) - } - } else if (userEmail.equalsIgnoreCase(email as String)) { - verifyAll { - outputStream.matches(successRegex) - errorStream.matches(blankRegex) - } - } else { - verifyAll { - outputStream.matches(blankRegex) - errorStream.matches(errorRegex) - } - } - where: - [flag, email, context] << [['-e', '--email'], [userEmail, "notanemail@mail.moc"], [noAuthCtx, ctx]].combinations() - } @Unroll def "querying version returns current version, with or without authentication"() { @@ -72,7 +49,7 @@ class BaseCommandSpec extends JustServeSpec { } where: - [args, context] << [[['-v'], ['--version'], ['version']], [noAuthCtx, ctx]].combinations() + [args, context] << [[['-V'], ['--version']], [noAuthCtx, ctx]].combinations() } /** @@ -89,12 +66,8 @@ class BaseCommandSpec extends JustServeSpec { OutputStream err = new ByteArrayOutputStream() System.setOut(new PrintStream(out)) System.setErr(new PrintStream(err)) - PicocliRunner.run(BaseCommand.class, ctx, args) + PicocliRunner.run(JustServeCommand.class, ctx, args) return new String[]{out.toString(), err.toString()} } - String stripColor(String string) { - return ansiRegex.matcher(string).replaceAll("") - } - } diff --git a/src/test/groovy/org/justserve/command/GetTempPasswordSpec.groovy b/src/test/groovy/org/justserve/command/GetTempPasswordSpec.groovy new file mode 100644 index 0000000..305d8da --- /dev/null +++ b/src/test/groovy/org/justserve/command/GetTempPasswordSpec.groovy @@ -0,0 +1,33 @@ +package org.justserve.command + +import io.micronaut.context.ApplicationContext +import spock.lang.Unroll + +class GetTempPasswordSpec extends BaseCommandSpec { + + @Unroll("command with args: calling 'justserve #flag #email' works as expected") + def "commands to query temporary password should behave as expected with or without authentication"() { + when: + def (outputStream, errorStream) = executeCommand(context as ApplicationContext, ["getTempPassword", flag, email] as String[]) + + then: + if (context == noAuthCtx) { + verifyAll { + outputStream.matches(blankRegex) + errorStream.matches(tokenNotSetRegex) + } + } else if (userEmail.equalsIgnoreCase(email as String)) { + verifyAll { + outputStream.matches(successRegex) + errorStream.matches(blankRegex) + } + } else { + verifyAll { + outputStream.matches(blankRegex) + errorStream.matches(errorRegex) + } + } + where: + [flag, email, context] << [['-e', '--email'], [userEmail, "notanemail@mail.moc"], [noAuthCtx, ctx]].combinations() + } +}