Skip to content

Commit ad3b96c

Browse files
authored
Introduce default completion provider supporting enumeration values if option is of enum type
Resolves #1282 Signed-off-by: czpilar <[email protected]>
1 parent 5272ecb commit ad3b96c

File tree

5 files changed

+151
-6
lines changed

5 files changed

+151
-6
lines changed

spring-shell-core/src/main/java/org/springframework/shell/core/command/AbstractCommand.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import java.io.PrintWriter;
1919
import java.util.ArrayList;
20-
import java.util.Collections;
2120
import java.util.List;
2221
import java.util.Objects;
2322

@@ -28,6 +27,7 @@
2827
import org.springframework.shell.core.ParameterValidationException;
2928
import org.springframework.shell.core.command.availability.Availability;
3029
import org.springframework.shell.core.command.availability.AvailabilityProvider;
30+
import org.springframework.shell.core.command.completion.DefaultCompletionProvider;
3131
import org.springframework.shell.core.command.exit.ExitStatusExceptionMapper;
3232
import org.springframework.shell.core.command.completion.CompletionProvider;
3333

@@ -54,7 +54,7 @@ public abstract class AbstractCommand implements Command {
5454

5555
@Nullable private ExitStatusExceptionMapper exitStatusExceptionMapper;
5656

57-
private CompletionProvider completionProvider = context -> Collections.emptyList();
57+
private CompletionProvider completionProvider = new DefaultCompletionProvider();
5858

5959
private List<CommandOption> options = new ArrayList<>();
6060

spring-shell-core/src/main/java/org/springframework/shell/core/command/Command.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.shell.core.command.adapter.ConsumerCommandAdapter;
2828
import org.springframework.shell.core.command.adapter.FunctionCommandAdapter;
2929
import org.springframework.shell.core.command.availability.AvailabilityProvider;
30+
import org.springframework.shell.core.command.completion.DefaultCompletionProvider;
3031
import org.springframework.shell.core.command.exit.ExitStatusExceptionMapper;
3132
import org.springframework.shell.core.command.completion.CompletionProvider;
3233
import org.springframework.util.Assert;
@@ -108,7 +109,7 @@ default AvailabilityProvider getAvailabilityProvider() {
108109
* @return the completion provider of the command
109110
*/
110111
default CompletionProvider getCompletionProvider() {
111-
return context -> Collections.emptyList();
112+
return context -> new DefaultCompletionProvider().apply(context);
112113
}
113114

114115
/**
@@ -147,7 +148,7 @@ final class Builder {
147148

148149
private AvailabilityProvider availabilityProvider = AvailabilityProvider.alwaysAvailable();
149150

150-
private CompletionProvider completionProvider = context -> Collections.emptyList();
151+
private CompletionProvider completionProvider = new DefaultCompletionProvider();
151152

152153
@Nullable ExitStatusExceptionMapper exitStatusExceptionMapper;
153154

spring-shell-core/src/main/java/org/springframework/shell/core/command/annotation/support/CommandFactoryBean.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.lang.reflect.Parameter;
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
22-
import java.util.Collections;
2322
import java.util.List;
2423

2524
import jakarta.validation.Validator;
@@ -42,6 +41,7 @@
4241
import org.springframework.shell.core.command.adapter.MethodInvokerCommandAdapter;
4342
import org.springframework.shell.core.command.annotation.Option;
4443
import org.springframework.shell.core.command.availability.AvailabilityProvider;
44+
import org.springframework.shell.core.command.completion.DefaultCompletionProvider;
4545
import org.springframework.shell.core.command.exit.ExitStatusExceptionMapper;
4646
import org.springframework.shell.core.command.completion.CompletionProvider;
4747
import org.springframework.shell.core.utils.Utils;
@@ -142,7 +142,7 @@ private List<CommandOption> getCommandOptions() {
142142
}
143143

144144
private CompletionProvider getCompletionProvider(String completionProviderBeanName) {
145-
CompletionProvider completionProvider = CompletionContext -> Collections.emptyList();
145+
CompletionProvider completionProvider = new DefaultCompletionProvider();
146146
if (!completionProviderBeanName.isEmpty()) {
147147
try {
148148
completionProvider = this.applicationContext.getBean(completionProviderBeanName,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2022-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.core.command.completion;
17+
18+
import org.jspecify.annotations.Nullable;
19+
import org.springframework.shell.core.command.CommandOption;
20+
21+
import java.util.Collections;
22+
import java.util.List;
23+
24+
/**
25+
* A default implementation of the {@link CompletionProvider} interface that provides
26+
* completion proposals for enum-type command options and handles prefix generation for
27+
* command options based on the current input context.
28+
*
29+
* @author David Pilar
30+
* @since 4.0.1
31+
*/
32+
public class DefaultCompletionProvider implements CompletionProvider {
33+
34+
@Override
35+
public List<CompletionProposal> apply(CompletionContext context) {
36+
if (context.getCommandOption() == null || !context.getCommandOption().type().isEnum()) {
37+
return Collections.emptyList();
38+
}
39+
return new EnumCompletionProvider(context.getCommandOption().type(),
40+
getPrefix(context.currentWord(), context.getCommandOption()))
41+
.apply(context);
42+
}
43+
44+
private String getPrefix(@Nullable String currentWord, CommandOption option) {
45+
String prefix = "";
46+
if (currentWord == null) {
47+
return prefix;
48+
}
49+
50+
if (option.longName() != null && currentWord.startsWith("--" + option.longName() + "=")) {
51+
prefix = "--" + option.longName();
52+
}
53+
else if (option.shortName() != ' ' && currentWord.startsWith("-" + option.shortName() + "=")) {
54+
prefix = "-" + option.shortName();
55+
}
56+
return prefix;
57+
}
58+
59+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package org.springframework.shell.core.command.completion;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.params.ParameterizedTest;
5+
import org.junit.jupiter.params.provider.Arguments;
6+
import org.junit.jupiter.params.provider.MethodSource;
7+
import org.springframework.shell.core.command.CommandOption;
8+
9+
import java.util.List;
10+
import java.util.stream.Stream;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
14+
import static org.mockito.Mockito.mock;
15+
import static org.mockito.Mockito.when;
16+
17+
/**
18+
* @author David Pilar
19+
*/
20+
class DefaultCompletionProviderTests {
21+
22+
@Test
23+
void testApplyWithNoCommandOption() {
24+
// given
25+
CompletionContext context = mock(CompletionContext.class);
26+
when(context.getCommandOption()).thenReturn(null);
27+
28+
// when
29+
List<CompletionProposal> result = new DefaultCompletionProvider().apply(context);
30+
31+
// then
32+
assertTrue(result.isEmpty());
33+
}
34+
35+
@Test
36+
void testApplyWithNoEnumCommandOption() {
37+
// given
38+
CompletionContext context = mock(CompletionContext.class);
39+
when(context.getCommandOption()).thenReturn(CommandOption.with().type(String.class).build());
40+
41+
// when
42+
List<CompletionProposal> result = new DefaultCompletionProvider().apply(context);
43+
44+
// then
45+
assertTrue(result.isEmpty());
46+
}
47+
48+
public enum TestEnum {
49+
50+
VALUE1, VALUE2
51+
52+
}
53+
54+
@ParameterizedTest
55+
@MethodSource("applyWithEnumCommandOptionData")
56+
void testApplyWithEnumCommandOption(String currentWord, String longName, char shortName,
57+
List<String> expectedValues) {
58+
// given
59+
CompletionContext context = mock(CompletionContext.class);
60+
when(context.getCommandOption())
61+
.thenReturn(CommandOption.with().longName(longName).shortName(shortName).type(TestEnum.class).build());
62+
when(context.currentWord()).thenReturn(currentWord);
63+
64+
// when
65+
List<CompletionProposal> result = new DefaultCompletionProvider().apply(context);
66+
List<String> resultValues = result.stream().map(CompletionProposal::value).toList();
67+
68+
// then
69+
assertEquals(expectedValues, resultValues);
70+
}
71+
72+
static Stream<Arguments> applyWithEnumCommandOptionData() {
73+
return Stream.of(Arguments.of(null, "name", 'n', List.of("VALUE1", "VALUE2")),
74+
Arguments.of("", "name", 'n', List.of("VALUE1", "VALUE2")),
75+
Arguments.of("different", "name", 'n', List.of("VALUE1", "VALUE2")),
76+
Arguments.of("VA", "name", 'n', List.of("VALUE1", "VALUE2")),
77+
78+
Arguments.of("--name=", "name", 'n', List.of("--name=VALUE1", "--name=VALUE2")),
79+
Arguments.of("-n=", "name", 'n', List.of("-n=VALUE1", "-n=VALUE2")),
80+
81+
Arguments.of("--name=", null, 'n', List.of("VALUE1", "VALUE2")),
82+
Arguments.of("-n=", "name", ' ', List.of("VALUE1", "VALUE2")));
83+
}
84+
85+
}

0 commit comments

Comments
 (0)