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

View File

@@ -105,7 +105,17 @@ public class CompletionContext {
position, commandRegistration, commandOption); position, commandRegistration, commandOption);
} }
/**
* Return a copy of this context with given command option.
*/
public CompletionContext commandOption(CommandOption commandOption) { public CompletionContext commandOption(CommandOption commandOption) {
return new CompletionContext(words, wordIndex, position, commandRegistration, 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); String best = findLongestCommand(prefix);
if (best != null) { if (best != null) {
CompletionContext argsContext = context.drop(best.split(" ").length); context = context.drop(best.split(" ").length);
CommandRegistration registration = commandRegistry.getRegistrations().get(best); CommandRegistration registration = commandRegistry.getRegistrations().get(best);
CompletionContext argsContext = context.commandRegistration(registration);
for (CompletionResolver resolver : completionResolvers) { for (CompletionResolver resolver : completionResolvers) {
List<CompletionProposal> resolved = resolver.resolve(registration, argsContext); List<CompletionProposal> resolved = resolver.apply(argsContext);
candidates.addAll(resolved); candidates.addAll(resolved);
} }

View File

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

View File

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

View File

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

View File

@@ -16,28 +16,31 @@
package org.springframework.shell.completion; package org.springframework.shell.completion;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.springframework.shell.CompletionContext; import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal; import org.springframework.shell.CompletionProposal;
import org.springframework.shell.command.CommandRegistration;
/** /**
* Default implementation of a {@link CompletionResolver}. * Default implementation of a {@link CompletionResolver}.
* *
* @author Janne Valkealahti * @author Janne Valkealahti
*/ */
public class DefaultCompletionResolver implements CompletionResolver { public class RegistrationOptionsCompletionResolver implements CompletionResolver {
@Override @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<>(); List<CompletionProposal> candidates = new ArrayList<>();
registration.getOptions().stream() context.getCommandRegistration().getOptions().stream()
.flatMap(o -> Stream.of(o.getLongNames())) .flatMap(o -> Stream.of(o.getLongNames()))
.map(ln -> new CompletionProposal("--" + ln)) .map(ln -> new CompletionProposal("--" + ln))
.forEach(candidates::add); .forEach(candidates::add);
registration.getOptions().stream() context.getCommandRegistration().getOptions().stream()
.flatMap(o -> Stream.of(o.getShortNames())) .flatMap(o -> Stream.of(o.getShortNames()))
.map(ln -> new CompletionProposal("-" + ln)) .map(ln -> new CompletionProposal("-" + ln))
.forEach(candidates::add); .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.CommandCatalog;
import org.springframework.shell.command.CommandRegistration; 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.assertThat;
import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.Assertions.fail;
@@ -56,9 +56,6 @@ public class ShellTests {
@Mock @Mock
CommandCatalog commandRegistry; CommandCatalog commandRegistry;
@Mock
private CompletionResolver completionResolver;
@InjectMocks @InjectMocks
private Shell shell; private Shell shell;
@@ -66,7 +63,7 @@ public class ShellTests {
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
shell.setCompletionResolvers(Arrays.asList(completionResolver)); shell.setCompletionResolvers(Arrays.asList(new RegistrationOptionsCompletionResolver()));
} }
@Test @Test
@@ -272,7 +269,6 @@ public class ShellTests {
@Test @Test
public void completionArgWithMethod() throws Exception { public void completionArgWithMethod() throws Exception {
when(completionResolver.resolve(any(), any())).thenReturn(Arrays.asList(new CompletionProposal("--arg1")));
CommandRegistration registration1 = CommandRegistration.builder() CommandRegistration registration1 = CommandRegistration.builder()
.command("hello world") .command("hello world")
.withTarget() .withTarget()
@@ -294,7 +290,6 @@ public class ShellTests {
@Test @Test
public void completionArgWithFunction() throws Exception { public void completionArgWithFunction() throws Exception {
when(completionResolver.resolve(any(), any())).thenReturn(Arrays.asList(new CompletionProposal("--arg1")));
CommandRegistration registration1 = CommandRegistration.builder() CommandRegistration registration1 = CommandRegistration.builder()
.command("hello world") .command("hello world")
.withTarget() .withTarget()

View File

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