diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandCatalogAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandCatalogAutoConfiguration.java index bc1d4edc..fbef8199 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandCatalogAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandCatalogAutoConfiguration.java @@ -20,7 +20,9 @@ import java.util.stream.Collectors; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.shell.MethodTargetRegistrar; @@ -29,6 +31,8 @@ import org.springframework.shell.command.CommandCatalog; import org.springframework.shell.command.CommandCatalogCustomizer; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.command.CommandRegistration.BuilderSupplier; +import org.springframework.shell.command.CommandRegistration.OptionNameModifier; +import org.springframework.shell.command.support.OptionNameModifierSupport; import org.springframework.shell.command.CommandResolver; @AutoConfiguration @@ -74,6 +78,40 @@ public class CommandCatalogAutoConfiguration { }; } + @Bean + @ConditionalOnBean(OptionNameModifier.class) + public CommandRegistrationCustomizer customOptionNameModifierCommandRegistrationCustomizer(OptionNameModifier modifier) { + return builder -> { + builder.defaultOptionNameModifier(modifier); + }; + } + + @Bean + @ConditionalOnMissingBean(OptionNameModifier.class) + @ConditionalOnProperty(prefix = "spring.shell.option.naming", name = "case-type") + public CommandRegistrationCustomizer defaultOptionNameModifierCommandRegistrationCustomizer(SpringShellProperties properties) { + return builder -> { + switch (properties.getOption().getNaming().getCaseType()) { + case NOOP: + break; + case CAMEL: + builder.defaultOptionNameModifier(OptionNameModifierSupport.CAMELCASE); + break; + case SNAKE: + builder.defaultOptionNameModifier(OptionNameModifierSupport.SNAKECASE); + break; + case KEBAB: + builder.defaultOptionNameModifier(OptionNameModifierSupport.KEBABCASE); + break; + case PASCAL: + builder.defaultOptionNameModifier(OptionNameModifierSupport.PASCALCASE); + break; + default: + break; + } + }; + } + @Bean @ConditionalOnMissingBean public BuilderSupplier commandRegistrationBuilderSupplier( diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java index 72be18d5..1894ec1c 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java @@ -33,6 +33,7 @@ public class SpringShellProperties { private Theme theme = new Theme(); private Command command = new Command(); private Help help = new Help(); + private Option option = new Option(); public void setConfig(Config config) { this.config = config; @@ -98,6 +99,14 @@ public class SpringShellProperties { return help; } + public Option getOption() { + return option; + } + + public void setOption(Option option) { + this.option = option; + } + public static class Config { private String env; @@ -559,4 +568,38 @@ public class SpringShellProperties { this.enabled = enabled; } } + + public static class Option { + + private OptionNaming naming = new OptionNaming(); + + public OptionNaming getNaming() { + return naming; + } + + public void setNaming(OptionNaming naming) { + this.naming = naming; + } + } + + public static class OptionNaming { + private OptionNamingCase caseType = OptionNamingCase.NOOP; + + public OptionNamingCase getCaseType() { + return caseType; + } + + public void setCaseType(OptionNamingCase caseType) { + this.caseType = caseType; + } + } + + public static enum OptionNamingCase { + NOOP, + CAMEL, + SNAKE, + KEBAB, + PASCAL + } + } diff --git a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/CommandCatalogAutoConfigurationTests.java b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/CommandCatalogAutoConfigurationTests.java index d1b39ad4..dd7bdb24 100644 --- a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/CommandCatalogAutoConfigurationTests.java +++ b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/CommandCatalogAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2023 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. @@ -27,6 +27,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.shell.command.CommandCatalog; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.command.CommandResolver; +import org.springframework.shell.command.CommandRegistration.Builder; +import org.springframework.shell.command.CommandRegistration.BuilderSupplier; +import org.springframework.shell.command.CommandRegistration.OptionNameModifier; import static org.assertj.core.api.Assertions.assertThat; @@ -68,6 +71,82 @@ public class CommandCatalogAutoConfigurationTests { }); } + @Test + void builderSupplierIsCreated() { + this.contextRunner + .run(context -> { + BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class); + assertThat(builderSupplier).isNotNull(); + }); + } + + @Test + void defaultOptionNameModifierIsNull() { + this.contextRunner + .run(context -> { + BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class); + Builder builder = builderSupplier.get(); + assertThat(builder).extracting("defaultOptionNameModifier").isNull(); + }); + } + + @Test + void defaultOptionNameModifierIsSet() { + this.contextRunner + .withUserConfiguration(CustomOptionNameModifierConfiguration.class) + .run(context -> { + BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class); + Builder builder = builderSupplier.get(); + assertThat(builder).extracting("defaultOptionNameModifier").isNotNull(); + }); + } + + @Test + void defaultOptionNameModifierIsSetFromProperties() { + this.contextRunner + .withPropertyValues("spring.shell.option.naming.case-type=kebab") + .run(context -> { + BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class); + Builder builder = builderSupplier.get(); + assertThat(builder).extracting("defaultOptionNameModifier").isNotNull(); + }); + } + + @Test + void defaultOptionNameModifierNoopNotSetFromProperties() { + this.contextRunner + .withPropertyValues("spring.shell.option.naming.case-type=noop") + .run(context -> { + BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class); + Builder builder = builderSupplier.get(); + assertThat(builder).extracting("defaultOptionNameModifier").isNull(); + // there is customizer but it doesn't do anything + assertThat(context).hasBean("defaultOptionNameModifierCommandRegistrationCustomizer"); + }); + } + + @Test + void noCustomizerIfPropertyIsNotSet() { + this.contextRunner + .run(context -> { + BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class); + Builder builder = builderSupplier.get(); + assertThat(builder).extracting("defaultOptionNameModifier").isNull(); + // no customizer added without property + assertThat(context).doesNotHaveBean("defaultOptionNameModifierCommandRegistrationCustomizer"); + }); + } + + // defaultOptionNameModifierCommandRegistrationCustomizer + @Configuration + static class CustomOptionNameModifierConfiguration { + + @Bean + OptionNameModifier customOptionNameModifier() { + return name -> name; + } + } + @Configuration static class CustomCommandResolverConfiguration { diff --git a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java index 67b299dd..47465773 100644 --- a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java +++ b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.shell.boot.SpringShellProperties.OptionNamingCase; import org.springframework.shell.boot.SpringShellProperties.HelpCommand.GroupingMode; import static org.assertj.core.api.Assertions.assertThat; @@ -67,6 +68,7 @@ public class SpringShellPropertiesTests { assertThat(properties.getHelp().getCommand()).isEqualTo("help"); assertThat(properties.getHelp().getLongNames()).containsExactly("help"); assertThat(properties.getHelp().getShortNames()).containsExactly('h'); + assertThat(properties.getOption().getNaming().getCaseType()).isEqualTo(OptionNamingCase.NOOP); }); } @@ -107,6 +109,7 @@ public class SpringShellPropertiesTests { .withPropertyValues("spring.shell.help.command=fake") .withPropertyValues("spring.shell.help.long-names=fake") .withPropertyValues("spring.shell.help.short-names=f") + .withPropertyValues("spring.shell.option.naming.case-type=camel") .withUserConfiguration(Config1.class) .run((context) -> { SpringShellProperties properties = context.getBean(SpringShellProperties.class); @@ -144,6 +147,7 @@ public class SpringShellPropertiesTests { assertThat(properties.getHelp().getCommand()).isEqualTo("fake"); assertThat(properties.getHelp().getLongNames()).containsExactly("fake"); assertThat(properties.getHelp().getShortNames()).containsExactly('f'); + assertThat(properties.getOption().getNaming().getCaseType()).isEqualTo(OptionNamingCase.CAMEL); }); } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java index 74a01d6d..4d8b19b9 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java @@ -145,6 +145,14 @@ public interface CommandRegistration { public interface BuilderSupplier extends Supplier { } + /** + * Interface used to modify option long name. Usual use case is i.e. making + * conversion from a {@code camelCase} to {@code snake-case}. + */ + @FunctionalInterface + public interface OptionNameModifier extends Function { + } + /** * Spec defining an option. */ @@ -247,6 +255,14 @@ public interface CommandRegistration { */ OptionSpec completion(CompletionResolver completion); + /** + * Define an option name modifier. + * + * @param modifier the option name modifier function + * @return option spec for chaining + */ + OptionSpec nameModifier(Function modifier); + /** * Return a builder for chaining. * @@ -673,6 +689,16 @@ public interface CommandRegistration { */ Builder hidden(boolean hidden); + /** + * Provides a global option name modifier. Will be used with all options to + * modify long names. Usual use case is to enforce naming convention i.e. to + * have {@code snake-case} for all names. + * + * @param modifier to modifier to change option name + * @return builder for chaining + */ + Builder defaultOptionNameModifier(Function modifier); + /** * Define an option what this command should user for. Can be used multiple * times. @@ -738,6 +764,7 @@ public interface CommandRegistration { private Integer arityMax; private String label; private CompletionResolver completion; + private Function optionNameModifier; DefaultOptionSpec(BaseBuilder builder) { this.builder = builder; @@ -842,6 +869,12 @@ public interface CommandRegistration { return this; } + @Override + public OptionSpec nameModifier(Function modifier) { + this.optionNameModifier = modifier; + return this; + } + @Override public Builder and() { return builder; @@ -890,6 +923,17 @@ public interface CommandRegistration { public CompletionResolver getCompletion() { return completion; } + + @Nullable + public Function getOptionNameModifier() { + if (optionNameModifier != null) { + return optionNameModifier; + } + if (builder.defaultOptionNameModifier != null) { + return builder.defaultOptionNameModifier; + } + return null; + } } static class DefaultTargetSpec implements TargetSpec { @@ -1142,9 +1186,16 @@ public interface CommandRegistration { @Override public List getOptions() { List options = optionSpecs.stream() - .map(o -> CommandOption.of(o.getLongNames(), o.getShortNames(), o.getDescription(), o.getType(), - o.isRequired(), o.getDefaultValue(), o.getPosition(), o.getArityMin(), o.getArityMax(), - o.getLabel(), o.getCompletion())) + .map(o -> { + String[] longNames = o.getLongNames(); + Function modifier = o.getOptionNameModifier(); + if (modifier != null) { + longNames = Arrays.stream(longNames).map(modifier).toArray(String[]::new); + } + return CommandOption.of(longNames, o.getShortNames(), o.getDescription(), o.getType(), + o.isRequired(), o.getDefaultValue(), o.getPosition(), o.getArityMin(), o.getArityMax(), + o.getLabel(), o.getCompletion()); + }) .collect(Collectors.toList()); if (helpOptionsSpec != null) { String[] longNames = helpOptionsSpec.longNames != null ? helpOptionsSpec.longNames : null; @@ -1235,6 +1286,7 @@ public interface CommandRegistration { private DefaultExitCodeSpec exitCodeSpec; private DefaultErrorHandlingSpec errorHandlingSpec; private DefaultHelpOptionsSpec helpOptionsSpec; + private Function defaultOptionNameModifier; @Override public Builder command(String... commands) { @@ -1283,6 +1335,12 @@ public interface CommandRegistration { return this; } + @Override + public Builder defaultOptionNameModifier(Function modifier) { + this.defaultOptionNameModifier = modifier; + return this; + } + @Override public OptionSpec withOption() { DefaultOptionSpec spec = new DefaultOptionSpec(this); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/support/OptionNameModifierSupport.java b/spring-shell-core/src/main/java/org/springframework/shell/command/support/OptionNameModifierSupport.java new file mode 100644 index 00000000..2ea61843 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/support/OptionNameModifierSupport.java @@ -0,0 +1,163 @@ +/* + * Copyright 2023 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.command.support; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.springframework.shell.command.CommandRegistration.OptionNameModifier; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Support facilities for {@link OptionNameModifier} providing common naming + * types. + * + * @author Janne Valkealahti + */ +public abstract class OptionNameModifierSupport { + + public static final OptionNameModifier NOOP = name -> name; + public static final OptionNameModifier CAMELCASE = name -> toCamelCase(name); + public static final OptionNameModifier SNAKECASE = name -> toSnakeCase(name); + public static final OptionNameModifier KEBABCASE = name -> toKebabCase(name); + public static final OptionNameModifier PASCALCASE = name -> toPascalCase(name); + + private static final Pattern PATTERN = Pattern + .compile("[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+"); + + /** + * Convert given name to {@code camelCase}. + * + * @param name the name to modify + * @return a modified name as camel case + */ + public static String toCamelCase(String name) { + return toCapitalizeCase(name, false, ' ', '-', '_'); + } + + /** + * Convert given name to {@code snake_case}. + * + * @param name the name to modify + * @return a modified name as snake case + */ + public static String toSnakeCase(String name) { + return matchJoin(name, "_"); + } + + /** + * Convert given name to {@code kebab-case}. + * + * @param name the name to modify + * @return a modified name as kebab case + */ + public static String toKebabCase(String name) { + return matchJoin(name, "-"); + } + + /** + * Convert given name to {@code PascalCase}. + * + * @param name the name to modify + * @return a modified name as pascal case + */ + public static String toPascalCase(String name) { + return toCapitalizeCase(name, true, ' ', '-', '_'); + } + + private static String matchJoin(String name, String delimiter) { + Matcher matcher = PATTERN.matcher(name); + List matches = new ArrayList<>(); + while (matcher.find()) { + String group = matcher.group(); + matches.add(group); + } + return matches.stream().map(x -> x.toLowerCase()).collect(Collectors.joining(delimiter)); + } + + private static String toCapitalizeCase(String name, final boolean capitalizeFirstLetter, final char... delimiters) { + if (!StringUtils.hasText(name)) { + return name; + } + String nameL = name.toLowerCase(); + + final int strLen = nameL.length(); + final int[] newCodePoints = new int[strLen]; + final Set delimiterSet = toDelimiterSet(delimiters); + + int outOffset = 0; + boolean capitalizeNext = capitalizeFirstLetter; + + boolean delimiterFound = false; + + for (int index = 0; index < strLen;) { + final int codePoint = nameL.codePointAt(index); + + if (delimiterSet.contains(codePoint)) { + capitalizeNext = outOffset != 0; + index += Character.charCount(codePoint); + delimiterFound = true; + } else if (capitalizeNext || outOffset == 0 && capitalizeFirstLetter) { + final int titleCaseCodePoint = Character.toTitleCase(codePoint); + newCodePoints[outOffset++] = titleCaseCodePoint; + index += Character.charCount(titleCaseCodePoint); + capitalizeNext = false; + } else { + newCodePoints[outOffset++] = codePoint; + index += Character.charCount(codePoint); + } + } + + if (!delimiterFound) { + if (capitalizeFirstLetter) { + return name.substring(0, 1).toUpperCase() + name.substring(1, name.length()); + } + else { + return name.substring(0, 1).toLowerCase() + name.substring(1, name.length()); + } + } + + return new String(newCodePoints, 0, outOffset); + } + + /** + * Converts an array of delimiters to a hash set of code points. Code point of + * space(32) is added as the default value. The generated hash set provides O(1) + * lookup time. + * + * @param delimiters set of characters to determine capitalization, null means + * whitespace + * @return Integers of code points + */ + private static Set toDelimiterSet(final char[] delimiters) { + final Set delimiterHashSet = new HashSet<>(); + delimiterHashSet.add(Character.codePointAt(new char[]{' '}, 0)); + if (ObjectUtils.isEmpty(delimiters)) { + return delimiterHashSet; + } + + for (int index = 0; index < delimiters.length; index++) { + delimiterHashSet.add(Character.codePointAt(delimiters, index)); + } + return delimiterHashSet; + } +} diff --git a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java index 04957e60..1f70beb0 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2023 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. @@ -543,4 +543,44 @@ public class CommandRegistrationTests extends AbstractCommandTests { assertThat(registration.getOptions().get(0).getLongNames()).containsExactly("help"); assertThat(registration.getOptions().get(0).getShortNames()).containsExactly('h'); } + + @Test + void testOptionNameModifierInOption() { + CommandRegistration registration = CommandRegistration.builder() + .command("command1") + .withOption() + .longNames("arg1") + .nameModifier(name -> "x" + name) + .and() + .withTarget() + .consumer(ctx -> {}) + .and() + .build(); + + assertThat(registration.getOptions()).hasSize(1); + assertThat(registration.getOptions().get(0)).satisfies(option -> { + assertThat(option).isNotNull(); + assertThat(option.getLongNames()).isEqualTo(new String[] { "xarg1" }); + }); + } + + @Test + void testOptionNameModifierFromDefault() { + CommandRegistration registration = CommandRegistration.builder() + .defaultOptionNameModifier(name -> "x" + name) + .command("command1") + .withOption() + .longNames("arg1") + .and() + .withTarget() + .consumer(ctx -> {}) + .and() + .build(); + + assertThat(registration.getOptions()).hasSize(1); + assertThat(registration.getOptions().get(0)).satisfies(option -> { + assertThat(option).isNotNull(); + assertThat(option.getLongNames()).isEqualTo(new String[] { "xarg1" }); + }); + } } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/command/support/OptionNameModifierSupportTests.java b/spring-shell-core/src/test/java/org/springframework/shell/command/support/OptionNameModifierSupportTests.java new file mode 100644 index 00000000..ca8360b4 --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/command/support/OptionNameModifierSupportTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2023 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.command.support; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class OptionNameModifierSupportTests { + + @ParameterizedTest + @CsvSource({ + "camel case,camelCase", + "camel_case,camelCase", + "camel-case,camelCase", + "camelCase,camelCase", + "CamelCase,camelCase" + }) + void testCamel(String name, String expected) { + assertThat(camel(name)).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ + "pascal-case,PascalCase", + "pascal_case,PascalCase", + "pascalCase,PascalCase", + "PascalCase,PascalCase" + }) + void testPascal(String name, String expected) { + assertThat(pascal(name)).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ + "kebabCase,kebab-case", + "kebab_case,kebab-case", + "kebab_Case,kebab-case", + "Kebab_case,kebab-case" + }) + void testKebab(String name, String expected) { + assertThat(kebab(name)).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ + "snakeCase,snake_case", + "snake_case,snake_case", + "snake_Case,snake_case", + "Snake_case,snake_case" + }) + void testSnake(String name, String expected) { + assertThat(snake(name)).isEqualTo(expected); + } + + private String camel(String name) { + return OptionNameModifierSupport.toCamelCase(name); + } + + private String kebab(String name) { + return OptionNameModifierSupport.toKebabCase(name); + } + + private String snake(String name) { + return OptionNameModifierSupport.toSnakeCase(name); + } + + private String pascal(String name) { + return OptionNameModifierSupport.toPascalCase(name); + } +} diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-commands-programmaticmodel.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-commands-programmaticmodel.adoc index 97f90a7b..f0830291 100644 --- a/spring-shell-docs/src/main/asciidoc/using-shell-commands-programmaticmodel.adoc +++ b/spring-shell-docs/src/main/asciidoc/using-shell-commands-programmaticmodel.adoc @@ -1,3 +1,4 @@ +[[using-shell-commands-programmaticmodel]] ==== Programmatic Model ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs] diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-options-naming.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-options-naming.adoc new file mode 100644 index 00000000..ba9c9c50 --- /dev/null +++ b/spring-shell-docs/src/main/asciidoc/using-shell-options-naming.adoc @@ -0,0 +1,113 @@ +[[using-shell-options-naming]] +=== Naming +ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs] + +If there is a need to modify option long names that can be done +using `OptionNameModifier` interface which is a simple +`Function`. In this interface original option +name goes in and modified name comes out. + +Modifier can be defined per `OptionSpec` in `CommandRegistration`, +defaulting globally as bean or via configuration properties. +Modifier defined manually in `OptionSpec` takes takes precedence +over one defined globally. There is no global modifier defined +on default. + +You can define one with an option in `CommandRegistration`. + +==== +[source, java, indent=0] +---- +include::{snippets}/OptionSnippets.java[tag=option-registration-naming-case-req] +---- +==== + +Add one _singleton bean_ as type `OptionNameModifier` and that becomes +a global default. + +==== +[source, java, indent=0] +---- +include::{snippets}/OptionSnippets.java[tag=option-registration-naming-case-bean] +---- +==== + +It's also possible to just add configuration property with +`spring.shell.option.naming.case-type` which auto-configures +one based on a type defined. + +`noop` is to do nothing, `camel`, `snake`, `kebab`, `pascal` +activates build-in modifiers for `camelCase`, `snake_case`, +`kebab-case` or `PascalCase` respectively. + +NOTE: If creating `CommandRegistration` beans directly, global +default via configuration properies only work if using +pre-configured `Builder` instance. See more +<>. + +==== +[source, yaml] +---- +spring: + shell: + option: + naming: + case-type: noop + # case-type: camel + # case-type: snake + # case-type: kebab + # case-type: pascal +---- +==== + +For example options defined in an annotated method like this. + +==== +[source, java, indent=0] +---- +include::{snippets}/OptionSnippets.java[tag=option-registration-naming-case-sample1] +---- +==== + +On default `help` for that command shows names coming +directly from `@ShellOption`. + +==== +[source, bash] +---- +OPTIONS + --from_snake String + [Mandatory] + + --fromCamel String + [Mandatory] + + --from-kebab String + [Mandatory] + + --FromPascal String + [Mandatory] +---- +==== + +Define `spring.shell.option.naming.case-type=kebab` and default +modifier is added and option names then look like. + +==== +[source, bash] +---- +OPTIONS + --from-snake String + [Mandatory] + + --from-camel String + [Mandatory] + + --from-kebab String + [Mandatory] + + --from-pascal String + [Mandatory] + +---- +==== diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-options.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-options.adoc index 34c146ab..05bf1305 100644 --- a/spring-shell-docs/src/main/asciidoc/using-shell-options.adoc +++ b/spring-shell-docs/src/main/asciidoc/using-shell-options.adoc @@ -22,3 +22,5 @@ include::using-shell-options-validation.adoc[] include::using-shell-options-label.adoc[] include::using-shell-options-types.adoc[] + +include::using-shell-options-naming.adoc[] 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 5b12925e..60d952c8 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 @@ -17,8 +17,11 @@ package org.springframework.shell.docs; import java.util.Arrays; +import org.springframework.context.annotation.Bean; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.command.CommandRegistration.OptionArity; +import org.springframework.shell.command.CommandRegistration.OptionNameModifier; +import org.springframework.shell.standard.ShellMethod; import org.springframework.shell.standard.ShellOption; public class OptionSnippets { @@ -239,6 +242,35 @@ public class OptionSnippets { .and() .build(); // end::option-registration-label[] + + // tag::option-registration-naming-case-req[] + CommandRegistration.builder() + .withOption() + .longNames("arg1") + .nameModifier(name -> "x" + name) + .and() + .build(); + // end::option-registration-naming-case-req[] + } + + class Dump8 { + // tag::option-registration-naming-case-bean[] + @Bean + OptionNameModifier sampleOptionNameModifier() { + return name -> "x" + name; + } + // end::option-registration-naming-case-bean[] + + // tag::option-registration-naming-case-sample1[] + @ShellMethod(key = "option-naming-sample") + public void optionNamingSample( + @ShellOption("from_snake") String snake, + @ShellOption("fromCamel") String camel, + @ShellOption("from-kebab") String kebab, + @ShellOption("FromPascal") String pascal + ) {} + // end::option-registration-naming-case-sample1[] + } } diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/OptionNamingCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/OptionNamingCommands.java new file mode 100644 index 00000000..3e2909e7 --- /dev/null +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/OptionNamingCommands.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023 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.samples.e2e; + +import org.springframework.context.annotation.Bean; +import org.springframework.shell.command.CommandRegistration; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; +import org.springframework.shell.standard.ShellOption; +import org.springframework.stereotype.Component; + +public class OptionNamingCommands { + + @ShellComponent + public static class OptionNamingCommandsLegacyAnnotation extends BaseE2ECommands { + + @ShellMethod(key = LEGACY_ANNO + "option-naming-1", group = GROUP) + public void testOptionNaming1Annotation( + @ShellOption("from_snake") String snake, + @ShellOption("fromCamel") String camel, + @ShellOption("from-kebab") String kebab, + @ShellOption("FromPascal") String pascal + ) { + } + + } + + @Component + public static class OptionNamingCommandsRegistration extends BaseE2ECommands { + + @Bean + public CommandRegistration testOptionNaming1Registration(CommandRegistration.BuilderSupplier builder) { + return builder.get() + .command(REG, "option-naming-1") + .group(GROUP) + .withOption() + .longNames("from_snake") + .required() + .and() + .withOption() + .longNames("fromCamel") + .required() + .and() + .withOption() + .longNames("from-kebab") + .required() + .and() + .withOption() + .longNames("FromPascal") + .required() + .and() + .withOption() + .longNames("arg1") + .nameModifier(name -> "x" + name) + .required() + .and() + .withTarget() + .consumer(ctx -> {}) + .and() + .build(); + } + } +} diff --git a/spring-shell-samples/src/main/resources/application.yml b/spring-shell-samples/src/main/resources/application.yml index ec7ec106..25542e94 100644 --- a/spring-shell-samples/src/main/resources/application.yml +++ b/spring-shell-samples/src/main/resources/application.yml @@ -2,6 +2,14 @@ spring: main: banner-mode: off shell: + ## pick global default option naming + # option: + # naming: + # case-type: noop + # case-type: camel + # case-type: snake + # case-type: kebab + # case-type: pascal config: env: SPRING_SHELL_SAMPLES_USER_HOME location: "{userconfig}/spring-shell-samples"