Skip to content

Commit 9d27466

Browse files
committed
Support explicit arity min max with @option
- @option now has arityMin/arityMax which if defined, non-negative, are used instead of arity. - Fixes #731
1 parent c36aa41 commit 9d27466

6 files changed

Lines changed: 220 additions & 35 deletions

File tree

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,28 @@
7474
* Define option arity.
7575
*
7676
* @return option arity
77+
* @see #arityMin()
78+
* @see #arityMax()
7779
*/
7880
OptionArity arity() default OptionArity.NONE;
81+
82+
/**
83+
* Define option arity min. If Defined non-negative will be used instead of
84+
* {@link #arity()}. If {@code arityMax} is not set non-negative it is set to
85+
* same as this.
86+
*
87+
* @return option arity min
88+
* @see #arity()
89+
*/
90+
int arityMin() default -1;
91+
92+
/**
93+
* Define option arity max. If Defined non-negative will be used instead of
94+
* {@link #arity()}. If {@code arityMin} is not set non-negative it is set to
95+
* zero.
96+
*
97+
* @return option arity max
98+
* @see #arity()
99+
*/
100+
int arityMax() default -1;
79101
}

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,22 @@ private void onCommandParameter(MethodParameter mp, Builder builder) {
251251
optionSpec.shortNames(shortNames.toArray(new Character[0]));
252252
optionSpec.position(mp.getParameterIndex());
253253
optionSpec.description(so.description());
254-
if (so.arity() != OptionArity.NONE) {
254+
int arityMin = so.arityMin();
255+
int arityMax = so.arityMax();
256+
if (arityMin > -1) {
257+
if (arityMax < arityMin) {
258+
arityMax = arityMin;
259+
}
260+
}
261+
else if (arityMax > -1) {
262+
if (arityMin < 0) {
263+
arityMin = 0;
264+
}
265+
}
266+
if (arityMin > -1 && arityMax > -1) {
267+
optionSpec.arity(arityMin, arityMax);
268+
}
269+
else if (so.arity() != OptionArity.NONE) {
255270
optionSpec.arity(so.arity());
256271
}
257272
else {

spring-shell-core/src/test/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBeanTests.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.shell.Availability;
2525
import org.springframework.shell.AvailabilityProvider;
2626
import org.springframework.shell.command.CommandRegistration;
27+
import org.springframework.shell.command.CommandRegistration.OptionArity;
2728
import org.springframework.shell.command.annotation.Command;
2829
import org.springframework.shell.command.annotation.CommandAvailability;
2930
import org.springframework.shell.command.annotation.Option;
@@ -242,6 +243,78 @@ CompletionProvider completionProvider() {
242243

243244
}
244245

246+
@Test
247+
void setsOptionWithArity() {
248+
configCommon(OptionWithArity.class, new OptionWithArity(), "command1", new Class[] { String.class })
249+
.run((context) -> {
250+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
251+
CommandRegistrationFactoryBean.class);
252+
assertThat(fb).isNotNull();
253+
CommandRegistration registration = fb.getObject();
254+
assertThat(registration).isNotNull();
255+
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(1);
256+
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(1);
257+
});
258+
configCommon(OptionWithArity.class, new OptionWithArity(), "command2", new Class[] { String.class })
259+
.run((context) -> {
260+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
261+
CommandRegistrationFactoryBean.class);
262+
assertThat(fb).isNotNull();
263+
CommandRegistration registration = fb.getObject();
264+
assertThat(registration).isNotNull();
265+
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(1);
266+
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(1);
267+
});
268+
configCommon(OptionWithArity.class, new OptionWithArity(), "command3", new Class[] { String.class })
269+
.run((context) -> {
270+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
271+
CommandRegistrationFactoryBean.class);
272+
assertThat(fb).isNotNull();
273+
CommandRegistration registration = fb.getObject();
274+
assertThat(registration).isNotNull();
275+
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0);
276+
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(2);
277+
});
278+
configCommon(OptionWithArity.class, new OptionWithArity(), "command4", new Class[] { String.class })
279+
.run((context) -> {
280+
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
281+
CommandRegistrationFactoryBean.class);
282+
assertThat(fb).isNotNull();
283+
CommandRegistration registration = fb.getObject();
284+
assertThat(registration).isNotNull();
285+
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0);
286+
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(2);
287+
});
288+
}
289+
290+
@Command
291+
private static class OptionWithArity {
292+
293+
@Command
294+
void command1(@Option(longNames = "arg", arity = OptionArity.EXACTLY_ONE) String arg) {
295+
}
296+
297+
@Command
298+
void command2(@Option(longNames = "arg", arityMin = 1) String arg) {
299+
}
300+
301+
@Command
302+
void command3(@Option(longNames = "arg", arityMax = 2) String arg) {
303+
}
304+
305+
@Command
306+
void command4(@Option(longNames = "arg", arityMax = 2, arity = OptionArity.EXACTLY_ONE) String arg) {
307+
}
308+
309+
@Bean
310+
CompletionProvider completionProvider() {
311+
return ctx -> {
312+
return Collections.emptyList();
313+
};
314+
}
315+
316+
}
317+
245318
private <T> ApplicationContextRunner configCommon(Class<T> type, T bean) {
246319
return configCommon(type, bean, "command", new Class[0]);
247320
}

spring-shell-docs/src/main/asciidoc/using-shell-options-arity.adoc

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,29 @@
22
=== Arity
33
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
44

5-
Sometimes, you want to have more fine control of how many parameters with an option
6-
are processed when parsing operations happen. Arity is defined as min and max
7-
values, where min must be zero or a positive integer and max has to be more or equal to min.
5+
Arity defines how many parameters option parsing takes.
86

9-
====
10-
[source, java, indent=0]
7+
NOTE: There are limitations in a `legacy annotation` compared to `annotation`
8+
and `programmatic` use of arity settings. These are mentioned in notes in
9+
below samples.
10+
11+
[source,java,indent=0,role="primary"]
12+
.Programmatic
1113
----
12-
include::{snippets}/OptionSnippets.java[tag=option-registration-arityints]
14+
include::{snippets}/OptionSnippets.java[tag=option-registration-zeroorone-programmatic]
1315
----
14-
====
1516

16-
Arity can also be defined as an `OptionArity` enum, which are shortcuts
17-
shown in below table <<using-shell-options-arity-optionarity-table>>.
17+
[source,java,indent=0,role="secondary"]
18+
.Annotation
19+
----
20+
include::{snippets}/OptionSnippets.java[tag=option-registration-zeroorone-annotation]
21+
----
1822

19-
====
20-
[source, java, indent=0]
23+
[source,java,indent=0,role="secondary"]
24+
.Legacy Annotation
2125
----
22-
include::{snippets}/OptionSnippets.java[tag=option-registration-arityenum]
26+
include::{snippets}/OptionSnippets.java[tag=option-registration-zeroorone-legacyannotation]
2327
----
24-
====
2528

2629
[[using-shell-options-arity-optionarity-table]]
2730
.OptionArity
@@ -44,26 +47,28 @@ include::{snippets}/OptionSnippets.java[tag=option-registration-arityenum]
4447
|1 / Integer MAX
4548
|===
4649

47-
The annotation model supports defining only the max value of an arity.
4850

49-
====
50-
[source, java, indent=0]
51+
NOTE: `legacy annotation` doesn't support defining minimum arity.
52+
53+
[source,java,indent=0,role="primary"]
54+
.Programmatic
5155
----
52-
include::{snippets}/OptionSnippets.java[tag=option-with-annotation-arity]
56+
include::{snippets}/OptionSnippets.java[tag=option-registration-zerooronewithminmax-programmatic]
5357
----
54-
====
5558

56-
One of a use cases to manually define arity is to impose restrictions how
57-
many parameters option accepts.
59+
[source,java,indent=0,role="secondary"]
60+
.Annotation
61+
----
62+
include::{snippets}/OptionSnippets.java[tag=option-registration-zerooronewithminmax-annotation]
63+
----
5864

59-
====
60-
[source, java, indent=0]
65+
[source,java,indent=0,role="secondary"]
66+
.Legacy Annotation
6167
----
62-
include::{snippets}/OptionSnippets.java[tag=option-registration-aritystrings-sample]
68+
include::{snippets}/OptionSnippets.java[tag=option-registration-zerooronewithminmax-legacyannotation]
6369
----
64-
====
6570

66-
In above example we have option _arg1_ and it's defined as type _String[]_. Arity
71+
In below example we have option _arg1_ and it's defined as type _String[]_. Arity
6772
defines that it needs at least 1 parameter and not more that 2. As seen in below
6873
spesific exceptions _TooManyArgumentsOptionException_ and
6974
_NotEnoughArgumentsOptionException_ are thrown to indicate arity mismatch.

spring-shell-docs/src/test/java/org/springframework/shell/docs/OptionSnippets.java

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,14 @@ public void dump1() {
155155
.build();
156156
// end::option-registration-arityenum[]
157157

158-
// tag::option-registration-arityints[]
159-
CommandRegistration.builder()
160-
.withOption()
161-
.longNames("arg1")
162-
.arity(0, 1)
163-
.and()
164-
.build();
165-
// end::option-registration-arityints[]
158+
// // tag::option-registration-arityints[]
159+
// CommandRegistration.builder()
160+
// .withOption()
161+
// .longNames("arg1")
162+
// .arity(0, 1)
163+
// .and()
164+
// .build();
165+
// // end::option-registration-arityints[]
166166

167167
// tag::option-registration-aritystrings-sample[]
168168
CommandRegistration.builder()
@@ -288,4 +288,74 @@ public void optionNamingSample(
288288

289289
}
290290

291+
static class LegacyAnnotation {
292+
293+
// tag::option-registration-zeroorone-legacyannotation[]
294+
@ShellMethod(key = "example")
295+
String zeroOrOne(
296+
@ShellOption(arity = 1) String arg)
297+
{
298+
return String.format("Hi '%s'", arg);
299+
}
300+
// end::option-registration-zeroorone-legacyannotation[]
301+
302+
// tag::option-registration-zerooronewithminmax-legacyannotation[]
303+
@ShellMethod(key = "example")
304+
String zeroOrOneWithMinMax(
305+
@ShellOption(arity = 1) String arg)
306+
{
307+
return String.format("Hi '%s'", arg);
308+
}
309+
// end::option-registration-zerooronewithminmax-legacyannotation[]
310+
}
311+
312+
static class Annotation {
313+
314+
// tag::option-registration-zeroorone-annotation[]
315+
@Command(command = "example")
316+
String zeroOrOne(
317+
@Option(arity = OptionArity.ZERO_OR_ONE) String arg)
318+
{
319+
return String.format("Hi '%s'", arg);
320+
}
321+
// end::option-registration-zeroorone-annotation[]
322+
323+
// tag::option-registration-zerooronewithminmax-annotation[]
324+
@Command(command = "example")
325+
String zeroOrOneWithMinMax(
326+
@Option(arityMin = 0, arityMax = 1) String arg)
327+
{
328+
return String.format("Hi '%s'", arg);
329+
}
330+
// end::option-registration-zerooronewithminmax-annotation[]
331+
332+
}
333+
334+
static class Registration {
335+
336+
// tag::option-registration-zeroorone-programmatic[]
337+
CommandRegistration zeroOrOne() {
338+
return CommandRegistration.builder()
339+
.command("example")
340+
.withOption()
341+
.longNames("arg")
342+
.arity(OptionArity.ZERO_OR_ONE)
343+
.and()
344+
.build();
345+
}
346+
// end::option-registration-zeroorone-programmatic[]
347+
348+
// tag::option-registration-zerooronewithminmax-programmatic[]
349+
CommandRegistration zeroOrOneWithMinMax() {
350+
return CommandRegistration.builder()
351+
.command("example")
352+
.withOption()
353+
.longNames("arg")
354+
.arity(0, 1)
355+
.and()
356+
.build();
357+
}
358+
// end::option-registration-zerooronewithminmax-programmatic[]
359+
360+
}
291361
}

spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ArityCommands.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public String testArityBooleanDefaultTrueAnnotation(
7373

7474
@Command(command = "arity-string-array")
7575
public String testArityStringArrayAnnotation(
76-
@Option(longNames = "arg1", defaultValue = "true", arity = OptionArity.ZERO_OR_MORE)
76+
@Option(longNames = "arg1", defaultValue = "true", arityMax = 3)
7777
String[] arg1
7878
) {
7979
return "Hello " + Arrays.asList(arg1);

0 commit comments

Comments
 (0)