Skip to content

Commit

Permalink
feat: adding mandatory flag to string input
Browse files Browse the repository at this point in the history
- Added to flow
- Added to component
- Add simple test case
- Add samples
  • Loading branch information
Nicola Di Falco committed Aug 4, 2023
1 parent 0ab3785 commit 93cd27f
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,28 @@ public class StringInput extends AbstractTextComponent<String, StringInputContex
private final String defaultValue;
private StringInputContext currentContext;
private Character maskCharacter;
private boolean mandatory;

public StringInput(Terminal terminal) {
this(terminal, null, null, null);
this(terminal, null, null, null, false);
}

public StringInput(Terminal terminal, String name, String defaultValue) {
this(terminal, name, defaultValue, null);
this(terminal, name, defaultValue, null, false);
}

public StringInput(Terminal terminal, String name, String defaultValue,
Function<StringInputContext, List<AttributedString>> renderer) {
this(terminal, name, defaultValue, renderer, false);
}

public StringInput(Terminal terminal, String name, String defaultValue,
Function<StringInputContext, List<AttributedString>> renderer, boolean mandatory) {
super(terminal, name, null);
setRenderer(renderer != null ? renderer : new DefaultRenderer());
setTemplateLocation("classpath:org/springframework/shell/component/string-input-default.stg");
this.defaultValue = defaultValue;
this.mandatory = mandatory;
}

/**
Expand All @@ -70,12 +77,21 @@ public void setMaskCharacter(Character maskCharacter) {
this.maskCharacter = maskCharacter;
}

/**
* Sets a mandatory flag to check that the result is not empty
*
* @param mandatory if input is required
*/
public void setMandatory(boolean mandatory) {
this.mandatory = mandatory;
}

@Override
public StringInputContext getThisContext(ComponentContext<?> context) {
if (context != null && currentContext == context) {
return currentContext;
}
currentContext = StringInputContext.of(defaultValue, maskCharacter);
currentContext = StringInputContext.of(defaultValue, maskCharacter, mandatory);
currentContext.setName(getName());
context.stream().forEach(e -> {
currentContext.put(e.getKey(), e.getValue());
Expand Down Expand Up @@ -116,6 +132,9 @@ protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, Strin
}
else if (context.getDefaultValue() != null) {
context.setResultValue(context.getDefaultValue());
} else if (mandatory) {
context.setMessage("This field is mandatory", TextComponentContext.MessageLevel.ERROR);
break;
}
return true;
default:
Expand Down Expand Up @@ -175,13 +194,27 @@ public interface StringInputContext extends TextComponentContext<String, StringI
*/
Character getMaskCharacter();

/**
* Sets flag for mandatory input.
*
* @param mandatory true if input is mandatory
*/
void setMandatory(boolean mandatory);

/**
* Returns flag if input is required.
*
* @return true if input is required, false otherwise
*/
boolean isMandatory();

/**
* Gets an empty {@link StringInputContext}.
*
* @return empty path input context
*/
public static StringInputContext empty() {
return of(null, null);
return of(null, null, false);
}

/**
Expand All @@ -190,7 +223,16 @@ public static StringInputContext empty() {
* @return path input context
*/
public static StringInputContext of(String defaultValue, Character maskCharacter) {
return new DefaultStringInputContext(defaultValue, maskCharacter);
return of(defaultValue, maskCharacter, false);
}

/**
* Gets an {@link StringInputContext}.
*
* @return path input context
*/
public static StringInputContext of(String defaultValue, Character maskCharacter, boolean mandatory) {
return new DefaultStringInputContext(defaultValue, maskCharacter, mandatory);
}
}

Expand All @@ -199,10 +241,12 @@ private static class DefaultStringInputContext extends BaseTextComponentContext<

private String defaultValue;
private Character maskCharacter;
private boolean mandatory;

public DefaultStringInputContext(String defaultValue, Character maskCharacter) {
public DefaultStringInputContext(String defaultValue, Character maskCharacter, boolean mandatory) {
this.defaultValue = defaultValue;
this.maskCharacter = maskCharacter;
this.mandatory = mandatory;
}

@Override
Expand All @@ -220,6 +264,11 @@ public void setMaskCharacter(Character maskCharacter) {
this.maskCharacter = maskCharacter;
}

@Override
public void setMandatory(boolean mandatory) {
this.mandatory = mandatory;
}

@Override
public String getMaskedInput() {
return maybeMask(getInput());
Expand All @@ -240,6 +289,11 @@ public Character getMaskCharacter() {
return maskCharacter;
}

@Override
public boolean isMandatory() {
return mandatory;
}

@Override
public Map<String, Object> toTemplateModel() {
Map<String, Object> attributes = super.toTemplateModel();
Expand All @@ -248,6 +302,7 @@ public Map<String, Object> toTemplateModel() {
attributes.put("maskedResultValue", getMaskedResultValue());
attributes.put("maskCharacter", getMaskCharacter());
attributes.put("hasMaskCharacter", hasMaskCharacter());
attributes.put("mandatory", isMandatory());
Map<String, Object> model = new HashMap<>();
model.put("model", attributes);
return model;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public abstract class BaseStringInput extends BaseInput<StringInputSpec> impleme
private ResultMode resultMode;
private String defaultValue;
private Character maskCharacter;
private boolean mandatory = false;
private Function<StringInputContext, List<AttributedString>> renderer;
private List<Consumer<StringInputContext>> preHandlers = new ArrayList<>();
private List<Consumer<StringInputContext>> postHandlers = new ArrayList<>();
Expand Down Expand Up @@ -79,6 +80,12 @@ public StringInputSpec maskCharacter(Character maskCharacter) {
return this;
}

@Override
public StringInputSpec mandatory() {
this.mandatory = true;
return this;
}

@Override
public StringInputSpec renderer(Function<StringInputContext, List<AttributedString>> renderer) {
this.renderer = renderer;
Expand Down Expand Up @@ -146,6 +153,10 @@ public Character getMaskCharacter() {
return maskCharacter;
}

public boolean isMandatory() {
return mandatory;
}

public Function<StringInputContext, List<AttributedString>> getRenderer() {
return renderer;
}
Expand All @@ -169,4 +180,4 @@ public boolean isStoreResult() {
public Function<StringInputContext, String> getNext() {
return next;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ else if (n.isPresent()) {

private Stream<OrderedInputOperation> stringInputsStream() {
return stringInputs.stream().map(input -> {
StringInput selector = new StringInput(terminal, input.getName(), input.getDefaultValue());
StringInput selector = new StringInput(terminal, input.getName(), input.getDefaultValue(), null, input.isMandatory());
Function<ComponentContext<?>, ComponentContext<?>> operation = (context) -> {
if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult()
&& StringUtils.hasText(input.getResultValue())) {
Expand All @@ -465,6 +465,7 @@ private Stream<OrderedInputOperation> stringInputsStream() {
if (input.getResultMode() == ResultMode.VERIFY && StringUtils.hasText(input.getResultValue())) {
selector.addPreRunHandler(c -> {
c.setDefaultValue(input.getResultValue());
c.setMandatory(input.isMandatory());
});
}
selector.addPostRunHandler(c -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ public interface StringInputSpec extends BaseInputSpec<StringInputSpec> {
*/
StringInputSpec maskCharacter(Character maskCharacter);

/**
* Sets input to mandatory
*
* @return a builder
*/
StringInputSpec mandatory();

/**
* Sets a renderer function.
*
Expand Down Expand Up @@ -128,4 +135,4 @@ public interface StringInputSpec extends BaseInputSpec<StringInputSpec> {
* @return the parent builder
*/
Builder and();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
// message
message(model) ::= <%
<if(model.message && model.hasMessageLevelError)>
<({<figures.error>}); format="style-level-error"> <model.message; format="style-level-error">
<elseif(model.message && model.hasMessageLevelWarn)>
<({<figures.warning>}); format="style-level-warn"> <model.message; format="style-level-warn">
<elseif(model.message && model.hasMessageLevelInfo)>
<({<figures.info>}); format="style-level-info"> <model.message; format="style-level-info">
<endif>
%>

// info section after '? xxx'
info(model) ::= <%
<if(model.hasMaskCharacter)>
<if(model.maskedInput)>
<model.maskedInput>
<else>
<if(model.mandatory)>
<("[Mandatory]"); format="style-value">
<endif>
<if(model.defaultValue)>
<("[Default "); format="style-value"><model.defaultValue; format="style-value"><("]"); format="style-value">
<endif>
Expand All @@ -12,6 +26,9 @@ info(model) ::= <%
<if(model.input)>
<model.input>
<else>
<if(model.mandatory)>
<("[Mandatory]"); format="style-value">
<endif>
<if(model.defaultValue)>
<("[Default "); format="style-value"><model.defaultValue; format="style-value"><("]"); format="style-value">
<endif>
Expand All @@ -32,6 +49,7 @@ result(model) ::= <<
// component is running
running(model) ::= <<
<question_name(model)> <info(model)>
<message(model)>
>>

// main
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,39 @@ public void testResultUserInput() throws InterruptedException {
assertThat(run1Context.getResultValue()).isEqualTo("test");
}

@Test
public void testResultMandatoryInput() throws InterruptedException {
ComponentContext<?> empty = ComponentContext.empty();
StringInput component1 = new StringInput(getTerminal());
component1.setResourceLoader(new DefaultResourceLoader());
component1.setTemplateExecutor(getTemplateExecutor());
component1.setMandatory(true);

service.execute(() -> {
StringInputContext run1Context = component1.run(empty);
result1.set(run1Context);
latch1.countDown();
});

TestBuffer testBuffer = new TestBuffer().cr();
write(testBuffer.getBytes());

latch1.await(2, TimeUnit.SECONDS);

StringInputContext run1Context = result1.get();
assertThat(consoleOut()).contains("This field is mandatory");
assertThat(run1Context).isNull();

testBuffer.append("test").cr();
write(testBuffer.getBytes());

latch1.await(2, TimeUnit.SECONDS);
run1Context = result1.get();

assertThat(run1Context).isNotNull();
assertThat(run1Context.getResultValue()).isEqualTo("test");
}

@Test
public void testPassingViaContext() throws InterruptedException {
ComponentContext<?> empty = ComponentContext.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public void testSimpleFlow() throws InterruptedException {
.withStringInput("field2")
.name("Field2")
.and()
.withStringInput("field3")
.name("Field3")
.mandatory()
.and()
.withPathInput("path1")
.name("Path1")
.and()
Expand Down Expand Up @@ -80,6 +84,9 @@ public void testSimpleFlow() throws InterruptedException {
// field2
testBuffer = new TestBuffer().append("Field2Value").cr();
write(testBuffer.getBytes());
// field3
testBuffer = new TestBuffer().cr().append("Field3Value").cr();
write(testBuffer.getBytes());
// path1
testBuffer = new TestBuffer().append("fakedir").cr();
write(testBuffer.getBytes());
Expand All @@ -95,10 +102,12 @@ public void testSimpleFlow() throws InterruptedException {
assertThat(inputWizardResult).isNotNull();
String field1 = inputWizardResult.getContext().get("field1");
String field2 = inputWizardResult.getContext().get("field2");
String field3 = inputWizardResult.getContext().get("field3");
Path path1 = inputWizardResult.getContext().get("path1");
String single1 = inputWizardResult.getContext().get("single1");
List<String> multi1 = inputWizardResult.getContext().get("multi1");
assertThat(field1).isEqualTo("defaultField1Value");
assertThat(field3).isEqualTo("Field3Value");
assertThat(field2).isEqualTo("Field2Value");
assertThat(path1.toString()).contains("fakedir");
assertThat(single1).isEqualTo("value1");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]

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

====
[source, java, indent=0]
Expand Down Expand Up @@ -38,6 +39,9 @@ The context object is `StringInputContext`. The following table lists its contex
|`hasMaskCharacter`
|`true` if a mask character is set. Otherwise, false.

|`mandatory`
|`true` if the input is required. Otherwise, false.

|`model`
|The parent context variables (see <<textcomponentcontext-template-variables>>).
|===
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ public String stringInput(boolean mask) {
return "Got value " + context.getResultValue();
}

@ShellMethod(key = "component string required", value = "String input", group = "Components")
public String stringRequired() {
StringInput component = new StringInput(getTerminal(), "Enter value", null, null, true);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}

@ShellMethod(key = "component path", value = "Path input", group = "Components")
public String pathInput() {
PathInput component = new PathInput(getTerminal(), "Enter value");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public void showcase1() {
.withStringInput("field2")
.name("Field2")
.and()
.withStringInput("field3")
.name("Field3")
.mandatory()
.and()
.withConfirmationInput("confirmation1")
.name("Confirmation1")
.and()
Expand Down

0 comments on commit 93cd27f

Please sign in to comment.