Skip to content

Commit

Permalink
Merge pull request #12 from rundeck/enh/refactor-path
Browse files Browse the repository at this point in the history
Add path option to SubCommand, allows locating subcommands with exist…
  • Loading branch information
gschueler authored Feb 24, 2020
2 parents b9bdd48 + fff4e4e commit 3747335
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 0 deletions.
6 changes: 6 additions & 0 deletions toolbelt/src/main/java/org/rundeck/toolbelt/SubCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SubCommand {
/**
* Defines a relative path of subcommands that should be the parent of this command
*
* @return path of subcommand parents
*/
String[] path() default {};
}
67 changes: 67 additions & 0 deletions toolbelt/src/main/java/org/rundeck/toolbelt/ToolBelt.java
Original file line number Diff line number Diff line change
Expand Up @@ -663,10 +663,77 @@ private void introspect(final Object instance) {

/**
* determine container/commands from annotations on an object, and add to the command set.
*
* @param parent
* @param instance
*/
private void introspect(CommandSet parent, final Object instance) {
Class<?> aClass = instance.getClass();
SubCommand annotation1 = aClass.getAnnotation(SubCommand.class);
List<String> path = new ArrayList<>();
if (null != annotation1 && annotation1.path().length > 0) {
path.addAll(Arrays.asList(annotation1.path()));
}
if (path.size() > 0) {
try {
parent = locatePath(parent, path);
} catch (InvalidPath invalidPath) {
throw new RuntimeException(String.format(
"Unable to define subcommand object of type %s at path: '%s': %s",
instance.getClass().getName(),
String.join(" ", path),
invalidPath.getMessage()
), invalidPath);
}
}
addCommandForParent(parent, instance);
}

/**
* Given a parent and a path, return a CommandSet matching the path
*
* @param parent
* @param path
*/
private CommandSet locatePath(final CommandSet parent, final List<String> path) throws InvalidPath {
if (path == null || path.size() < 1) {
return parent;
}
String part = path.get(0);
CommandSet sub = null;
CommandInvoker commandInvoker = parent.commands.get(part);
if (null == commandInvoker) {

CommandSet commandSet = new CommandSet(part);
commandSet.hidden = false;
commandSet.context = commands.context;
commandSet.helpCommands = helpCommands;

parent.commands.put(part, commandSet);
sub = commandSet;
} else if (commandInvoker instanceof CommandSet) {
sub = (CommandSet) commandInvoker;
} else {
//TODO: construct a commandset and add invoker as default command
throw new InvalidPath(String.format(
"The subcommand at path: '%s' cannot be extended",
String.join(" ", path)
));
}
ArrayList<String> subpath = new ArrayList<>(path);
subpath.remove(0);
return locatePath(sub, subpath);
}

static class InvalidPath
extends Exception
{
public InvalidPath(final String message) {
super(message);
}
}

private void addCommandForParent(CommandSet parent, final Object instance) {
HashMap<String, CommandInvoker> subCommands = new HashMap<>();
HashMap<String, CommandInvoker> subSynonyms = new HashMap<>();
//look for methods
Expand Down
128 changes: 128 additions & 0 deletions toolbelt/src/test/groovy/org/rundeck/toolbelt/ToolBeltSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,36 @@ class ToolBeltSpec extends Specification {
}
}

@Command
@SubCommand(path = ["test1"])
class SubCmd1 {
Set<String> runMethods = []

@Command(description = "sub 1")
public void amethod(@Arg("name") String name, @Arg("age") Integer age, @Arg("leaving") Boolean leaving) {
runMethods << 'amethod'

}

@Command(description = "sub 2")
public void bmethod(@Arg("name") String name, @Arg("age") Integer age, @Arg("leaving") Boolean leaving) {
runMethods << 'bmethod'
}
}

@Command
@SubCommand(path = ["mytool1"])
class SubCmd2 extends SubCmd1{

}


@Command
@SubCommand(path = ["mytool1", "greet"])
class SubCmd3 extends SubCmd1{

}

class MyTool5 implements HasSubCommands {

@Override
Expand Down Expand Up @@ -204,6 +234,104 @@ class ToolBeltSpec extends Specification {
output.output.contains 'Use "test [command] help" to get help on any command.'
}

def "subcommand with path"() {
given:
def test1 = new MyTool1()
def test2 = new SubCmd1()
def output = new TestOutput()
def tool = ToolBelt.with('test', output, test1, test2)
when:
def result = tool.runMain(['-h'] as String[], false)
then:
result == false
output.output.contains "Available commands:\n"
output.output.contains ' mytool1 - '
output.output.contains ' test1 - '
output.output.contains 'Use "test [command] help" to get help on any command.'
when:
output.output = []
def result2 = tool.runMain(['test1', '-h'] as String[], false)
then:
result2 == false
output.output.contains "Available commands:\n"
output.output.contains ' amethod - sub 1'
output.output.contains ' bmethod - sub 2'
output.output.contains 'Use "test1 [command] help" to get help on any command.'
}

def "subcommand with path can run"() {
given:
def test1 = new MyTool1()
def test2 = new SubCmd1()
def output = new TestOutput()
def tool = ToolBelt.with('test', output, test1, test2)
when:
def result = tool.runMain(['test1',method] as String[], false)
then:
result
test2.runMethods.contains method

where:
method << ['amethod','bmethod']
}


def "subcommand with path extending existing path"() {
given:
def test1 = new MyTool1()
def test2 = new SubCmd2()
def output = new TestOutput()
def tool = ToolBelt.with('test', output, test1, test2)
when:
def result = tool.runMain(['-h'] as String[], false)
then:
result == false
output.output.contains "Available commands:\n"
output.output.contains ' amethod - sub 1'
output.output.contains ' bmethod - sub 2'
output.output.contains ' greet - '
output.output.contains 'Use "mytool1 [command] help" to get help on any command.'
when:
output.output = []
def result2 = tool.runMain(['mytool1', '-h'] as String[], false)
then:
result2 == false
output.output.contains "Available commands:\n"
output.output.contains ' amethod - sub 1'
output.output.contains ' bmethod - sub 2'
output.output.contains ' greet - '
output.output.contains 'Use "mytool1 [command] help" to get help on any command.'
}
def "subcommand with path extending existing path can run"() {
given:
def test1 = new MyTool1()
def test2 = new SubCmd2()
def output = new TestOutput()
def tool = ToolBelt.with('test', output, test1, test2)
when:
def result = tool.runMain(['mytool1',method] as String[], false)
then:
result
test2.runMethods.contains method

where:
method << ['amethod','bmethod']
}


def "subcommand with path extending terminal path fails"() {
given:
def test1 = new MyTool1()
def test2 = new SubCmd3()
def output = new TestOutput()
when:
def tool = ToolBelt.with('test', output, test1, test2)
then:
RuntimeException e = thrown()
e.message.contains("Unable to define subcommand object of type")
e.message.contains("at path: 'mytool1 greet': The subcommand at path: 'greet' cannot be extended")
}

def "only hassubcommands"() {
given:
def test = new MyTool5()
Expand Down

0 comments on commit 3747335

Please sign in to comment.