Support explicit arity min max with @Option

- @Option now has arityMin/arityMax which if defined,
  non-negative, are used instead of arity.
- Fixes #731
This commit is contained in:
Janne Valkealahti
2023-05-01 08:36:41 +01:00
parent c36aa41878
commit 9d27466c6a
6 changed files with 225 additions and 40 deletions

View File

@@ -74,6 +74,28 @@ public @interface Option {
* Define option arity.
*
* @return option arity
* @see #arityMin()
* @see #arityMax()
*/
OptionArity arity() default OptionArity.NONE;
/**
* Define option arity min. If Defined non-negative will be used instead of
* {@link #arity()}. If {@code arityMax} is not set non-negative it is set to
* same as this.
*
* @return option arity min
* @see #arity()
*/
int arityMin() default -1;
/**
* Define option arity max. If Defined non-negative will be used instead of
* {@link #arity()}. If {@code arityMin} is not set non-negative it is set to
* zero.
*
* @return option arity max
* @see #arity()
*/
int arityMax() default -1;
}

View File

@@ -251,7 +251,22 @@ class CommandRegistrationFactoryBean implements FactoryBean<CommandRegistration>
optionSpec.shortNames(shortNames.toArray(new Character[0]));
optionSpec.position(mp.getParameterIndex());
optionSpec.description(so.description());
if (so.arity() != OptionArity.NONE) {
int arityMin = so.arityMin();
int arityMax = so.arityMax();
if (arityMin > -1) {
if (arityMax < arityMin) {
arityMax = arityMin;
}
}
else if (arityMax > -1) {
if (arityMin < 0) {
arityMin = 0;
}
}
if (arityMin > -1 && arityMax > -1) {
optionSpec.arity(arityMin, arityMax);
}
else if (so.arity() != OptionArity.NONE) {
optionSpec.arity(so.arity());
}
else {

View File

@@ -24,6 +24,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.shell.Availability;
import org.springframework.shell.AvailabilityProvider;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.command.CommandRegistration.OptionArity;
import org.springframework.shell.command.annotation.Command;
import org.springframework.shell.command.annotation.CommandAvailability;
import org.springframework.shell.command.annotation.Option;
@@ -242,6 +243,78 @@ class CommandRegistrationFactoryBeanTests {
}
@Test
void setsOptionWithArity() {
configCommon(OptionWithArity.class, new OptionWithArity(), "command1", new Class[] { String.class })
.run((context) -> {
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
CommandRegistrationFactoryBean.class);
assertThat(fb).isNotNull();
CommandRegistration registration = fb.getObject();
assertThat(registration).isNotNull();
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(1);
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(1);
});
configCommon(OptionWithArity.class, new OptionWithArity(), "command2", new Class[] { String.class })
.run((context) -> {
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
CommandRegistrationFactoryBean.class);
assertThat(fb).isNotNull();
CommandRegistration registration = fb.getObject();
assertThat(registration).isNotNull();
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(1);
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(1);
});
configCommon(OptionWithArity.class, new OptionWithArity(), "command3", new Class[] { String.class })
.run((context) -> {
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
CommandRegistrationFactoryBean.class);
assertThat(fb).isNotNull();
CommandRegistration registration = fb.getObject();
assertThat(registration).isNotNull();
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0);
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(2);
});
configCommon(OptionWithArity.class, new OptionWithArity(), "command4", new Class[] { String.class })
.run((context) -> {
CommandRegistrationFactoryBean fb = context.getBean(FACTORYBEANREF,
CommandRegistrationFactoryBean.class);
assertThat(fb).isNotNull();
CommandRegistration registration = fb.getObject();
assertThat(registration).isNotNull();
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0);
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(2);
});
}
@Command
private static class OptionWithArity {
@Command
void command1(@Option(longNames = "arg", arity = OptionArity.EXACTLY_ONE) String arg) {
}
@Command
void command2(@Option(longNames = "arg", arityMin = 1) String arg) {
}
@Command
void command3(@Option(longNames = "arg", arityMax = 2) String arg) {
}
@Command
void command4(@Option(longNames = "arg", arityMax = 2, arity = OptionArity.EXACTLY_ONE) String arg) {
}
@Bean
CompletionProvider completionProvider() {
return ctx -> {
return Collections.emptyList();
};
}
}
private <T> ApplicationContextRunner configCommon(Class<T> type, T bean) {
return configCommon(type, bean, "command", new Class[0]);
}

View File

@@ -2,26 +2,29 @@
=== Arity
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
Sometimes, you want to have more fine control of how many parameters with an option
are processed when parsing operations happen. Arity is defined as min and max
values, where min must be zero or a positive integer and max has to be more or equal to min.
Arity defines how many parameters option parsing takes.
====
[source, java, indent=0]
----
include::{snippets}/OptionSnippets.java[tag=option-registration-arityints]
----
====
NOTE: There are limitations in a `legacy annotation` compared to `annotation`
and `programmatic` use of arity settings. These are mentioned in notes in
below samples.
Arity can also be defined as an `OptionArity` enum, which are shortcuts
shown in below table <<using-shell-options-arity-optionarity-table>>.
[source,java,indent=0,role="primary"]
.Programmatic
----
include::{snippets}/OptionSnippets.java[tag=option-registration-zeroorone-programmatic]
----
====
[source, java, indent=0]
[source,java,indent=0,role="secondary"]
.Annotation
----
include::{snippets}/OptionSnippets.java[tag=option-registration-arityenum]
include::{snippets}/OptionSnippets.java[tag=option-registration-zeroorone-annotation]
----
[source,java,indent=0,role="secondary"]
.Legacy Annotation
----
include::{snippets}/OptionSnippets.java[tag=option-registration-zeroorone-legacyannotation]
----
====
[[using-shell-options-arity-optionarity-table]]
.OptionArity
@@ -44,26 +47,28 @@ include::{snippets}/OptionSnippets.java[tag=option-registration-arityenum]
|1 / Integer MAX
|===
The annotation model supports defining only the max value of an arity.
====
[source, java, indent=0]
----
include::{snippets}/OptionSnippets.java[tag=option-with-annotation-arity]
----
====
NOTE: `legacy annotation` doesn't support defining minimum arity.
One of a use cases to manually define arity is to impose restrictions how
many parameters option accepts.
====
[source, java, indent=0]
[source,java,indent=0,role="primary"]
.Programmatic
----
include::{snippets}/OptionSnippets.java[tag=option-registration-aritystrings-sample]
include::{snippets}/OptionSnippets.java[tag=option-registration-zerooronewithminmax-programmatic]
----
====
In above example we have option _arg1_ and it's defined as type _String[]_. Arity
[source,java,indent=0,role="secondary"]
.Annotation
----
include::{snippets}/OptionSnippets.java[tag=option-registration-zerooronewithminmax-annotation]
----
[source,java,indent=0,role="secondary"]
.Legacy Annotation
----
include::{snippets}/OptionSnippets.java[tag=option-registration-zerooronewithminmax-legacyannotation]
----
In below example we have option _arg1_ and it's defined as type _String[]_. Arity
defines that it needs at least 1 parameter and not more that 2. As seen in below
spesific exceptions _TooManyArgumentsOptionException_ and
_NotEnoughArgumentsOptionException_ are thrown to indicate arity mismatch.

View File

@@ -155,14 +155,14 @@ public class OptionSnippets {
.build();
// end::option-registration-arityenum[]
// tag::option-registration-arityints[]
CommandRegistration.builder()
.withOption()
.longNames("arg1")
.arity(0, 1)
.and()
.build();
// end::option-registration-arityints[]
// // tag::option-registration-arityints[]
// CommandRegistration.builder()
// .withOption()
// .longNames("arg1")
// .arity(0, 1)
// .and()
// .build();
// // end::option-registration-arityints[]
// tag::option-registration-aritystrings-sample[]
CommandRegistration.builder()
@@ -288,4 +288,74 @@ public class OptionSnippets {
}
static class LegacyAnnotation {
// tag::option-registration-zeroorone-legacyannotation[]
@ShellMethod(key = "example")
String zeroOrOne(
@ShellOption(arity = 1) String arg)
{
return String.format("Hi '%s'", arg);
}
// end::option-registration-zeroorone-legacyannotation[]
// tag::option-registration-zerooronewithminmax-legacyannotation[]
@ShellMethod(key = "example")
String zeroOrOneWithMinMax(
@ShellOption(arity = 1) String arg)
{
return String.format("Hi '%s'", arg);
}
// end::option-registration-zerooronewithminmax-legacyannotation[]
}
static class Annotation {
// tag::option-registration-zeroorone-annotation[]
@Command(command = "example")
String zeroOrOne(
@Option(arity = OptionArity.ZERO_OR_ONE) String arg)
{
return String.format("Hi '%s'", arg);
}
// end::option-registration-zeroorone-annotation[]
// tag::option-registration-zerooronewithminmax-annotation[]
@Command(command = "example")
String zeroOrOneWithMinMax(
@Option(arityMin = 0, arityMax = 1) String arg)
{
return String.format("Hi '%s'", arg);
}
// end::option-registration-zerooronewithminmax-annotation[]
}
static class Registration {
// tag::option-registration-zeroorone-programmatic[]
CommandRegistration zeroOrOne() {
return CommandRegistration.builder()
.command("example")
.withOption()
.longNames("arg")
.arity(OptionArity.ZERO_OR_ONE)
.and()
.build();
}
// end::option-registration-zeroorone-programmatic[]
// tag::option-registration-zerooronewithminmax-programmatic[]
CommandRegistration zeroOrOneWithMinMax() {
return CommandRegistration.builder()
.command("example")
.withOption()
.longNames("arg")
.arity(0, 1)
.and()
.build();
}
// end::option-registration-zerooronewithminmax-programmatic[]
}
}

View File

@@ -73,7 +73,7 @@ public class ArityCommands {
@Command(command = "arity-string-array")
public String testArityStringArrayAnnotation(
@Option(longNames = "arg1", defaultValue = "true", arity = OptionArity.ZERO_OR_MORE)
@Option(longNames = "arg1", defaultValue = "true", arityMax = 3)
String[] arg1
) {
return "Hello " + Arrays.asList(arg1);