Skip to content

Commit 640fd87

Browse files
jimschubertwing328
authored andcommitted
[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
1 parent 43b60e6 commit 640fd87

File tree

3 files changed

+126
-17
lines changed

3 files changed

+126
-17
lines changed

docker-entrypoint.sh

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,26 @@ JAVA_OPTS=${JAVA_OPTS:-"-Xmx1024M -DloggerPath=conf/log4j.properties"}
88

99
cli="${GEN_DIR}/modules/openapi-generator-cli"
1010
codegen="${cli}/target/openapi-generator-cli.jar"
11-
cmdsrc="${cli}/src/main/java/org/openapitools/codegen/cmd"
1211

13-
pattern="@Command(name = \"$1\""
14-
if expr "x$1" : 'x[a-z][a-z-]*$' > /dev/null && fgrep -qe "$pattern" "$cmdsrc"/*.java || expr "$1" = 'help' > /dev/null; then
15-
# If ${GEN_DIR} has been mapped elsewhere from default, and that location has not been built
12+
# We code in a list of commands here as source processing is potentially buggy (requires undocumented conventional use of annotations).
13+
# 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
14+
# list won't be considered a "real" command. We can get around that a bit by checking CLI completions beforehand if it exists.
15+
commands="list,generate,meta,langs,help,config-help,validate,version"
16+
17+
# if CLI jar exists, check $1 against completions available in the CLI
18+
if [[ -f "${codegen}" && -n "$(java ${JAVA_OPTS} -jar "${codegen}" completion | grep "^$1\$" )" ]]; then
19+
command=$1
20+
shift
21+
exec java ${JAVA_OPTS} -jar "${codegen}" "${command}" "$@"
22+
elif [[ -n "$(echo commands | tr ',' '\n' | grep "^$1\$" )" ]]; then
23+
# If CLI jar does not exist, and $1 is a known CLI command, build the CLI jar and run that command.
1624
if [[ ! -f "${codegen}" ]]; then
1725
(cd "${GEN_DIR}" && exec mvn -am -pl "modules/openapi-generator-cli" -Duser.home=$(dirname $MAVEN_CONFIG) package)
1826
fi
1927
command=$1
2028
shift
2129
exec java ${JAVA_OPTS} -jar "${codegen}" "${command}" "$@"
2230
else
31+
# Pass args as linux commands. This allows us to do something like: docker run -it (-e…, -v…) image ls -la
2332
exec "$@"
2433
fi

modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/OpenAPIGenerator.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
import io.airlift.airline.ParseOptionMissingValueException;
2424
import org.openapitools.codegen.cmd.*;
2525

26-
import java.util.Arrays;
27-
2826
/**
2927
* User: lanwen Date: 24.03.15 Time: 17:56
3028
* <p>
@@ -52,21 +50,23 @@ public static void main(String[] args) {
5250
Help.class,
5351
ConfigHelp.class,
5452
Validate.class,
55-
Version.class
53+
Version.class,
54+
CompletionCommand.class
5655
);
5756

58-
// If CLI is run without a command, consider this an error.
59-
// We can check against empty args because unrecognized arguments/commands result in an exception.
60-
// This is useful to exit with status 1, for example, so that misconfigured scripts fail fast.
61-
// We don't want the default command to exit internally with status 1 because when the default command is something like "list",
62-
// it would prevent scripting using the command directly. Example:
63-
// java -jar cli.jar list --short | tr ',' '\n' | xargs -I{} echo "Doing something with {}"
64-
if (args.length == 0) {
65-
System.exit(1);
66-
}
67-
6857
try {
6958
builder.build().parse(args).run();
59+
60+
// If CLI is run without a command, consider this an error. This exists after initial parse/run
61+
// so we can present the configured "default command".
62+
// We can check against empty args because unrecognized arguments/commands result in an exception.
63+
// This is useful to exit with status 1, for example, so that misconfigured scripts fail fast.
64+
// We don't want the default command to exit internally with status 1 because when the default command is something like "list",
65+
// it would prevent scripting using the command directly. Example:
66+
// java -jar cli.jar list --short | tr ',' '\n' | xargs -I{} echo "Doing something with {}"
67+
if (args.length == 0) {
68+
System.exit(1);
69+
}
7070
} catch (ParseOptionMissingException | ParseOptionMissingValueException e) {
7171
System.err.printf("[error] %s%n", e.getMessage());
7272
System.exit(1);
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright (C) 2010 the original author or authors.
3+
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
/*
18+
* NOTICE: File originally taken from:
19+
* https://github.com/airlift/airline/blob/fc7a55e34b6361cb97235de5a1b21cba9b508f4b/src/main/java/io/airlift/airline/SuggestCommand.java#L1
20+
* Modifications have been made to fit the needs of OpenAPI Tools CLI.
21+
*/
22+
package org.openapitools.codegen.cmd;
23+
24+
import com.google.common.annotations.VisibleForTesting;
25+
import com.google.common.base.Joiner;
26+
import com.google.common.collect.ImmutableList;
27+
import com.google.common.collect.ImmutableMap;
28+
import io.airlift.airline.*;
29+
import io.airlift.airline.model.*;
30+
31+
import javax.inject.Inject;
32+
import java.util.List;
33+
import java.util.Map;
34+
import java.util.concurrent.Callable;
35+
36+
import static com.google.common.collect.Lists.newArrayList;
37+
import static io.airlift.airline.ParserUtil.createInstance;
38+
39+
@Command(name = "completion", description = "Complete commands (for using in tooling such as Bash Completions).", hidden = true)
40+
public class CompletionCommand
41+
implements Runnable, Callable<Void> {
42+
private static final Map<Context, Class<? extends Suggester>> BUILTIN_SUGGESTERS = ImmutableMap.<Context, Class<? extends Suggester>>builder()
43+
.put(Context.GLOBAL, GlobalSuggester.class)
44+
.put(Context.GROUP, GroupSuggester.class)
45+
.put(Context.COMMAND, CommandSuggester.class)
46+
.build();
47+
48+
@Inject
49+
public GlobalMetadata metadata;
50+
51+
@Arguments
52+
public List<String> arguments = newArrayList();
53+
54+
@Override
55+
public Void call() {
56+
run();
57+
return null;
58+
}
59+
60+
@VisibleForTesting
61+
public Iterable<String> generateSuggestions() {
62+
Parser parser = new Parser();
63+
ParseState state = parser.parse(metadata, arguments);
64+
65+
Class<? extends Suggester> suggesterClass = BUILTIN_SUGGESTERS.get(state.getLocation());
66+
if (suggesterClass != null) {
67+
SuggesterMetadata suggesterMetadata = MetadataLoader.loadSuggester(suggesterClass);
68+
69+
if (suggesterMetadata != null) {
70+
ImmutableMap.Builder<Class<?>, Object> bindings = ImmutableMap.<Class<?>, Object>builder()
71+
.put(GlobalMetadata.class, metadata);
72+
73+
if (state.getGroup() != null) {
74+
bindings.put(CommandGroupMetadata.class, state.getGroup());
75+
}
76+
77+
if (state.getCommand() != null) {
78+
bindings.put(CommandMetadata.class, state.getCommand());
79+
}
80+
81+
Suggester suggester = createInstance(suggesterMetadata.getSuggesterClass(),
82+
ImmutableList.<OptionMetadata>of(),
83+
null,
84+
null,
85+
null,
86+
suggesterMetadata.getMetadataInjections(),
87+
bindings.build());
88+
89+
return suggester.suggest();
90+
}
91+
}
92+
93+
return ImmutableList.of();
94+
}
95+
96+
@Override
97+
public void run() {
98+
System.out.println(Joiner.on("\n").join(generateSuggestions()));
99+
}
100+
}

0 commit comments

Comments
 (0)