diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/Option.java b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/Option.java index 409542aa..b494b5e8 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/Option.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/Option.java @@ -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; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBean.java b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBean.java index f73b4560..f3a89dc1 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBean.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBean.java @@ -251,7 +251,22 @@ class CommandRegistrationFactoryBean implements FactoryBean 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 { diff --git a/spring-shell-core/src/test/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBeanTests.java b/spring-shell-core/src/test/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBeanTests.java index f12aae56..287d689b 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBeanTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBeanTests.java @@ -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 ApplicationContextRunner configCommon(Class type, T bean) { return configCommon(type, bean, "command", new Class[0]); } diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-options-arity.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-options-arity.adoc index a25ed93d..53c2dc18 100644 --- a/spring-shell-docs/src/main/asciidoc/using-shell-options-arity.adoc +++ b/spring-shell-docs/src/main/asciidoc/using-shell-options-arity.adoc @@ -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 <>. +[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. diff --git a/spring-shell-docs/src/test/java/org/springframework/shell/docs/OptionSnippets.java b/spring-shell-docs/src/test/java/org/springframework/shell/docs/OptionSnippets.java index 22ff3edf..3e49dce8 100644 --- a/spring-shell-docs/src/test/java/org/springframework/shell/docs/OptionSnippets.java +++ b/spring-shell-docs/src/test/java/org/springframework/shell/docs/OptionSnippets.java @@ -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[] + + } } diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ArityCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ArityCommands.java index b35edbe5..dd66786a 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ArityCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ArityCommands.java @@ -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);