Skip to content

Commit

Permalink
feat(core): input validator step 1
Browse files Browse the repository at this point in the history
  • Loading branch information
yuri1969 authored and loicmathieu committed Apr 25, 2023
1 parent be29ef5 commit c62fd37
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 6 deletions.
32 changes: 30 additions & 2 deletions core/src/main/java/io/kestra/core/models/flows/Input.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package io.kestra.core.models.flows;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.kestra.core.validations.InputValidation;
import io.kestra.core.validations.Regex;
import io.micronaut.core.annotation.Introspected;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -17,6 +21,7 @@
@NoArgsConstructor
@Introspected
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@InputValidation
public class Input {
@NotNull
@NotBlank
Expand All @@ -35,9 +40,28 @@ public class Input {

String defaults;

@Schema(
title = "Regular expression validating the value."
)
@Regex
String validator;

@JsonIgnore
public boolean canBeValidated() {
if (type == null) {
return false;
}
return type.canBeValidated();
}

@Introspected
public enum Type {
STRING,
STRING() {
@Override
public boolean canBeValidated() {
return true;
}
},
INT,
FLOAT,
BOOLEAN,
Expand All @@ -47,6 +71,10 @@ public enum Type {
DURATION,
FILE,
JSON,
URI,
URI;

public boolean canBeValidated() {
return false;
}
}
}
16 changes: 16 additions & 0 deletions core/src/main/java/io/kestra/core/runners/RunnerUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;

@Singleton
Expand Down Expand Up @@ -120,6 +121,7 @@ public Map<String, Object> typedInputs(Flow flow, Execution execution, Map<Strin

switch (input.getType()) {
case STRING:
validateStringInput(input, current);
return Optional.of(new AbstractMap.SimpleEntry<String, Object>(
input.getName(),
current
Expand Down Expand Up @@ -234,6 +236,20 @@ public Map<String, Object> typedInputs(Flow flow, Execution execution, Map<Strin
return handleNestedInputs(results);
}

private void validateStringInput(Input input, String current) {
final String validator = input.getValidator();
if (validator == null) {
return;
}
try {
if (!Pattern.matches(validator, current)) {
throw new MissingRequiredInput("Invalid format for '" + input.getName() + "' defined by validator '" + validator + "'");
}
} catch (PatternSyntaxException e) {
throw new MissingRequiredInput("Invalid validator syntax '" + validator + "' for '" + input.getName() + "'");
}
}

@SuppressWarnings("unchecked")
private Map<String, Object> handleNestedInputs(Map<String, Object> inputs) {
Map<String, Object> result = new TreeMap<>();
Expand Down
11 changes: 11 additions & 0 deletions core/src/main/java/io/kestra/core/validations/InputValidation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.kestra.core.validations;

import javax.validation.Constraint;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { })
public @interface InputValidation {
String message() default "invalid Input";
}
11 changes: 11 additions & 0 deletions core/src/main/java/io/kestra/core/validations/Regex.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.kestra.core.validations;

import javax.validation.Constraint;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { })
public @interface Regex {
String message() default "invalid pattern ({validatedValue})";
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.cronutils.model.Cron;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.Input;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.tasks.flows.Switch;
import io.micronaut.context.annotation.Factory;
Expand All @@ -12,6 +13,7 @@
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;

Expand Down Expand Up @@ -134,7 +136,7 @@ ConstraintValidator<FlowValidation, Flow> flowValidation() {
.filter(entry -> Collections.frequency(ids, entry) > 1).collect(Collectors.toList());

if (duplicates.size() > 0) {
violations.add("Duplicate task id with name [" + String.join(", ", duplicates) + "]");
violations.add("Duplicate task id with name [" + String.join(", ", duplicates) + "]");
}

if (violations.size() > 0) {
Expand All @@ -145,5 +147,41 @@ ConstraintValidator<FlowValidation, Flow> flowValidation() {
}
};
}

@Singleton
ConstraintValidator<InputValidation, Input> inputValidation() {
return (value, annotationMetadata, context) -> {
if (value == null) {
return true;
}

if (value.getValidator() != null && !value.canBeValidated()) {
context.messageTemplate(
"Invalid Input: Validator defined at [" + value.getName() + "] is not allowed for type [" + value.getType() + "]"
);
return false;
}

return true;
};
}

@Singleton
ConstraintValidator<Regex, String> patternValidator() {
return (value, annotationMetadata, context) -> {
if (value == null) {
return true;
}

try {
java.util.regex.Pattern.compile(value);
} catch(PatternSyntaxException e) {
context.messageTemplate("invalid pattern [" + value + "]");
return false;
}

return true;
};
}
}

56 changes: 55 additions & 1 deletion core/src/test/java/io/kestra/core/runners/InputsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.io.CharStreams;
import io.kestra.core.exceptions.MissingRequiredInput;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.serializers.YamlFlowParser;
import io.kestra.core.utils.TestsUtils;
import org.junit.jupiter.api.Test;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.State;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.storages.StorageInterface;

import jakarta.inject.Inject;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
Expand Down Expand Up @@ -46,6 +52,7 @@ public class InputsTest extends AbstractMemoryRunnerTest {
.put("nested.string", "a string")
.put("nested.more.int", "123")
.put("nested.bool", "true")
.put("validatedString", "A123")
.build();

@Inject
Expand All @@ -54,9 +61,16 @@ public class InputsTest extends AbstractMemoryRunnerTest {
@Inject
private StorageInterface storageInterface;

@Inject
private YamlFlowParser yamlFlowParser;

private Map<String, Object> typedInputs(Map<String, String> map) {
return typedInputs(map, flowRepository.findById("io.kestra.tests", "inputs").get());
}

private Map<String, Object> typedInputs(Map<String, String> map, Flow flow) {
return runnerUtils.typedInputs(
flowRepository.findById("io.kestra.tests", "inputs").get(),
flow,
Execution.builder()
.id("test")
.namespace("test")
Expand Down Expand Up @@ -170,6 +184,37 @@ void inputUri() {
assertThat(typeds.get("uri"), is("https://www.google.com"));
}

@Test
void inputValidatedString() {
Map<String, Object> typeds = typedInputs(inputs);
assertThat(typeds.get("validatedString"), is("A123"));
}

@Test
void inputValidatedStringBadValue() {
HashMap<String, String> map = new HashMap<>(inputs);
map.put("validatedString", "foo");

MissingRequiredInput e = assertThrows(MissingRequiredInput.class, () -> {
Map<String, Object> typeds = typedInputs(map);
});

assertThat(e.getMessage(), containsString("Invalid format for "));
}

@Test
void inputValidatedStringBadSyntax() {
final Flow flow = parse("flows/invalids/inputs-bad-validator-syntax.yaml");
final HashMap<String, String> map = new HashMap<>(inputs);
map.put("badValidatorSyntax", "foo");

final MissingRequiredInput e = assertThrows(MissingRequiredInput.class, () -> {
typedInputs(map, flow);
});

assertThat(e.getMessage(), containsString("Invalid validator syntax"));
}

@Test
void inputFailed() {
HashMap<String, String> map = new HashMap<>(inputs);
Expand All @@ -190,4 +235,13 @@ void inputNested() {
assertThat(((Map<String, Object>)typeds.get("nested")).get("bool"), is(true));
assertThat(((Map<String, Object>)((Map<String, Object>)typeds.get("nested")).get("more")).get("int"), is(123));
}

private Flow parse(String path) {
URL resource = TestsUtils.class.getClassLoader().getResource(path);
assert resource != null;

File file = new File(resource.getFile());

return yamlFlowParser.parse(file, Flow.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,11 @@ void inputsFailed() {
void inputs() {
Flow flow = this.parse("flows/valids/inputs.yaml");

assertThat(flow.getInputs().size(), is(17));
assertThat(flow.getInputs().size(), is(18));
assertThat(flow.getInputs().stream().filter(Input::getRequired).count(), is(6L));
assertThat(flow.getInputs().stream().filter(r -> !r.getRequired()).count(), is(11L));
assertThat(flow.getInputs().stream().filter(r -> !r.getRequired()).count(), is(12L));
assertThat(flow.getInputs().stream().filter(r -> r.getDefaults() != null).count(), is(1L));
assertThat(flow.getInputs().stream().filter(r -> r.getValidator() != null).count(), is(1L));
}

@Test
Expand Down
37 changes: 37 additions & 0 deletions core/src/test/java/io/kestra/core/validations/InputTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.kestra.core.validations;

import io.kestra.core.models.flows.Input;
import io.kestra.core.models.validations.ModelValidator;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;

@MicronautTest
class InputTest {
@Inject
private ModelValidator modelValidator;

@Test
void inputValidation() {
final Input validInput = Input.builder()
.name("test")
.type(Input.Type.STRING)
.validator("[A-Z]+")
.build();

assertThat(modelValidator.isValid(validInput).isEmpty(), is(true));

final Input invalidInput = Input.builder()
.name("test")
.type(Input.Type.INT)
.validator("[A-Z]+")
.build();

assertThat(modelValidator.isValid(invalidInput).isPresent(), is(true));
assertThat(modelValidator.isValid(invalidInput).get().getMessage(), containsString("Invalid Input: Validator"));
}
}
39 changes: 39 additions & 0 deletions core/src/test/java/io/kestra/core/validations/RegexTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.kestra.core.validations;

import io.kestra.core.models.validations.ModelValidator;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;

@MicronautTest
class RegexTest {
@Inject
private ModelValidator modelValidator;

@AllArgsConstructor
@Introspected
@Getter
public static class RegexCls {
@Regex
String pattern;
}

@Test
void inputValidation() {
final RegexCls validRegex = new RegexCls("[A-Z]+");

assertThat(modelValidator.isValid(validRegex).isEmpty(), is(true));

final RegexCls invalidRegex = new RegexCls("\\");

assertThat(modelValidator.isValid(invalidRegex).isPresent(), is(true));
assertThat(modelValidator.isValid(invalidRegex).get().getMessage(), containsString("invalid pattern"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
id: empty
namespace: io.kestra.tests
inputs:
- name: badValidatorSyntax
type: STRING
validator: \
tasks:
- id: date
type: io.kestra.core.tasks.debugs.Return
format: "{{taskrun.startDate}}"
Loading

0 comments on commit c62fd37

Please sign in to comment.