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 918063b
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 9 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 @@ -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());
// field2
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 918063b

Please sign in to comment.