diff --git a/spring-shell-core/src/main/java/org/springframework/shell/core/command/AbstractCommand.java b/spring-shell-core/src/main/java/org/springframework/shell/core/command/AbstractCommand.java index 6e6f397c7..42214ccbd 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/core/command/AbstractCommand.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/core/command/AbstractCommand.java @@ -17,7 +17,6 @@ import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; @@ -28,6 +27,7 @@ import org.springframework.shell.core.ParameterValidationException; import org.springframework.shell.core.command.availability.Availability; import org.springframework.shell.core.command.availability.AvailabilityProvider; +import org.springframework.shell.core.command.completion.DefaultCompletionProvider; import org.springframework.shell.core.command.exit.ExitStatusExceptionMapper; import org.springframework.shell.core.command.completion.CompletionProvider; @@ -54,7 +54,7 @@ public abstract class AbstractCommand implements Command { @Nullable private ExitStatusExceptionMapper exitStatusExceptionMapper; - private CompletionProvider completionProvider = context -> Collections.emptyList(); + private CompletionProvider completionProvider = new DefaultCompletionProvider(); private List options = new ArrayList<>(); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/core/command/Command.java b/spring-shell-core/src/main/java/org/springframework/shell/core/command/Command.java index f36d291bf..fc536808e 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/core/command/Command.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/core/command/Command.java @@ -27,6 +27,7 @@ import org.springframework.shell.core.command.adapter.ConsumerCommandAdapter; import org.springframework.shell.core.command.adapter.FunctionCommandAdapter; import org.springframework.shell.core.command.availability.AvailabilityProvider; +import org.springframework.shell.core.command.completion.DefaultCompletionProvider; import org.springframework.shell.core.command.exit.ExitStatusExceptionMapper; import org.springframework.shell.core.command.completion.CompletionProvider; import org.springframework.util.Assert; @@ -108,7 +109,7 @@ default AvailabilityProvider getAvailabilityProvider() { * @return the completion provider of the command */ default CompletionProvider getCompletionProvider() { - return context -> Collections.emptyList(); + return context -> new DefaultCompletionProvider().apply(context); } /** @@ -147,7 +148,7 @@ final class Builder { private AvailabilityProvider availabilityProvider = AvailabilityProvider.alwaysAvailable(); - private CompletionProvider completionProvider = context -> Collections.emptyList(); + private CompletionProvider completionProvider = new DefaultCompletionProvider(); @Nullable ExitStatusExceptionMapper exitStatusExceptionMapper; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/core/command/annotation/support/CommandFactoryBean.java b/spring-shell-core/src/main/java/org/springframework/shell/core/command/annotation/support/CommandFactoryBean.java index d3532e375..569a41dda 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/core/command/annotation/support/CommandFactoryBean.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/core/command/annotation/support/CommandFactoryBean.java @@ -19,7 +19,6 @@ import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import jakarta.validation.Validator; @@ -42,6 +41,7 @@ import org.springframework.shell.core.command.adapter.MethodInvokerCommandAdapter; import org.springframework.shell.core.command.annotation.Option; import org.springframework.shell.core.command.availability.AvailabilityProvider; +import org.springframework.shell.core.command.completion.DefaultCompletionProvider; import org.springframework.shell.core.command.exit.ExitStatusExceptionMapper; import org.springframework.shell.core.command.completion.CompletionProvider; import org.springframework.shell.core.utils.Utils; @@ -142,7 +142,7 @@ private List getCommandOptions() { } private CompletionProvider getCompletionProvider(String completionProviderBeanName) { - CompletionProvider completionProvider = CompletionContext -> Collections.emptyList(); + CompletionProvider completionProvider = new DefaultCompletionProvider(); if (!completionProviderBeanName.isEmpty()) { try { completionProvider = this.applicationContext.getBean(completionProviderBeanName, diff --git a/spring-shell-core/src/main/java/org/springframework/shell/core/command/completion/DefaultCompletionProvider.java b/spring-shell-core/src/main/java/org/springframework/shell/core/command/completion/DefaultCompletionProvider.java new file mode 100644 index 000000000..18f1b0390 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/core/command/completion/DefaultCompletionProvider.java @@ -0,0 +1,59 @@ +/* + * Copyright 2022-present 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.core.command.completion; + +import org.jspecify.annotations.Nullable; +import org.springframework.shell.core.command.CommandOption; + +import java.util.Collections; +import java.util.List; + +/** + * A default implementation of the {@link CompletionProvider} interface that provides + * completion proposals for enum-type command options and handles prefix generation for + * command options based on the current input context. + * + * @author David Pilar + * @since 4.0.1 + */ +public class DefaultCompletionProvider implements CompletionProvider { + + @Override + public List apply(CompletionContext context) { + if (context.getCommandOption() == null || !context.getCommandOption().type().isEnum()) { + return Collections.emptyList(); + } + return new EnumCompletionProvider(context.getCommandOption().type(), + getPrefix(context.currentWord(), context.getCommandOption())) + .apply(context); + } + + private String getPrefix(@Nullable String currentWord, CommandOption option) { + String prefix = ""; + if (currentWord == null) { + return prefix; + } + + if (option.longName() != null && currentWord.startsWith("--" + option.longName() + "=")) { + prefix = "--" + option.longName(); + } + else if (option.shortName() != ' ' && currentWord.startsWith("-" + option.shortName() + "=")) { + prefix = "-" + option.shortName(); + } + return prefix; + } + +} diff --git a/spring-shell-core/src/test/java/org/springframework/shell/core/command/completion/DefaultCompletionProviderTests.java b/spring-shell-core/src/test/java/org/springframework/shell/core/command/completion/DefaultCompletionProviderTests.java new file mode 100644 index 000000000..4865d6f9b --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/core/command/completion/DefaultCompletionProviderTests.java @@ -0,0 +1,85 @@ +package org.springframework.shell.core.command.completion; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.shell.core.command.CommandOption; + +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author David Pilar + */ +class DefaultCompletionProviderTests { + + @Test + void testApplyWithNoCommandOption() { + // given + CompletionContext context = mock(CompletionContext.class); + when(context.getCommandOption()).thenReturn(null); + + // when + List result = new DefaultCompletionProvider().apply(context); + + // then + assertTrue(result.isEmpty()); + } + + @Test + void testApplyWithNoEnumCommandOption() { + // given + CompletionContext context = mock(CompletionContext.class); + when(context.getCommandOption()).thenReturn(CommandOption.with().type(String.class).build()); + + // when + List result = new DefaultCompletionProvider().apply(context); + + // then + assertTrue(result.isEmpty()); + } + + public enum TestEnum { + + VALUE1, VALUE2 + + } + + @ParameterizedTest + @MethodSource("applyWithEnumCommandOptionData") + void testApplyWithEnumCommandOption(String currentWord, String longName, char shortName, + List expectedValues) { + // given + CompletionContext context = mock(CompletionContext.class); + when(context.getCommandOption()) + .thenReturn(CommandOption.with().longName(longName).shortName(shortName).type(TestEnum.class).build()); + when(context.currentWord()).thenReturn(currentWord); + + // when + List result = new DefaultCompletionProvider().apply(context); + List resultValues = result.stream().map(CompletionProposal::value).toList(); + + // then + assertEquals(expectedValues, resultValues); + } + + static Stream applyWithEnumCommandOptionData() { + return Stream.of(Arguments.of(null, "name", 'n', List.of("VALUE1", "VALUE2")), + Arguments.of("", "name", 'n', List.of("VALUE1", "VALUE2")), + Arguments.of("different", "name", 'n', List.of("VALUE1", "VALUE2")), + Arguments.of("VA", "name", 'n', List.of("VALUE1", "VALUE2")), + + Arguments.of("--name=", "name", 'n', List.of("--name=VALUE1", "--name=VALUE2")), + Arguments.of("-n=", "name", 'n', List.of("-n=VALUE1", "-n=VALUE2")), + + Arguments.of("--name=", null, 'n', List.of("VALUE1", "VALUE2")), + Arguments.of("-n=", "name", ' ', List.of("VALUE1", "VALUE2"))); + } + +} \ No newline at end of file