Rework completion interfaces

- Use same interface type in a generic interactive completions
  in a method level and option value level.
- Change CompletionResolver to have same function signature
  as with options and use CompletionContext to keep
  relevant information.
- Fixes #449
This commit is contained in:
Janne Valkealahti
2022-07-06 09:02:06 +01:00
parent 2520e0681f
commit 32f77c1917
9 changed files with 57 additions and 47 deletions

View File

@@ -1,3 +1,18 @@
/*
* 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.boot;
import java.util.ArrayList;
@@ -11,7 +26,7 @@ import org.springframework.shell.command.ArgumentHeaderMethodArgumentResolver;
import org.springframework.shell.command.CommandContextMethodArgumentResolver;
import org.springframework.shell.command.CommandExecution.CommandExecutionHandlerMethodArgumentResolvers;
import org.springframework.shell.completion.CompletionResolver;
import org.springframework.shell.completion.DefaultCompletionResolver;
import org.springframework.shell.completion.RegistrationOptionsCompletionResolver;
import org.springframework.shell.config.ShellConversionServiceSupplier;
import org.springframework.shell.standard.ShellOptionMethodArgumentResolver;
@@ -20,7 +35,7 @@ public class ParameterResolverAutoConfiguration {
@Bean
public CompletionResolver defaultCompletionResolver() {
return new DefaultCompletionResolver();
return new RegistrationOptionsCompletionResolver();
}
@Bean

View File

@@ -105,7 +105,17 @@ public class CompletionContext {
position, commandRegistration, commandOption);
}
/**
* Return a copy of this context with given command option.
*/
public CompletionContext commandOption(CommandOption commandOption) {
return new CompletionContext(words, wordIndex, position, commandRegistration, commandOption);
}
/**
* Return a copy of this context with given command registration.
*/
public CompletionContext commandRegistration(CommandRegistration commandRegistration) {
return new CompletionContext(words, wordIndex, position, commandRegistration, commandOption);
}
}

View File

@@ -277,11 +277,12 @@ public class Shell {
String best = findLongestCommand(prefix);
if (best != null) {
CompletionContext argsContext = context.drop(best.split(" ").length);
context = context.drop(best.split(" ").length);
CommandRegistration registration = commandRegistry.getRegistrations().get(best);
CompletionContext argsContext = context.commandRegistration(registration);
for (CompletionResolver resolver : completionResolvers) {
List<CompletionProposal> resolved = resolver.resolve(registration, argsContext);
List<CompletionProposal> resolved = resolver.apply(argsContext);
candidates.addAll(resolved);
}

View File

@@ -15,12 +15,8 @@
*/
package org.springframework.shell.command;
import java.util.List;
import java.util.function.Function;
import org.springframework.core.ResolvableType;
import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal;
import org.springframework.shell.completion.CompletionResolver;
/**
* Interface representing an option in a command.
@@ -104,7 +100,7 @@ public interface CommandOption {
*
* @return the completion function
*/
Function<CompletionContext, List<CompletionProposal>> getCompletion();
CompletionResolver getCompletion();
/**
* Gets an instance of a default {@link CommandOption}.
@@ -150,7 +146,7 @@ public interface CommandOption {
*/
public static CommandOption of(String[] longNames, Character[] shortNames, String description,
ResolvableType type, boolean required, String defaultValue, Integer position, Integer arityMin,
Integer arityMax, String label, Function<CompletionContext, List<CompletionProposal>> completion) {
Integer arityMax, String label, CompletionResolver completion) {
return new DefaultCommandOption(longNames, shortNames, description, type, required, defaultValue, position,
arityMin, arityMax, label, completion);
}
@@ -170,12 +166,12 @@ public interface CommandOption {
private int arityMin;
private int arityMax;
private String label;
private Function<CompletionContext, List<CompletionProposal>> completion;
private CompletionResolver completion;
public DefaultCommandOption(String[] longNames, Character[] shortNames, String description,
ResolvableType type, boolean required, String defaultValue, Integer position,
Integer arityMin, Integer arityMax, String label,
Function<CompletionContext, List<CompletionProposal>> completion) {
CompletionResolver completion) {
this.longNames = longNames != null ? longNames : new String[0];
this.shortNames = shortNames != null ? shortNames : new Character[0];
this.description = description;
@@ -240,7 +236,7 @@ public interface CommandOption {
}
@Override
public Function<CompletionContext, List<CompletionProposal>> getCompletion() {
public CompletionResolver getCompletion() {
return completion;
}
}

View File

@@ -29,8 +29,7 @@ import java.util.stream.Stream;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.shell.Availability;
import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal;
import org.springframework.shell.completion.CompletionResolver;
import org.springframework.shell.context.InteractionMode;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -216,7 +215,7 @@ public interface CommandRegistration {
* @param completion the completion function
* @return option spec for chaining
*/
OptionSpec completion(Function<CompletionContext, List<CompletionProposal>> completion);
OptionSpec completion(CompletionResolver completion);
/**
* Return a builder for chaining.
@@ -538,7 +537,7 @@ public interface CommandRegistration {
private Integer arityMin;
private Integer arityMax;
private String label;
private Function<CompletionContext, List<CompletionProposal>> completion;
private CompletionResolver completion;
DefaultOptionSpec(BaseBuilder builder) {
this.builder = builder;
@@ -638,7 +637,7 @@ public interface CommandRegistration {
}
@Override
public OptionSpec completion(Function<CompletionContext, List<CompletionProposal>> completion) {
public OptionSpec completion(CompletionResolver completion) {
this.completion = completion;
return this;
}
@@ -688,7 +687,7 @@ public interface CommandRegistration {
return label;
}
public Function<CompletionContext, List<CompletionProposal>> getCompletion() {
public CompletionResolver getCompletion() {
return completion;
}
}

View File

@@ -16,24 +16,16 @@
package org.springframework.shell.completion;
import java.util.List;
import java.util.function.Function;
import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal;
import org.springframework.shell.command.CommandRegistration;
/**
* Interface resolving completion proposals.
*
* @author Janne Valkealahti
*/
public interface CompletionResolver {
/**
* Resolve completions.
*
* @param registration the command registration
* @param context the completion context
* @return list of resolved completions
*/
List<CompletionProposal> resolve(CommandRegistration registration, CompletionContext context);
@FunctionalInterface
public interface CompletionResolver extends Function<CompletionContext, List<CompletionProposal>> {
}

View File

@@ -16,28 +16,31 @@
package org.springframework.shell.completion;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal;
import org.springframework.shell.command.CommandRegistration;
/**
* Default implementation of a {@link CompletionResolver}.
*
* @author Janne Valkealahti
*/
public class DefaultCompletionResolver implements CompletionResolver {
public class RegistrationOptionsCompletionResolver implements CompletionResolver {
@Override
public List<CompletionProposal> resolve(CommandRegistration registration, CompletionContext context) {
public List<CompletionProposal> apply(CompletionContext context) {
if (context.getCommandRegistration() == null) {
return Collections.emptyList();
}
List<CompletionProposal> candidates = new ArrayList<>();
registration.getOptions().stream()
context.getCommandRegistration().getOptions().stream()
.flatMap(o -> Stream.of(o.getLongNames()))
.map(ln -> new CompletionProposal("--" + ln))
.forEach(candidates::add);
registration.getOptions().stream()
context.getCommandRegistration().getOptions().stream()
.flatMap(o -> Stream.of(o.getShortNames()))
.map(ln -> new CompletionProposal("-" + ln))
.forEach(candidates::add);

View File

@@ -30,7 +30,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.completion.CompletionResolver;
import org.springframework.shell.completion.RegistrationOptionsCompletionResolver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
@@ -56,9 +56,6 @@ public class ShellTests {
@Mock
CommandCatalog commandRegistry;
@Mock
private CompletionResolver completionResolver;
@InjectMocks
private Shell shell;
@@ -66,7 +63,7 @@ public class ShellTests {
@BeforeEach
public void setUp() {
shell.setCompletionResolvers(Arrays.asList(completionResolver));
shell.setCompletionResolvers(Arrays.asList(new RegistrationOptionsCompletionResolver()));
}
@Test
@@ -272,7 +269,6 @@ public class ShellTests {
@Test
public void completionArgWithMethod() throws Exception {
when(completionResolver.resolve(any(), any())).thenReturn(Arrays.asList(new CompletionProposal("--arg1")));
CommandRegistration registration1 = CommandRegistration.builder()
.command("hello world")
.withTarget()
@@ -294,7 +290,6 @@ public class ShellTests {
@Test
public void completionArgWithFunction() throws Exception {
when(completionResolver.resolve(any(), any())).thenReturn(Arrays.asList(new CompletionProposal("--arg1")));
CommandRegistration registration1 = CommandRegistration.builder()
.command("hello world")
.withTarget()

View File

@@ -23,7 +23,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -37,7 +36,6 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import org.springframework.shell.Availability;
import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal;
import org.springframework.shell.MethodTargetRegistrar;
import org.springframework.shell.Utils;
@@ -45,6 +43,7 @@ import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.command.CommandRegistration.Builder;
import org.springframework.shell.command.CommandRegistration.OptionSpec;
import org.springframework.shell.completion.CompletionResolver;
import org.springframework.shell.standard.ShellOption.NoValueProvider;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -149,12 +148,12 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, App
optionSpec.required();
}
if (!ClassUtils.isAssignable(NoValueProvider.class, so.valueProvider())) {
Function<CompletionContext, List<CompletionProposal>> completionFunction = ctx -> {
CompletionResolver completionResolver = ctx -> {
ValueProvider valueProviderBean = this.applicationContext.getBean(so.valueProvider());
List<CompletionProposal> complete = valueProviderBean.complete(ctx);
return complete;
};
optionSpec.completion(completionFunction);
optionSpec.completion(completionResolver);
}
}
}