Rework help command

- Change help command output to get templated using
  model classes.
- Remove things around ParameterDescription as those are
  replaced with template classes.
- Fixes for native configs.
- For now availability and aliases are removed from
  help to get back in better form.
- Aliases has been partly introduced to structure.
- Fixes #422
This commit is contained in:
Janne Valkealahti
2022-05-26 07:45:35 +01:00
committed by GitHub
parent eed1d84653
commit bd9ab62013
33 changed files with 1055 additions and 794 deletions

View File

@@ -1,181 +0,0 @@
/*
* Copyright 2015 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.shell;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.springframework.core.MethodParameter;
import javax.validation.metadata.ElementDescriptor;
/**
* Encapsulates information about a shell invokable method parameter, so that it can be documented.
*
* <p>Instances of this class are constructed by {@link ParameterResolver#describe(MethodParameter)}.</p>
*
* @author Eric Bottard
*/
public class ParameterDescription {
/**
* A string representation of the type of the parameter.
*/
private String type;
/**
* A string representation of the parameter, as it should appear in a parameter list.
* If not provided, this is derived from the parameter type.
*/
private String formal;
/**
* A string representation of the default value (if the option is left out entirely) for the parameter, if any.
*/
private Optional<String> defaultValue = Optional.empty();
/**
* A string representation of the default value for this parameter, if it can be used as a mere flag (<em>e.g.</em>
* {@literal --force} without a value, being an equivalent to {@literal --force true}).
* <p>{@literal Optional.empty()} (the default) means that this parameter cannot be used as a flag.</p>
*/
private Optional<String> defaultValueWhenFlag = Optional.empty();
/**
* The list of 'keys' that can be used to specify this parameter, if any.
*/
private List<String> keys = Collections.emptyList();
/**
* Depending on the {@link ParameterResolver}, whether keys are mandatory to identify this parameter.
*/
private boolean mandatoryKey = true;
/**
* A short description of this parameter.
*/
private String help = "";
/**
* Allows discovery of bean validation constraints for the command parameter.
* <p>Note that most often, constraints will directly come from parameter constraints,
* but sometimes (<em>e.g.</em> in case of one method argument mapping to multiple
* command options) may come from property constraints.</p>
*/
private ElementDescriptor elementDescriptor;
public void type(String type) {
this.type = type;
}
public ParameterDescription help(String help) {
this.help = help;
return this;
}
public boolean mandatoryKey() {
return mandatoryKey;
}
public List<String> keys() {
return keys;
}
public Optional<String> defaultValue() {
return defaultValue;
}
public ParameterDescription defaultValue(String defaultValue) {
this.defaultValue = Optional.ofNullable(defaultValue);
return this;
}
public ParameterDescription whenFlag(String defaultValue) {
this.defaultValueWhenFlag = Optional.of(defaultValue);
return this;
}
public ParameterDescription keys(List<String> keys) {
this.keys = keys;
return this;
}
public ParameterDescription mandatoryKey(boolean mandatoryKey) {
this.mandatoryKey = mandatoryKey;
return this;
}
/**
* @return an ElementDescriptor used to discover constraints. May be {@literal null}.
*/
public ElementDescriptor elementDescriptor() {
return this.elementDescriptor;
}
public ParameterDescription elementDescriptor(ElementDescriptor descriptor) {
this.elementDescriptor = descriptor;
return this;
}
public String type() {
return type;
}
public String formal() {
return formal;
}
public String help() {
return help;
}
public Optional<String> defaultValueWhenFlag() {
return defaultValueWhenFlag;
}
public ParameterDescription formal(String formal) {
this.formal = formal;
return this;
}
@Override
public String toString() {
return String.format("%s %s", keys.isEmpty() ? "" : keys().iterator().next(), formal());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ParameterDescription that = (ParameterDescription) o;
return mandatoryKey == that.mandatoryKey &&
// Objects.equals(parameter, that.parameter) &&
Objects.equals(type, that.type) &&
Objects.equals(formal, that.formal) &&
Objects.equals(defaultValue, that.defaultValue) &&
Objects.equals(defaultValueWhenFlag, that.defaultValueWhenFlag) &&
Objects.equals(keys, that.keys) &&
Objects.equals(help, that.help);
}
@Override
public int hashCode() {
return Objects.hash(type, formal, defaultValue, defaultValueWhenFlag, keys, mandatoryKey, help);
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2015 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.shell;
/**
* Thrown by a {@link ParameterResolver} when a parameter that should have been set has been left out altogether.
*
* @author Eric Bottard
*/
public class ParameterMissingResolutionException extends RuntimeException {
private final ParameterDescription parameterDescription;
public ParameterMissingResolutionException(ParameterDescription parameterDescription) {
this.parameterDescription = parameterDescription;
}
public ParameterDescription getParameterDescription() {
return parameterDescription;
}
@Override
public String getMessage() {
return String.format("Parameter '%s' should be specified", parameterDescription);
}
}

View File

@@ -35,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.shell.command.CommandAlias;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandExecution;
import org.springframework.shell.command.CommandExecution.CommandExecutionException;
@@ -155,7 +156,15 @@ public class Shell {
Optional<CommandRegistration> commandRegistration = commandRegistry.getRegistrations().values().stream()
.filter(r -> {
return r.getCommand().equals(command);
if (r.getCommand().equals(command)) {
return true;
}
for (CommandAlias a : r.getAliases()) {
if (a.getCommand().equals(command)) {
return true;
}
}
return false;
})
.findFirst();
@@ -255,14 +264,12 @@ public class Shell {
// Workaround for https://github.com/spring-projects/spring-shell/issues/150
// (sadly, this ties this class to JLine somehow)
int lastWordStart = prefix.lastIndexOf(' ') + 1;
return commandRegistry.getRegistrations().values().stream()
.filter(r -> {
return r.getCommand().startsWith(prefix);
})
.map(r -> {
String c = r.getCommand();
return commandRegistry.getRegistrations().entrySet().stream()
.filter(e -> e.getKey().startsWith(prefix))
.map(e -> {
String c = e.getKey();
c = c.substring(lastWordStart);
return toCommandProposal(c, r);
return toCommandProposal(c, e.getValue());
})
.collect(Collectors.toList());
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright 2016 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.shell;
/**
* Thrown during parameter resolution, when a parameter has been identified, but could not be correctly resolved.
*
* @author Eric Bottard
*/
public class UnfinishedParameterResolutionException extends RuntimeException {
private final ParameterDescription parameterDescription;
private final CharSequence input;
public UnfinishedParameterResolutionException(ParameterDescription parameterDescription, CharSequence input) {
this.parameterDescription = parameterDescription;
this.input = input;
}
public ParameterDescription getParameterDescription() {
return parameterDescription;
}
public CharSequence getInput() {
return input;
}
@Override
public String getMessage() {
return String.format("Error trying to resolve '%s' using [%s]", parameterDescription, input);
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2022 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.shell.command;
/**
* Interface representing an alias in a command.
*
* @author Janne Valkealahti
*/
public interface CommandAlias {
/**
* Gets a command an alias.
*
* @return command
*/
String getCommand();
/**
* Get group for an alias.
*
* @return the group
*/
String getGroup();
/**
* Gets an instance of a default {@link CommandAlias}.
*
* @param command the command
* @param group the group
* @return default command alias
*/
public static CommandAlias of(String command, String group) {
return new DefaultCommandAlias(command, group);
}
/**
* Default implementation of {@link CommandAlias}.
*/
public static class DefaultCommandAlias implements CommandAlias {
private final String command;
private final String group;
public DefaultCommandAlias(String command, String group) {
this.command = command;
this.group = group;
}
@Override
public String getCommand() {
return command;
}
@Override
public String getGroup() {
return group;
}
}
}

View File

@@ -103,6 +103,9 @@ public interface CommandCatalog {
for (CommandRegistration r : registration) {
String commandName = r.getCommand();
commandRegistrations.put(commandName, r);
for (CommandAlias a : r.getAliases()) {
commandRegistrations.put(a.getCommand(), r);
}
}
}
@@ -111,6 +114,9 @@ public interface CommandCatalog {
for (CommandRegistration r : registration) {
String commandName = r.getCommand();
commandRegistrations.remove(commandName);
for (CommandAlias a : r.getAliases()) {
commandRegistrations.remove(a.getCommand());
}
}
}
@@ -156,13 +162,5 @@ public interface CommandCatalog {
return true;
};
}
// private static String commandName(String[] commands) {
// return Arrays.asList(commands).stream()
// .flatMap(c -> Stream.of(c.split(" ")))
// .filter(c -> StringUtils.hasText(c))
// .map(c -> c.trim())
// .collect(Collectors.joining(" "));
// }
}
}

View File

@@ -91,6 +91,13 @@ public interface CommandRegistration {
*/
List<CommandOption> getOptions();
/**
* Gets an aliases.
*
* @return the aliases
*/
List<CommandAlias> getAliases();
/**
* Gets a new instance of a {@link Buidler}.
*
@@ -350,6 +357,35 @@ public interface CommandRegistration {
Builder and();
}
/**
* Spec defining an alias.
*/
public interface AliasSpec {
/**
* Define commands for an alias.
*
* @param commands the commands
* @return a target spec for chaining
*/
AliasSpec command(String... commands);
/**
* Define group for an alias.
*
* @param group the group
* @return a target spec for chaining
*/
AliasSpec group(String group);
/**
* Return a builder for chaining.
*
* @return a builder for chaining
*/
Builder and();
}
/**
* Builder interface for {@link CommandRegistration}.
*/
@@ -413,6 +449,13 @@ public interface CommandRegistration {
*/
TargetSpec withTarget();
/**
* Define an alias what this command should execute
*
* @return alias spec for chaining
*/
AliasSpec withAlias();
/**
* Builds a {@link CommandRegistration}.
*
@@ -612,6 +655,40 @@ public interface CommandRegistration {
}
}
static class DefaultAliasSpec implements AliasSpec {
private BaseBuilder builder;
private String[] commands;
private String group;
DefaultAliasSpec(BaseBuilder builder) {
this.builder = builder;
}
@Override
public AliasSpec command(String... commands) {
Assert.notNull(commands, "commands must be set");
this.commands = Arrays.asList(commands).stream()
.flatMap(c -> Stream.of(c.split(" ")))
.filter(c -> StringUtils.hasText(c))
.map(c -> c.trim())
.collect(Collectors.toList())
.toArray(new String[0]);
return this;
}
@Override
public AliasSpec group(String group) {
this.group = group;
return this;
}
@Override
public Builder and() {
return builder;
}
}
static class DefaultCommandRegistration implements CommandRegistration {
private String command;
@@ -621,10 +698,11 @@ public interface CommandRegistration {
private Supplier<Availability> availability;
private List<DefaultOptionSpec> optionSpecs;
private DefaultTargetSpec targetSpec;
private List<DefaultAliasSpec> aliasSpecs;
public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String group,
String description, Supplier<Availability> availability, List<DefaultOptionSpec> optionSpecs,
DefaultTargetSpec targetSpec) {
DefaultTargetSpec targetSpec, List<DefaultAliasSpec> aliasSpecs) {
this.command = commandArrayToName(commands);
this.interactionMode = interactionMode;
this.group = group;
@@ -632,6 +710,7 @@ public interface CommandRegistration {
this.availability = availability;
this.optionSpecs = optionSpecs;
this.targetSpec = targetSpec;
this.aliasSpecs = aliasSpecs;
}
@Override
@@ -681,6 +760,15 @@ public interface CommandRegistration {
throw new IllegalArgumentException("No bean, function or consumer defined");
}
@Override
public List<CommandAlias> getAliases() {
return this.aliasSpecs.stream()
.map(spec -> {
return CommandAlias.of(commandArrayToName(spec.commands), spec.group);
})
.collect(Collectors.toList());
}
private static String commandArrayToName(String[] commands) {
return Arrays.asList(commands).stream()
.flatMap(c -> Stream.of(c.split(" ")))
@@ -702,6 +790,7 @@ public interface CommandRegistration {
private String description;
private Supplier<Availability> availability;
private List<DefaultOptionSpec> optionSpecs = new ArrayList<>();
private List<DefaultAliasSpec> aliasSpecs = new ArrayList<>();
private DefaultTargetSpec targetSpec;
@Override
@@ -754,13 +843,20 @@ public interface CommandRegistration {
return spec;
}
@Override
public AliasSpec withAlias() {
DefaultAliasSpec spec = new DefaultAliasSpec(this);
this.aliasSpecs.add(spec);
return spec;
};
@Override
public CommandRegistration build() {
Assert.notNull(commands, "command cannot be empty");
Assert.notNull(targetSpec, "target cannot be empty");
Assert.state(!(targetSpec.bean != null && targetSpec.function != null), "only one target can exist");
return new DefaultCommandRegistration(commands, interactionMode, group, description, availability,
optionSpecs, targetSpec);
optionSpecs, targetSpec, aliasSpecs);
}
}
}

View File

@@ -40,6 +40,22 @@ public class CommandCatalogTests extends AbstractCommandTests {
assertThat(catalog.getRegistrations()).hasSize(0);
}
@Test
public void testCommandAliases () {
CommandRegistration r1 = CommandRegistration.builder()
.command("group1 sub1")
.withAlias()
.command("group1 sub2")
.and()
.withTarget()
.function(function1)
.and()
.build();
CommandCatalog catalog = CommandCatalog.of();
catalog.register(r1);
assertThat(catalog.getRegistrations()).hasSize(2);
}
@Test
public void testResolver() {
// catalog itself would not have any registered command but

View File

@@ -354,4 +354,31 @@ public class CommandRegistrationTests extends AbstractCommandTests {
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0);
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(0);
}
@Test
public void testAliases() {
CommandRegistration registration = CommandRegistration.builder()
.command("command1")
.group("Test Group")
.withAlias()
.command("alias1")
.group("Alias Group")
.and()
.withAlias()
.command("alias2")
.group("Alias Group")
.and()
.withTarget()
.function(function1)
.and()
.build();
assertThat(registration.getCommand()).isEqualTo("command1");
assertThat(registration.getGroup()).isEqualTo("Test Group");
assertThat(registration.getAliases()).hasSize(2);
assertThat(registration.getAliases().stream().map(CommandAlias::getCommand)).containsExactlyInAnyOrder("alias1",
"alias2");
assertThat(registration.getAliases().get(0).getGroup()).isEqualTo("Alias Group");
assertThat(registration.getAliases().get(1).getGroup()).isEqualTo("Alias Group");
}
}