Support modify option names
- New OptionNameModifier which is just a Function<String,String> to modify a name. - Can be defined per option in CommandRegistration. - Can be defined as global default as bean. - Default implementation for common case types is enabled via boot's config props under spring.shell.option.naming.case-type - Support facilities for camel, kebab, snake and pascal conversions. - Fixes #621
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,14 @@ public interface CommandRegistration {
|
||||
public interface BuilderSupplier extends Supplier<Builder> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<String, String> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<String, String> 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<String, String> 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<String, String> optionNameModifier;
|
||||
|
||||
DefaultOptionSpec(BaseBuilder builder) {
|
||||
this.builder = builder;
|
||||
@@ -842,6 +869,12 @@ public interface CommandRegistration {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec nameModifier(Function<String, String> 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<String, String> 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<CommandOption> getOptions() {
|
||||
List<CommandOption> 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<String, String> 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<String, String> defaultOptionNameModifier;
|
||||
|
||||
@Override
|
||||
public Builder command(String... commands) {
|
||||
@@ -1283,6 +1335,12 @@ public interface CommandRegistration {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder defaultOptionNameModifier(Function<String,String> modifier) {
|
||||
this.defaultOptionNameModifier = modifier;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec withOption() {
|
||||
DefaultOptionSpec spec = new DefaultOptionSpec(this);
|
||||
|
||||
@@ -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<String> 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<Integer> 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<Integer> toDelimiterSet(final char[] delimiters) {
|
||||
final Set<Integer> 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;
|
||||
}
|
||||
}
|
||||
@@ -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" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
[[using-shell-commands-programmaticmodel]]
|
||||
==== Programmatic Model
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
|
||||
@@ -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<String, String>`. 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
|
||||
<<using-shell-commands-programmaticmodel>>.
|
||||
|
||||
====
|
||||
[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]
|
||||
|
||||
----
|
||||
====
|
||||
@@ -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[]
|
||||
|
||||
@@ -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[]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user