Skip to content

Commit a04091c

Browse files
committed
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
1 parent 448c507 commit a04091c

File tree

14 files changed

+747
-5
lines changed

14 files changed

+747
-5
lines changed

spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandCatalogAutoConfiguration.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020

2121
import org.springframework.beans.factory.ObjectProvider;
2222
import org.springframework.boot.autoconfigure.AutoConfiguration;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2324
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
25+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2426
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2527
import org.springframework.context.annotation.Bean;
2628
import org.springframework.shell.MethodTargetRegistrar;
@@ -29,6 +31,8 @@
2931
import org.springframework.shell.command.CommandCatalogCustomizer;
3032
import org.springframework.shell.command.CommandRegistration;
3133
import org.springframework.shell.command.CommandRegistration.BuilderSupplier;
34+
import org.springframework.shell.command.CommandRegistration.OptionNameModifier;
35+
import org.springframework.shell.command.support.OptionNameModifierSupport;
3236
import org.springframework.shell.command.CommandResolver;
3337

3438
@AutoConfiguration
@@ -74,6 +78,40 @@ public CommandRegistrationCustomizer helpOptionsCommandRegistrationCustomizer(Sp
7478
};
7579
}
7680

81+
@Bean
82+
@ConditionalOnBean(OptionNameModifier.class)
83+
public CommandRegistrationCustomizer customOptionNameModifierCommandRegistrationCustomizer(OptionNameModifier modifier) {
84+
return builder -> {
85+
builder.defaultOptionNameModifier(modifier);
86+
};
87+
}
88+
89+
@Bean
90+
@ConditionalOnMissingBean(OptionNameModifier.class)
91+
@ConditionalOnProperty(prefix = "spring.shell.option.naming", name = "case-type")
92+
public CommandRegistrationCustomizer defaultOptionNameModifierCommandRegistrationCustomizer(SpringShellProperties properties) {
93+
return builder -> {
94+
switch (properties.getOption().getNaming().getCaseType()) {
95+
case NOOP:
96+
break;
97+
case CAMEL:
98+
builder.defaultOptionNameModifier(OptionNameModifierSupport.CAMELCASE);
99+
break;
100+
case SNAKE:
101+
builder.defaultOptionNameModifier(OptionNameModifierSupport.SNAKECASE);
102+
break;
103+
case KEBAB:
104+
builder.defaultOptionNameModifier(OptionNameModifierSupport.KEBABCASE);
105+
break;
106+
case PASCAL:
107+
builder.defaultOptionNameModifier(OptionNameModifierSupport.PASCALCASE);
108+
break;
109+
default:
110+
break;
111+
}
112+
};
113+
}
114+
77115
@Bean
78116
@ConditionalOnMissingBean
79117
public BuilderSupplier commandRegistrationBuilderSupplier(

spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class SpringShellProperties {
3333
private Theme theme = new Theme();
3434
private Command command = new Command();
3535
private Help help = new Help();
36+
private Option option = new Option();
3637

3738
public void setConfig(Config config) {
3839
this.config = config;
@@ -98,6 +99,14 @@ public Help getHelp() {
9899
return help;
99100
}
100101

102+
public Option getOption() {
103+
return option;
104+
}
105+
106+
public void setOption(Option option) {
107+
this.option = option;
108+
}
109+
101110
public static class Config {
102111

103112
private String env;
@@ -559,4 +568,38 @@ public void setEnabled(boolean enabled) {
559568
this.enabled = enabled;
560569
}
561570
}
571+
572+
public static class Option {
573+
574+
private OptionNaming naming = new OptionNaming();
575+
576+
public OptionNaming getNaming() {
577+
return naming;
578+
}
579+
580+
public void setNaming(OptionNaming naming) {
581+
this.naming = naming;
582+
}
583+
}
584+
585+
public static class OptionNaming {
586+
private OptionNamingCase caseType = OptionNamingCase.NOOP;
587+
588+
public OptionNamingCase getCaseType() {
589+
return caseType;
590+
}
591+
592+
public void setCaseType(OptionNamingCase caseType) {
593+
this.caseType = caseType;
594+
}
595+
}
596+
597+
public static enum OptionNamingCase {
598+
NOOP,
599+
CAMEL,
600+
SNAKE,
601+
KEBAB,
602+
PASCAL
603+
}
604+
562605
}

spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/CommandCatalogAutoConfigurationTests.java

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,9 @@
2727
import org.springframework.shell.command.CommandCatalog;
2828
import org.springframework.shell.command.CommandRegistration;
2929
import org.springframework.shell.command.CommandResolver;
30+
import org.springframework.shell.command.CommandRegistration.Builder;
31+
import org.springframework.shell.command.CommandRegistration.BuilderSupplier;
32+
import org.springframework.shell.command.CommandRegistration.OptionNameModifier;
3033

3134
import static org.assertj.core.api.Assertions.assertThat;
3235

@@ -68,6 +71,82 @@ void registerCommandRegistration() {
6871
});
6972
}
7073

74+
@Test
75+
void builderSupplierIsCreated() {
76+
this.contextRunner
77+
.run(context -> {
78+
BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class);
79+
assertThat(builderSupplier).isNotNull();
80+
});
81+
}
82+
83+
@Test
84+
void defaultOptionNameModifierIsNull() {
85+
this.contextRunner
86+
.run(context -> {
87+
BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class);
88+
Builder builder = builderSupplier.get();
89+
assertThat(builder).extracting("defaultOptionNameModifier").isNull();
90+
});
91+
}
92+
93+
@Test
94+
void defaultOptionNameModifierIsSet() {
95+
this.contextRunner
96+
.withUserConfiguration(CustomOptionNameModifierConfiguration.class)
97+
.run(context -> {
98+
BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class);
99+
Builder builder = builderSupplier.get();
100+
assertThat(builder).extracting("defaultOptionNameModifier").isNotNull();
101+
});
102+
}
103+
104+
@Test
105+
void defaultOptionNameModifierIsSetFromProperties() {
106+
this.contextRunner
107+
.withPropertyValues("spring.shell.option.naming.case-type=kebab")
108+
.run(context -> {
109+
BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class);
110+
Builder builder = builderSupplier.get();
111+
assertThat(builder).extracting("defaultOptionNameModifier").isNotNull();
112+
});
113+
}
114+
115+
@Test
116+
void defaultOptionNameModifierNoopNotSetFromProperties() {
117+
this.contextRunner
118+
.withPropertyValues("spring.shell.option.naming.case-type=noop")
119+
.run(context -> {
120+
BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class);
121+
Builder builder = builderSupplier.get();
122+
assertThat(builder).extracting("defaultOptionNameModifier").isNull();
123+
// there is customizer but it doesn't do anything
124+
assertThat(context).hasBean("defaultOptionNameModifierCommandRegistrationCustomizer");
125+
});
126+
}
127+
128+
@Test
129+
void noCustomizerIfPropertyIsNotSet() {
130+
this.contextRunner
131+
.run(context -> {
132+
BuilderSupplier builderSupplier = context.getBean(BuilderSupplier.class);
133+
Builder builder = builderSupplier.get();
134+
assertThat(builder).extracting("defaultOptionNameModifier").isNull();
135+
// no customizer added without property
136+
assertThat(context).doesNotHaveBean("defaultOptionNameModifierCommandRegistrationCustomizer");
137+
});
138+
}
139+
140+
// defaultOptionNameModifierCommandRegistrationCustomizer
141+
@Configuration
142+
static class CustomOptionNameModifierConfiguration {
143+
144+
@Bean
145+
OptionNameModifier customOptionNameModifier() {
146+
return name -> name;
147+
}
148+
}
149+
71150
@Configuration
72151
static class CustomCommandResolverConfiguration {
73152

spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2121
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
22+
import org.springframework.shell.boot.SpringShellProperties.OptionNamingCase;
2223
import org.springframework.shell.boot.SpringShellProperties.HelpCommand.GroupingMode;
2324

2425
import static org.assertj.core.api.Assertions.assertThat;
@@ -67,6 +68,7 @@ public void defaultNoPropertiesSet() {
6768
assertThat(properties.getHelp().getCommand()).isEqualTo("help");
6869
assertThat(properties.getHelp().getLongNames()).containsExactly("help");
6970
assertThat(properties.getHelp().getShortNames()).containsExactly('h');
71+
assertThat(properties.getOption().getNaming().getCaseType()).isEqualTo(OptionNamingCase.NOOP);
7072
});
7173
}
7274

@@ -107,6 +109,7 @@ public void setProperties() {
107109
.withPropertyValues("spring.shell.help.command=fake")
108110
.withPropertyValues("spring.shell.help.long-names=fake")
109111
.withPropertyValues("spring.shell.help.short-names=f")
112+
.withPropertyValues("spring.shell.option.naming.case-type=camel")
110113
.withUserConfiguration(Config1.class)
111114
.run((context) -> {
112115
SpringShellProperties properties = context.getBean(SpringShellProperties.class);
@@ -144,6 +147,7 @@ public void setProperties() {
144147
assertThat(properties.getHelp().getCommand()).isEqualTo("fake");
145148
assertThat(properties.getHelp().getLongNames()).containsExactly("fake");
146149
assertThat(properties.getHelp().getShortNames()).containsExactly('f');
150+
assertThat(properties.getOption().getNaming().getCaseType()).isEqualTo(OptionNamingCase.CAMEL);
147151
});
148152
}
149153

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

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ public static Builder builder() {
145145
public interface BuilderSupplier extends Supplier<Builder> {
146146
}
147147

148+
/**
149+
* Interface used to modify option long name. Usual use case is i.e. making
150+
* conversion from a {@code camelCase} to {@code snake-case}.
151+
*/
152+
@FunctionalInterface
153+
public interface OptionNameModifier extends Function<String, String> {
154+
}
155+
148156
/**
149157
* Spec defining an option.
150158
*/
@@ -247,6 +255,14 @@ public interface OptionSpec {
247255
*/
248256
OptionSpec completion(CompletionResolver completion);
249257

258+
/**
259+
* Define an option name modifier.
260+
*
261+
* @param modifier the option name modifier function
262+
* @return option spec for chaining
263+
*/
264+
OptionSpec nameModifier(Function<String, String> modifier);
265+
250266
/**
251267
* Return a builder for chaining.
252268
*
@@ -673,6 +689,16 @@ public interface Builder {
673689
*/
674690
Builder hidden(boolean hidden);
675691

692+
/**
693+
* Provides a global option name modifier. Will be used with all options to
694+
* modify long names. Usual use case is to enforce naming convention i.e. to
695+
* have {@code snake-case} for all names.
696+
*
697+
* @param modifier to modifier to change option name
698+
* @return builder for chaining
699+
*/
700+
Builder defaultOptionNameModifier(Function<String, String> modifier);
701+
676702
/**
677703
* Define an option what this command should user for. Can be used multiple
678704
* times.
@@ -738,6 +764,7 @@ static class DefaultOptionSpec implements OptionSpec {
738764
private Integer arityMax;
739765
private String label;
740766
private CompletionResolver completion;
767+
private Function<String, String> optionNameModifier;
741768

742769
DefaultOptionSpec(BaseBuilder builder) {
743770
this.builder = builder;
@@ -842,6 +869,12 @@ public OptionSpec completion(CompletionResolver completion) {
842869
return this;
843870
}
844871

872+
@Override
873+
public OptionSpec nameModifier(Function<String, String> modifier) {
874+
this.optionNameModifier = modifier;
875+
return this;
876+
}
877+
845878
@Override
846879
public Builder and() {
847880
return builder;
@@ -890,6 +923,17 @@ public String getLabel() {
890923
public CompletionResolver getCompletion() {
891924
return completion;
892925
}
926+
927+
@Nullable
928+
public Function<String, String> getOptionNameModifier() {
929+
if (optionNameModifier != null) {
930+
return optionNameModifier;
931+
}
932+
if (builder.defaultOptionNameModifier != null) {
933+
return builder.defaultOptionNameModifier;
934+
}
935+
return null;
936+
}
893937
}
894938

895939
static class DefaultTargetSpec implements TargetSpec {
@@ -1142,9 +1186,16 @@ public Availability getAvailability() {
11421186
@Override
11431187
public List<CommandOption> getOptions() {
11441188
List<CommandOption> options = optionSpecs.stream()
1145-
.map(o -> CommandOption.of(o.getLongNames(), o.getShortNames(), o.getDescription(), o.getType(),
1146-
o.isRequired(), o.getDefaultValue(), o.getPosition(), o.getArityMin(), o.getArityMax(),
1147-
o.getLabel(), o.getCompletion()))
1189+
.map(o -> {
1190+
String[] longNames = o.getLongNames();
1191+
Function<String, String> modifier = o.getOptionNameModifier();
1192+
if (modifier != null) {
1193+
longNames = Arrays.stream(longNames).map(modifier).toArray(String[]::new);
1194+
}
1195+
return CommandOption.of(longNames, o.getShortNames(), o.getDescription(), o.getType(),
1196+
o.isRequired(), o.getDefaultValue(), o.getPosition(), o.getArityMin(), o.getArityMax(),
1197+
o.getLabel(), o.getCompletion());
1198+
})
11481199
.collect(Collectors.toList());
11491200
if (helpOptionsSpec != null) {
11501201
String[] longNames = helpOptionsSpec.longNames != null ? helpOptionsSpec.longNames : null;
@@ -1235,6 +1286,7 @@ static abstract class BaseBuilder implements Builder {
12351286
private DefaultExitCodeSpec exitCodeSpec;
12361287
private DefaultErrorHandlingSpec errorHandlingSpec;
12371288
private DefaultHelpOptionsSpec helpOptionsSpec;
1289+
private Function<String, String> defaultOptionNameModifier;
12381290

12391291
@Override
12401292
public Builder command(String... commands) {
@@ -1283,6 +1335,12 @@ public Builder availability(Supplier<Availability> availability) {
12831335
return this;
12841336
}
12851337

1338+
@Override
1339+
public Builder defaultOptionNameModifier(Function<String,String> modifier) {
1340+
this.defaultOptionNameModifier = modifier;
1341+
return this;
1342+
}
1343+
12861344
@Override
12871345
public OptionSpec withOption() {
12881346
DefaultOptionSpec spec = new DefaultOptionSpec(this);

0 commit comments

Comments
 (0)