cli: module convert to Java (#473)

This commit is contained in:
Paul Campbell 2020-06-22 06:36:44 +01:00 committed by GitHub
parent 0104d9c08d
commit 0092d163f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 379 additions and 212 deletions

View file

@ -30,7 +30,7 @@ trait Program {
args: List[String] args: List[String]
): ZIO[Storage with Clock with FileScanner, Nothing, Unit] = { ): ZIO[Storage with Clock with FileScanner, Nothing, Unit] = {
(for { (for {
cli <- CliArgs.parse(args) cli <- UIO(CliArgs.parse(args.toArray))
config <- IO(ConfigurationBuilder.buildConfig(cli)) config <- IO(ConfigurationBuilder.buildConfig(cli))
_ <- UIO(Console.putStrLn(versionLabel)) _ <- UIO(Console.putStrLn(versionLabel))
_ <- ZIO.when(!showVersion(cli))( _ <- ZIO.when(!showVersion(cli))(

View file

@ -12,49 +12,36 @@
<name>cli</name> <name>cli</name>
<dependencies> <dependencies>
<!-- picocli -->
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.3.2</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- thorp --> <!-- thorp -->
<dependency> <dependency>
<groupId>net.kemitix.thorp</groupId> <groupId>net.kemitix.thorp</groupId>
<artifactId>thorp-config</artifactId> <artifactId>thorp-config</artifactId>
</dependency> </dependency>
<dependency>
<groupId>net.kemitix.thorp</groupId>
<artifactId>thorp-filesystem</artifactId>
</dependency>
<!-- command line parsing --> <!-- testing -->
<dependency> <dependency>
<groupId>com.github.scopt</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>scopt_2.13</artifactId> <artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency> </dependency>
<!-- scala -->
<dependency> <dependency>
<groupId>org.scala-lang</groupId> <groupId>org.assertj</groupId>
<artifactId>scala-library</artifactId> <artifactId>assertj-core</artifactId>
</dependency>
<!-- zio -->
<dependency>
<groupId>dev.zio</groupId>
<artifactId>zio_2.13</artifactId>
</dependency>
<!-- scala - testing -->
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_2.13</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project> </project>

View file

@ -0,0 +1,162 @@
package net.kemitix.thorp.cli;
import net.kemitix.thorp.config.ConfigOption;
import net.kemitix.thorp.config.ConfigOptions;
import picocli.CommandLine;
import picocli.CommandLine.Option;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class CliArgs {
@Override
public String toString() {
return "CliArgs{" +
"showVersion=" + showVersion +
",\n batchMode=" + batchMode +
",\n sources=" + sources +
",\n bucket='" + bucket + '\'' +
",\n prefix='" + prefix + '\'' +
",\n parallel=" + parallel +
",\n includes=" + includes +
",\n excludes=" + excludes +
",\n debug=" + debug +
",\n ignoreUserOptions=" + ignoreUserOptions +
",\n ignoreGlobalOptions=" + ignoreGlobalOptions +
"\n}";
}
public static ConfigOptions parse(String[] args) {
CliArgs cliArgs = new CliArgs();
new CommandLine(cliArgs).parseArgs(args);
return ConfigOptions.empty()
.merge(flag(cliArgs.showVersion, ConfigOption::version))
.merge(flag(cliArgs.debug, ConfigOption::debug))
.merge(flag(cliArgs.batchMode, ConfigOption::batchMode))
.merge(flag(cliArgs.ignoreGlobalOptions, ConfigOption::ignoreGlobalOptions))
.merge(flag(cliArgs.ignoreUserOptions, ConfigOption::ignoreUserOptions))
.merge(option(cliArgs.bucket, ConfigOption::bucket))
.merge(option(cliArgs.prefix, ConfigOption::prefix))
.merge(paths(cliArgs.sources, ConfigOption::source))
.merge(strings(cliArgs.includes, ConfigOption::include))
.merge(strings(cliArgs.excludes, ConfigOption::exclude))
.merge(integer(cliArgs.parallel, 1, ConfigOption::parallel));
}
private static ConfigOptions flag(
boolean flag,
Supplier<ConfigOption> configOption
) {
if (flag)
return ConfigOptions.create(Collections.singletonList(
configOption.get()));
return ConfigOptions.empty();
}
private static ConfigOptions option(
String value,
Function<String, ConfigOption> configOption
) {
if (value.isEmpty())
return ConfigOptions.empty();
return ConfigOptions.create(Collections.singletonList(
configOption.apply(value)));
}
private static ConfigOptions integer(
int value,
int defaultValue,
Function<Integer, ConfigOption> configOption
) {
if (value == defaultValue) return ConfigOptions.empty();
return ConfigOptions.create(Collections.singletonList(
configOption.apply(value)));
}
private static ConfigOptions paths(
List<String> values,
Function<Path, ConfigOption> configOption
) {
if (values == null) return ConfigOptions.empty();
return ConfigOptions.create(
values.stream()
.map(Paths::get)
.map(configOption)
.collect(Collectors.toList()));
}
private static ConfigOptions strings(
List<String> values,
Function<String, ConfigOption> configOption
) {
if (values == null) return ConfigOptions.empty();
return ConfigOptions.create(
values.stream()
.map(configOption)
.collect(Collectors.toList()));
}
@Option(
names = {"-V", "--version"},
description = "Show version")
boolean showVersion;
@Option(
names = {"-B", "--batch"},
description = "Enable batch-mode")
boolean batchMode;
@Option(
names = {"-s", "--source"},
description = "Source directory to sync to destination")
List<String> sources;
@Option(
names = {"-b", "--bucket"},
defaultValue = "",
description = "S3 bucket name")
String bucket;
@Option(
names = {"-p", "--prefix"},
defaultValue = "",
description = "Prefix within the S3 Bucket")
String prefix;
@Option(
names = {"-P", "--parallel"},
defaultValue = "1",
description = "Maximum Parallel uploads")
int parallel;
@Option(
names = {"-i", "--include"},
description = "Include only matching paths")
List<String> includes;
@Option(
names = {"-x", "--exclude"},
description = "Exclude matching paths")
List<String> excludes;
@Option(
names = {"-d", "--debug"},
description = "Enable debug logging")
boolean debug;
@Option(
names = {"--no-user"},
description = "Ignore user configuration")
boolean ignoreUserOptions;
@Option(
names = {"--no-global"},
description = "Ignore global configuration")
boolean ignoreGlobalOptions;
}

View file

@ -1,65 +0,0 @@
package net.kemitix.thorp.cli
import java.nio.file.Paths
import scala.jdk.CollectionConverters._
import net.kemitix.thorp.config.{ConfigOption, ConfigOptions}
import scopt.OParser
import zio.Task
object CliArgs {
def parse(args: List[String]): Task[ConfigOptions] = Task {
OParser
.parse(configParser, args, List())
.map(options => ConfigOptions.create(options.asJava))
.getOrElse(ConfigOptions.empty)
}
val configParser: OParser[Unit, List[ConfigOption]] = {
val parserBuilder = OParser.builder[List[ConfigOption]]
import parserBuilder._
OParser.sequence(
programName("thorp"),
head("thorp"),
opt[Unit]('V', "version")
.action((_, cos) => ConfigOption.version() :: cos)
.text("Show version"),
opt[Unit]('B', "batch")
.action((_, cos) => ConfigOption.batchMode() :: cos)
.text("Enable batch-mode"),
opt[String]('s', "source")
.unbounded()
.action((str, cos) => ConfigOption.source(Paths.get(str)) :: cos)
.text("Source directory to sync to destination"),
opt[String]('b', "bucket")
.action((str, cos) => ConfigOption.bucket(str) :: cos)
.text("S3 bucket name"),
opt[String]('p', "prefix")
.action((str, cos) => ConfigOption.prefix(str) :: cos)
.text("Prefix within the S3 Bucket"),
opt[Int]('P', "parallel")
.action((int, cos) => ConfigOption.parallel(int) :: cos)
.text("Maximum Parallel uploads"),
opt[String]('i', "include")
.unbounded()
.action((str, cos) => ConfigOption.include(str) :: cos)
.text("Include only matching paths"),
opt[String]('x', "exclude")
.unbounded()
.action((str, cos) => ConfigOption.exclude(str) :: cos)
.text("Exclude matching paths"),
opt[Unit]('d', "debug")
.action((_, cos) => ConfigOption.debug() :: cos)
.text("Enable debug logging"),
opt[Unit]("no-global")
.action((_, cos) => ConfigOption.ignoreGlobalOptions() :: cos)
.text("Ignore global configuration"),
opt[Unit]("no-user")
.action((_, cos) => ConfigOption.ignoreUserOptions() :: cos)
.text("Ignore user configuration")
)
}
}

View file

@ -0,0 +1,190 @@
package net.kemitix.thorp.cli;
import net.kemitix.thorp.config.ConfigOption;
import net.kemitix.thorp.config.ConfigOptions;
import org.assertj.core.api.WithAssertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CliArgsTest
implements WithAssertions {
@Test
@DisplayName("when no args then empty options")
public void whenNoArgs_thenEmptyOptions() {
assertThat(invoke(new String[]{}))
.isEqualTo(ConfigOptions.empty());
}
@Test
@DisplayName("version")
public void version() {
testOptions(new String[]{"-V", "--version"},
ConfigOption::version);
}
@Test
@DisplayName("batch")
public void batch() {
testOptions(new String[]{"-B", "--batch"},
ConfigOption::batchMode);
}
@Test
@DisplayName("debug")
public void debug() {
testOptions(new String[]{"-d", "--debug"},
ConfigOption::debug);
}
@Test
@DisplayName("ignore user options")
public void ignoreUserOptions() {
testOptions(new String[]{"--no-user"},
ConfigOption::ignoreUserOptions);
}
@Test
@DisplayName("ignore global options")
public void ignoreGlobalOptions() {
testOptions(new String[]{"--no-global"},
ConfigOption::ignoreGlobalOptions);
}
@Test
@DisplayName("bucket")
public void bucket() {
testStringOptions(new String[]{"--bucket"},
ConfigOption::bucket);
}
@Test
@DisplayName("prefix")
public void prefix() {
testStringOptions(new String[]{"--prefix"},
ConfigOption::prefix);
}
@Test
@DisplayName("source")
public void source() {
testPathsOptions(
new String[]{"--source"},
ConfigOption::source);
}
@Test
@DisplayName("include")
public void include() {
testStringListOptions(
new String[]{"--include"},
ConfigOption::include);
}
@Test
@DisplayName("exclude")
public void exclude() {
testStringListOptions(
new String[]{"--exclude"},
ConfigOption::exclude);
}
@Test
@DisplayName("parallel")
public void parallel() {
testIntOptions(
new String[]{"--parallel"},
ConfigOption::parallel);
}
private void testOptions(
String[] parameters,
Supplier<ConfigOption> optionSupplier
) {
assertThat(Arrays.asList(parameters))
.allSatisfy(arg -> assertThat(
invoke(new String[]{arg}))
.isEqualTo(
ConfigOptions.create(
Collections.singletonList(
optionSupplier.get()
))));
}
private void testStringOptions(
String[] parameters,
Function<String, ConfigOption> valueToOption
) {
String value = UUID.randomUUID().toString();
assertThat(Arrays.asList(parameters))
.allSatisfy(arg -> assertThat(
invoke(new String[]{arg, value}))
.isEqualTo(
ConfigOptions.create(
Collections.singletonList(
valueToOption.apply(value)
))));
}
private void testIntOptions(
String[] parameters,
Function<Integer, ConfigOption> valueToOption
) {
Integer value = new Random().nextInt();
assertThat(Arrays.asList(parameters))
.allSatisfy(arg -> assertThat(
invoke(new String[]{arg, "" + value}))
.isEqualTo(
ConfigOptions.create(
Collections.singletonList(
valueToOption.apply(value)
))));
}
private void testPathsOptions(
String[] parameters,
Function<Path, ConfigOption> pathToOption
) {
String value1 = UUID.randomUUID().toString();
String value2 = UUID.randomUUID().toString();
List<ConfigOption> paths = Stream.of(value1, value2)
.map(Paths::get)
.map(pathToOption)
.collect(Collectors.toList());
assertThat(Arrays.asList(parameters))
.allSatisfy(arg -> assertThat(
invoke(new String[]{
arg, value1,
arg, value2}))
.isEqualTo(
ConfigOptions.create(
paths
)));
}
private void testStringListOptions(
String[] parameters,
Function<String, ConfigOption> stringToOption
) {
String value1 = UUID.randomUUID().toString();
String value2 = UUID.randomUUID().toString();
List<ConfigOption> expected = Stream.of(value1, value2)
.map(stringToOption)
.collect(Collectors.toList());
assertThat(Arrays.asList(parameters))
.allSatisfy(arg -> assertThat(
invoke(new String[]{arg, value1, arg, value2}))
.isEqualTo(ConfigOptions.create(expected)));
}
ConfigOptions invoke(String[] args) {
return CliArgs.parse(args);
}
}

View file

@ -1,113 +0,0 @@
package net.kemitix.thorp.cli
import java.nio.file.Paths
import net.kemitix.thorp.config.{ConfigOption, ConfigOptions, ConfigQuery}
import net.kemitix.thorp.filesystem.Resource
import org.scalatest.FunSpec
import zio.DefaultRuntime
import scala.jdk.CollectionConverters._
import scala.util.Try
class CliArgsTest extends FunSpec {
private val runtime = new DefaultRuntime {}
val source = Resource.select(this, "")
describe("parse - source") {
def invokeWithSource(path: String) =
invoke(List("--source", path, "--bucket", "bucket"))
describe("when source is a directory") {
it("should succeed") {
val result = invokeWithSource(pathTo("."))
assert(result.isDefined)
}
}
describe("when source is a relative path to a directory") {
val result = invokeWithSource(pathTo("."))
it("should succeed") { pending }
}
describe("when there are multiple sources") {
val args =
List("--source", "path1", "--source", "path2", "--bucket", "bucket")
it("should get multiple sources") {
val expected = Some(Set("path1", "path2").map(Paths.get(_)))
val configOptions = invoke(args)
val result =
configOptions.map(ConfigQuery.sources(_).paths.asScala.toSet)
assertResult(expected)(result)
}
}
}
describe("parse - debug") {
def invokeWithArgument(arg: String): ConfigOptions = {
val strings = List("--source", pathTo("."), "--bucket", "bucket", arg)
.filter(_ != "")
val maybeOptions = invoke(strings)
maybeOptions.getOrElse(ConfigOptions.empty)
}
val containsDebug = (options: ConfigOptions) =>
options.options.stream().anyMatch(_.isInstanceOf[ConfigOption.Debug])
describe("when no debug flag") {
val configOptions = invokeWithArgument("")
it("debug should be false") {
assertResult(false)(containsDebug(configOptions))
}
}
describe("when long debug flag") {
val configOptions = invokeWithArgument("--debug")
it("debug should be true") {
assert(containsDebug(configOptions))
}
}
describe("when short debug flag") {
val configOptions = invokeWithArgument("-d")
it("debug should be true") {
assert(containsDebug(configOptions))
}
}
}
describe("parse - parallel") {
def invokeWithArguments(args: List[String]): ConfigOptions = {
val strings = List("--source", pathTo("."), "--bucket", "bucket")
.concat(args)
.filter(_ != "")
val maybeOptions = invoke(strings)
maybeOptions.getOrElse(ConfigOptions.empty)
}
describe("when no parallel parameter") {
val configOptions = invokeWithArguments(List.empty[String])
it("should have parallel of 1") {
assertResult(1)(ConfigOptions.parallel(configOptions))
}
}
describe("when parallel parameter given") {
val configOptions = invokeWithArguments(List("--parallel", "5"))
it("should have parallel of 5") {
assertResult(5)(ConfigOptions.parallel(configOptions))
}
}
}
private def pathTo(value: String): String =
Try(Resource.select(this, value))
.map(_.getCanonicalPath)
.getOrElse("[not-found]")
private def invoke(args: List[String]) =
runtime
.unsafeRunSync {
CliArgs.parse(args)
}
.toEither
.toOption
}

View file

@ -103,6 +103,7 @@ public interface ConfigOption {
static ConfigOption batchMode() { static ConfigOption batchMode() {
return new BatchMode(); return new BatchMode();
} }
@EqualsAndHashCode
class BatchMode implements ConfigOption { class BatchMode implements ConfigOption {
@Override @Override
public Configuration update(Configuration config) { public Configuration update(Configuration config) {
@ -117,6 +118,7 @@ public interface ConfigOption {
static ConfigOption version() { static ConfigOption version() {
return new Version(); return new Version();
} }
@EqualsAndHashCode
class Version implements ConfigOption { class Version implements ConfigOption {
@Override @Override
public Configuration update(Configuration config) { public Configuration update(Configuration config) {
@ -131,6 +133,7 @@ public interface ConfigOption {
static ConfigOption ignoreUserOptions() { static ConfigOption ignoreUserOptions() {
return new IgnoreUserOptions(); return new IgnoreUserOptions();
} }
@EqualsAndHashCode
class IgnoreUserOptions implements ConfigOption { class IgnoreUserOptions implements ConfigOption {
@Override @Override
public Configuration update(Configuration config) { public Configuration update(Configuration config) {
@ -145,6 +148,7 @@ public interface ConfigOption {
static ConfigOption ignoreGlobalOptions() { static ConfigOption ignoreGlobalOptions() {
return new IgnoreGlobalOptions(); return new IgnoreGlobalOptions();
} }
@EqualsAndHashCode
class IgnoreGlobalOptions implements ConfigOption { class IgnoreGlobalOptions implements ConfigOption {
@Override @Override
public Configuration update(Configuration config) { public Configuration update(Configuration config) {

View file

@ -2,6 +2,7 @@ package net.kemitix.thorp.config;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.ToString;
import java.util.*; import java.util.*;
@ -25,6 +26,7 @@ public interface ConfigOptions {
static ConfigOptions create(List<ConfigOption> options) { static ConfigOptions create(List<ConfigOption> options) {
return new ConfigOptionsImpl(options); return new ConfigOptionsImpl(options);
} }
@ToString
@EqualsAndHashCode @EqualsAndHashCode
@RequiredArgsConstructor @RequiredArgsConstructor
class ConfigOptionsImpl implements ConfigOptions { class ConfigOptionsImpl implements ConfigOptions {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 236 KiB