Skip to content

Commit 2bdf843

Browse files
Nico-DFfmbenhassine
authored andcommitted
Add mandatory flag to string input
- Added to flow - Added to component - Add simple test case - Add samples PR #844 Signed-off-by: Nico-DF <difalco.nicola@gmail.com>
1 parent 7c4b7f3 commit 2bdf843

File tree

9 files changed

+150
-9
lines changed

9 files changed

+150
-9
lines changed

spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/StringInput.java

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,28 @@ public class StringInput extends AbstractTextComponent<String, StringInputContex
4949

5050
private @Nullable Character maskCharacter;
5151

52+
private boolean required;
53+
5254
public StringInput(Terminal terminal) {
53-
this(terminal, null, null, null);
55+
this(terminal, null, null, null, false);
5456
}
5557

5658
public StringInput(Terminal terminal, @Nullable String name, @Nullable String defaultValue) {
57-
this(terminal, name, defaultValue, null);
59+
this(terminal, name, defaultValue, null, false);
5860
}
5961

6062
public StringInput(Terminal terminal, @Nullable String name, @Nullable String defaultValue,
6163
@Nullable Function<StringInputContext, List<AttributedString>> renderer) {
64+
this(terminal, name, defaultValue, renderer, false);
65+
}
66+
67+
public StringInput(Terminal terminal, @Nullable String name, @Nullable String defaultValue,
68+
@Nullable Function<StringInputContext, List<AttributedString>> renderer, boolean required) {
6269
super(terminal, name, null);
6370
setRenderer(renderer != null ? renderer : new DefaultRenderer());
6471
setTemplateLocation("classpath:org/springframework/shell/component/string-input-default.stg");
6572
this.defaultValue = defaultValue;
73+
this.required = required;
6674
}
6775

6876
/**
@@ -73,12 +81,20 @@ public void setMaskCharacter(@Nullable Character maskCharacter) {
7381
this.maskCharacter = maskCharacter;
7482
}
7583

84+
/**
85+
* Sets a required flag to check that the result is not empty
86+
* @param required if input is required
87+
*/
88+
public void setRequired(boolean required) {
89+
this.required = required;
90+
}
91+
7692
@Override
7793
public StringInputContext getThisContext(@Nullable ComponentContext<?> context) {
7894
if (context != null && currentContext == context) {
7995
return currentContext;
8096
}
81-
currentContext = StringInputContext.of(defaultValue, maskCharacter);
97+
currentContext = StringInputContext.of(defaultValue, maskCharacter, required);
8298
currentContext.setName(getName());
8399
if (context != null) {
84100
context.stream().forEach(e -> {
@@ -125,6 +141,10 @@ protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, Strin
125141
else if (context.getDefaultValue() != null) {
126142
context.setResultValue(context.getDefaultValue());
127143
}
144+
else if (required) {
145+
context.setMessage("This field is mandatory", TextComponentContext.MessageLevel.ERROR);
146+
break;
147+
}
128148
return true;
129149
default:
130150
break;
@@ -176,20 +196,41 @@ public interface StringInputContext extends TextComponentContext<String, StringI
176196
*/
177197
@Nullable Character getMaskCharacter();
178198

199+
/**
200+
* Sets flag for mandatory input.
201+
* @param required true if input is required
202+
*/
203+
void setRequired(boolean required);
204+
205+
/**
206+
* Returns flag if input is required.
207+
* @return true if input is required, false otherwise
208+
*/
209+
boolean isRequired();
210+
179211
/**
180212
* Gets an empty {@link StringInputContext}.
181213
* @return empty path input context
182214
*/
183215
public static StringInputContext empty() {
184-
return of(null, null);
216+
return of(null, null, false);
185217
}
186218

187219
/**
188220
* Gets an {@link StringInputContext}.
189221
* @return path input context
190222
*/
191223
public static StringInputContext of(@Nullable String defaultValue, @Nullable Character maskCharacter) {
192-
return new DefaultStringInputContext(defaultValue, maskCharacter);
224+
return of(defaultValue, maskCharacter, false);
225+
}
226+
227+
/**
228+
* Gets an {@link StringInputContext}.
229+
* @return path input context
230+
*/
231+
public static StringInputContext of(@Nullable String defaultValue, @Nullable Character maskCharacter,
232+
boolean required) {
233+
return new DefaultStringInputContext(defaultValue, maskCharacter, required);
193234
}
194235

195236
}
@@ -201,9 +242,13 @@ private static class DefaultStringInputContext extends BaseTextComponentContext<
201242

202243
private @Nullable Character maskCharacter;
203244

204-
public DefaultStringInputContext(@Nullable String defaultValue, @Nullable Character maskCharacter) {
245+
private boolean required;
246+
247+
public DefaultStringInputContext(@Nullable String defaultValue, @Nullable Character maskCharacter,
248+
boolean required) {
205249
this.defaultValue = defaultValue;
206250
this.maskCharacter = maskCharacter;
251+
this.required = required;
207252
}
208253

209254
@Override
@@ -221,6 +266,11 @@ public void setMaskCharacter(Character maskCharacter) {
221266
this.maskCharacter = maskCharacter;
222267
}
223268

269+
@Override
270+
public void setRequired(boolean required) {
271+
this.required = required;
272+
}
273+
224274
@Override
225275
public @Nullable String getMaskedInput() {
226276
return maybeMask(getInput());
@@ -241,6 +291,11 @@ public boolean hasMaskCharacter() {
241291
return maskCharacter;
242292
}
243293

294+
@Override
295+
public boolean isRequired() {
296+
return required;
297+
}
298+
244299
@Override
245300
public Map<String, @Nullable Object> toTemplateModel() {
246301
Map<String, @Nullable Object> attributes = super.toTemplateModel();
@@ -249,6 +304,7 @@ public boolean hasMaskCharacter() {
249304
attributes.put("maskedResultValue", getMaskedResultValue());
250305
attributes.put("maskCharacter", getMaskCharacter());
251306
attributes.put("hasMaskCharacter", hasMaskCharacter());
307+
attributes.put("required", isRequired());
252308
Map<String, Object> model = new HashMap<>();
253309
model.put("model", attributes);
254310
return model;

spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/BaseStringInput.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public abstract class BaseStringInput extends BaseInput<StringInputSpec> impleme
4545

4646
private @Nullable Character maskCharacter;
4747

48+
private boolean required = false;
49+
4850
private @Nullable Function<StringInputContext, List<AttributedString>> renderer;
4951

5052
private List<Consumer<StringInputContext>> preHandlers = new ArrayList<>();
@@ -91,6 +93,12 @@ public StringInputSpec maskCharacter(Character maskCharacter) {
9193
return this;
9294
}
9395

96+
@Override
97+
public StringInputSpec required() {
98+
this.required = true;
99+
return this;
100+
}
101+
94102
@Override
95103
public StringInputSpec renderer(Function<StringInputContext, List<AttributedString>> renderer) {
96104
this.renderer = renderer;
@@ -158,6 +166,10 @@ public StringInputSpec getThis() {
158166
return maskCharacter;
159167
}
160168

169+
public boolean isRequired() {
170+
return required;
171+
}
172+
161173
public @Nullable Function<StringInputContext, List<AttributedString>> getRenderer() {
162174
return renderer;
163175
}

spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/ComponentFlow.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,8 @@ else if (n.isPresent()) {
496496

497497
private Stream<OrderedInputOperation> stringInputsStream() {
498498
return stringInputs.stream().map(input -> {
499-
StringInput selector = new StringInput(terminal, input.getName(), input.getDefaultValue());
499+
StringInput selector = new StringInput(terminal, input.getName(), input.getDefaultValue(), null,
500+
input.isRequired());
500501
Function<ComponentContext<?>, ComponentContext<?>> operation = (context) -> {
501502
if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult()
502503
&& StringUtils.hasText(input.getResultValue())) {
@@ -520,6 +521,7 @@ private Stream<OrderedInputOperation> stringInputsStream() {
520521
if (input.getResultMode() == ResultMode.VERIFY && StringUtils.hasText(input.getResultValue())) {
521522
selector.addPreRunHandler(c -> {
522523
c.setDefaultValue(input.getResultValue());
524+
c.setRequired(input.isRequired());
523525
});
524526
}
525527
selector.addPostRunHandler(c -> {

spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/StringInputSpec.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ public interface StringInputSpec extends BaseInputSpec<StringInputSpec> {
6767
*/
6868
StringInputSpec maskCharacter(Character maskCharacter);
6969

70+
/**
71+
* Sets input to required
72+
* @return a builder
73+
*/
74+
StringInputSpec required();
75+
7076
/**
7177
* Sets a renderer function.
7278
* @param renderer the renderer

spring-shell-core/src/test/java/org/springframework/shell/core/tui/component/StringInputTests.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,39 @@ void testResultUserInput() {
162162
});
163163
}
164164

165+
@Test
166+
void testResultMandatoryInput() {
167+
ComponentContext<?> empty = ComponentContext.empty();
168+
StringInput component1 = new StringInput(getTerminal());
169+
component1.setResourceLoader(new DefaultResourceLoader());
170+
component1.setTemplateExecutor(getTemplateExecutor());
171+
component1.setRequired(true);
172+
173+
service.execute(() -> {
174+
StringInputContext run1Context = component1.run(empty);
175+
result1.set(run1Context);
176+
});
177+
178+
TestBuffer testBuffer = new TestBuffer().cr();
179+
write(testBuffer.getBytes());
180+
181+
await().atMost(Duration.ofSeconds(2)).untilAsserted(() -> {
182+
StringInputContext run1Context = result1.get();
183+
assertThat(consoleOut()).contains("This field is mandatory");
184+
assertThat(run1Context).isNull();
185+
});
186+
187+
testBuffer.append("test").cr();
188+
write(testBuffer.getBytes());
189+
190+
await().atMost(Duration.ofSeconds(2)).untilAsserted(() -> {
191+
StringInputContext run1Context = result1.get();
192+
193+
assertThat(run1Context).isNotNull();
194+
assertThat(run1Context.getResultValue()).isEqualTo("test");
195+
});
196+
}
197+
165198
@Test
166199
void testResultUserInputUnicode() {
167200
ComponentContext<?> empty = ComponentContext.empty();

spring-shell-core/src/test/java/org/springframework/shell/core/tui/component/flow/ComponentFlowTests.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ void testSimpleFlow() {
5454
.withStringInput("field2")
5555
.name("Field2")
5656
.and()
57+
.withStringInput("field3")
58+
.name("Field3")
59+
.required()
60+
.and()
5761
.withNumberInput("number1")
5862
.name("Number1")
5963
.and()
@@ -90,6 +94,9 @@ void testSimpleFlow() {
9094
// field2
9195
testBuffer = new TestBuffer().append("Field2Value").cr();
9296
write(testBuffer.getBytes());
97+
// field3
98+
testBuffer = new TestBuffer().cr().append("Field3Value").cr();
99+
write(testBuffer.getBytes());
93100
// number1
94101
testBuffer = new TestBuffer().append("35").cr();
95102
write(testBuffer.getBytes());
@@ -114,13 +121,15 @@ void testSimpleFlow() {
114121
assertThat(inputWizardResult).isNotNull();
115122
String field1 = inputWizardResult.getContext().get("field1");
116123
String field2 = inputWizardResult.getContext().get("field2");
124+
String field3 = inputWizardResult.getContext().get("field3");
117125
Integer number1 = inputWizardResult.getContext().get("number1");
118126
Double number2 = inputWizardResult.getContext().get("number2");
119127
Integer number3 = inputWizardResult.getContext().get("number3");
120128
Path path1 = inputWizardResult.getContext().get("path1");
121129
String single1 = inputWizardResult.getContext().get("single1");
122130
List<String> multi1 = inputWizardResult.getContext().get("multi1");
123131
assertThat(field1).isEqualTo("defaultField1Value");
132+
assertThat(field3).isEqualTo("Field3Value");
124133
assertThat(field2).isEqualTo("Field2Value");
125134
assertThat(number1).isEqualTo(35);
126135
assertThat(number2).isEqualTo(20.5);
@@ -130,7 +139,6 @@ void testSimpleFlow() {
130139
assertThat(multi1).containsExactlyInAnyOrder("value2");
131140
assertThat(consoleOut()).contains("Field1 defaultField1Value");
132141
});
133-
134142
}
135143

136144
@Test

spring-shell-core/src/test/resources/org/springframework/shell/component/string-input-default.stg

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
// message
2+
message(model) ::= <%
3+
<if(model.message && model.hasMessageLevelError)>
4+
<({<figures.error>}); format="style-level-error"> <model.message; format="style-level-error">
5+
<elseif(model.message && model.hasMessageLevelWarn)>
6+
<({<figures.warning>}); format="style-level-warn"> <model.message; format="style-level-warn">
7+
<elseif(model.message && model.hasMessageLevelInfo)>
8+
<({<figures.info>}); format="style-level-info"> <model.message; format="style-level-info">
9+
<endif>
10+
%>
11+
112
// info section after '? xxx'
213
info(model) ::= <%
314
<if(model.hasMaskCharacter)>
@@ -6,6 +17,8 @@ info(model) ::= <%
617
<else>
718
<if(model.defaultValue)>
819
<("[Default "); format="style-value"><model.defaultValue; format="style-value"><("]"); format="style-value">
20+
<elseif(model.required)>
21+
<("[Required]"); format="style-value">
922
<endif>
1023
<endif>
1124
<else>
@@ -14,6 +27,8 @@ info(model) ::= <%
1427
<else>
1528
<if(model.defaultValue)>
1629
<("[Default "); format="style-value"><model.defaultValue; format="style-value"><("]"); format="style-value">
30+
<elseif(model.required)>
31+
<("[Required]"); format="style-value">
1732
<endif>
1833
<endif>
1934
<endif>
@@ -32,6 +47,7 @@ result(model) ::= <<
3247
// component is running
3348
running(model) ::= <<
3449
<question_name(model)> <info(model)>
50+
<message(model)>
3551
>>
3652

3753
// main

spring-shell-docs/modules/ROOT/pages/components/ui/stringinput.adoc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
ifndef::snippets[:snippets: ../../../../../src/test/java/org/springframework/shell/docs]
55

66
The string input component asks a user for simple text input, optionally masking values
7-
if the content contains something sensitive. The following listing shows an example:
7+
if the content contains something sensitive. The input can also be required (at least 1 char). +
8+
The following listing shows an example:
89

910
[source, java, indent=0]
1011
----
@@ -40,6 +41,9 @@ The context object is `StringInputContext`. The following table lists its contex
4041
|`hasMaskCharacter`
4142
|`true` if a mask character is set. Otherwise, false.
4243

44+
|`required`
45+
|`true` if the input is required. Otherwise, false.
46+
4347
|`model`
4448
|The parent context variables (see xref:/components/ui/render.adoc#textcomponentcontext-template-variables[TextComponentContext Template Variables]).
4549
|===

spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ public void showcase1() {
6161
.withStringInput("field2")
6262
.name("Field2")
6363
.and()
64+
.withStringInput("field3")
65+
.name("Field3")
66+
.mandatory()
67+
.and()
6468
.withConfirmationInput("confirmation1")
6569
.name("Confirmation1")
6670
.and()

0 commit comments

Comments
 (0)