[cli] Completions command for suggestions (#213)

* [cli] Completions command for suggestions

This takes airlift's 'suggest' command and reuses it as a different
command name, 'completion'. This gives us in-built CLI completions which
are useful in the repo-level container's docker-entrypoint.sh. This
previously parsed Java files for conventional usage of Command
annotations, which is potentially buggy. The new implementation relies
only on CLI to provide command completion suggestions.

As part of this, we can prepare for bash completion scripts which can be
added to our homebrew formula.  The new completion command will also
complete on command options, for example:

cli completion generate

This will provide all short and long form switches available to the
generate command.

* Add piggyback license onto licensed of file borrowed from airlift/airline
This commit is contained in:
Jim Schubert 2018-06-10 07:33:00 -04:00 committed by William Cheng
parent 43b60e667e
commit 640fd87e3b
3 changed files with 126 additions and 17 deletions

View File

@ -8,11 +8,19 @@ JAVA_OPTS=${JAVA_OPTS:-"-Xmx1024M -DloggerPath=conf/log4j.properties"}
cli="${GEN_DIR}/modules/openapi-generator-cli" cli="${GEN_DIR}/modules/openapi-generator-cli"
codegen="${cli}/target/openapi-generator-cli.jar" codegen="${cli}/target/openapi-generator-cli.jar"
cmdsrc="${cli}/src/main/java/org/openapitools/codegen/cmd"
pattern="@Command(name = \"$1\"" # We code in a list of commands here as source processing is potentially buggy (requires undocumented conventional use of annotations).
if expr "x$1" : 'x[a-z][a-z-]*$' > /dev/null && fgrep -qe "$pattern" "$cmdsrc"/*.java || expr "$1" = 'help' > /dev/null; then # A list of known commands helps us determine if we should compile CLI. There's an edge-case where a new command not added to this
# If ${GEN_DIR} has been mapped elsewhere from default, and that location has not been built # list won't be considered a "real" command. We can get around that a bit by checking CLI completions beforehand if it exists.
commands="list,generate,meta,langs,help,config-help,validate,version"
# if CLI jar exists, check $1 against completions available in the CLI
if [[ -f "${codegen}" && -n "$(java ${JAVA_OPTS} -jar "${codegen}" completion | grep "^$1\$" )" ]]; then
command=$1
shift
exec java ${JAVA_OPTS} -jar "${codegen}" "${command}" "$@"
elif [[ -n "$(echo commands | tr ',' '\n' | grep "^$1\$" )" ]]; then
# If CLI jar does not exist, and $1 is a known CLI command, build the CLI jar and run that command.
if [[ ! -f "${codegen}" ]]; then if [[ ! -f "${codegen}" ]]; then
(cd "${GEN_DIR}" && exec mvn -am -pl "modules/openapi-generator-cli" -Duser.home=$(dirname $MAVEN_CONFIG) package) (cd "${GEN_DIR}" && exec mvn -am -pl "modules/openapi-generator-cli" -Duser.home=$(dirname $MAVEN_CONFIG) package)
fi fi
@ -20,5 +28,6 @@ if expr "x$1" : 'x[a-z][a-z-]*$' > /dev/null && fgrep -qe "$pattern" "$cmdsrc"/*
shift shift
exec java ${JAVA_OPTS} -jar "${codegen}" "${command}" "$@" exec java ${JAVA_OPTS} -jar "${codegen}" "${command}" "$@"
else else
# Pass args as linux commands. This allows us to do something like: docker run -it (-e…, -v…) image ls -la
exec "$@" exec "$@"
fi fi

View File

@ -23,8 +23,6 @@ import io.airlift.airline.ParseOptionMissingException;
import io.airlift.airline.ParseOptionMissingValueException; import io.airlift.airline.ParseOptionMissingValueException;
import org.openapitools.codegen.cmd.*; import org.openapitools.codegen.cmd.*;
import java.util.Arrays;
/** /**
* User: lanwen Date: 24.03.15 Time: 17:56 * User: lanwen Date: 24.03.15 Time: 17:56
* <p> * <p>
@ -52,21 +50,23 @@ public class OpenAPIGenerator {
Help.class, Help.class,
ConfigHelp.class, ConfigHelp.class,
Validate.class, Validate.class,
Version.class Version.class,
CompletionCommand.class
); );
// If CLI is run without a command, consider this an error.
// We can check against empty args because unrecognized arguments/commands result in an exception.
// This is useful to exit with status 1, for example, so that misconfigured scripts fail fast.
// We don't want the default command to exit internally with status 1 because when the default command is something like "list",
// it would prevent scripting using the command directly. Example:
// java -jar cli.jar list --short | tr ',' '\n' | xargs -I{} echo "Doing something with {}"
if (args.length == 0) {
System.exit(1);
}
try { try {
builder.build().parse(args).run(); builder.build().parse(args).run();
// If CLI is run without a command, consider this an error. This exists after initial parse/run
// so we can present the configured "default command".
// We can check against empty args because unrecognized arguments/commands result in an exception.
// This is useful to exit with status 1, for example, so that misconfigured scripts fail fast.
// We don't want the default command to exit internally with status 1 because when the default command is something like "list",
// it would prevent scripting using the command directly. Example:
// java -jar cli.jar list --short | tr ',' '\n' | xargs -I{} echo "Doing something with {}"
if (args.length == 0) {
System.exit(1);
}
} catch (ParseOptionMissingException | ParseOptionMissingValueException e) { } catch (ParseOptionMissingException | ParseOptionMissingValueException e) {
System.err.printf("[error] %s%n", e.getMessage()); System.err.printf("[error] %s%n", e.getMessage());
System.exit(1); System.exit(1);

View File

@ -0,0 +1,100 @@
/*
* Copyright (C) 2010 the original author or authors.
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
*
* Licensed 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.
*/
/*
* NOTICE: File originally taken from:
* https://github.com/airlift/airline/blob/fc7a55e34b6361cb97235de5a1b21cba9b508f4b/src/main/java/io/airlift/airline/SuggestCommand.java#L1
* Modifications have been made to fit the needs of OpenAPI Tools CLI.
*/
package org.openapitools.codegen.cmd;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.airlift.airline.*;
import io.airlift.airline.model.*;
import javax.inject.Inject;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import static com.google.common.collect.Lists.newArrayList;
import static io.airlift.airline.ParserUtil.createInstance;
@Command(name = "completion", description = "Complete commands (for using in tooling such as Bash Completions).", hidden = true)
public class CompletionCommand
implements Runnable, Callable<Void> {
private static final Map<Context, Class<? extends Suggester>> BUILTIN_SUGGESTERS = ImmutableMap.<Context, Class<? extends Suggester>>builder()
.put(Context.GLOBAL, GlobalSuggester.class)
.put(Context.GROUP, GroupSuggester.class)
.put(Context.COMMAND, CommandSuggester.class)
.build();
@Inject
public GlobalMetadata metadata;
@Arguments
public List<String> arguments = newArrayList();
@Override
public Void call() {
run();
return null;
}
@VisibleForTesting
public Iterable<String> generateSuggestions() {
Parser parser = new Parser();
ParseState state = parser.parse(metadata, arguments);
Class<? extends Suggester> suggesterClass = BUILTIN_SUGGESTERS.get(state.getLocation());
if (suggesterClass != null) {
SuggesterMetadata suggesterMetadata = MetadataLoader.loadSuggester(suggesterClass);
if (suggesterMetadata != null) {
ImmutableMap.Builder<Class<?>, Object> bindings = ImmutableMap.<Class<?>, Object>builder()
.put(GlobalMetadata.class, metadata);
if (state.getGroup() != null) {
bindings.put(CommandGroupMetadata.class, state.getGroup());
}
if (state.getCommand() != null) {
bindings.put(CommandMetadata.class, state.getCommand());
}
Suggester suggester = createInstance(suggesterMetadata.getSuggesterClass(),
ImmutableList.<OptionMetadata>of(),
null,
null,
null,
suggesterMetadata.getMetadataInjections(),
bindings.build());
return suggester.suggest();
}
}
return ImmutableList.of();
}
@Override
public void run() {
System.out.println(Joiner.on("\n").join(generateSuggestions()));
}
}