diff --git a/test-suite/build.gradle b/test-suite/build.gradle index 408d4145..e777baf0 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -3,9 +3,12 @@ plugins { } dependencies { + testAnnotationProcessor("org.projectlombok:lombok") testAnnotationProcessor projects.micronautValidationProcessor testAnnotationProcessor mn.micronaut.inject.java + testCompileOnly("org.projectlombok:lombok") + testImplementation mn.micronaut.inject testImplementation mn.micronaut.core.reactive testImplementation libs.managed.validation diff --git a/test-suite/src/test/java/io/micronaut/docs/validation/path/ValidationTest.java b/test-suite/src/test/java/io/micronaut/docs/validation/path/ValidationTest.java new file mode 100644 index 00000000..f3ee7210 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/validation/path/ValidationTest.java @@ -0,0 +1,53 @@ +package io.micronaut.docs.validation.path; + +import io.micronaut.docs.validation.path.model.Dag; +import io.micronaut.docs.validation.path.model.Flow; +import io.micronaut.docs.validation.path.model.Log; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import jakarta.validation.Validator; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@MicronautTest(startApplication = false) +public class ValidationTest { + @Inject + Validator validator; + + @Test + void testValidationOk() { + Flow f = Flow.builder() + .id("id") + .namespace("namespace") + .tasks(List.of(Log.builder().id("task").type(Log.class.getName()).message("").build())) + .build(); + + var violation = validator.validate(f).stream().findFirst().get(); + assertEquals("tasks[0].message", violation.getPropertyPath().toString()); + assertEquals("must not be blank", violation.getMessage()); + } + + @Test + void testValidationKo() { + Flow f = Flow.builder() + .id("id") + .namespace("namespace") + .tasks(List.of( + Dag.builder() + .id("dag") + .type(Dag.class.getName()) + .tasks(List.of( + Dag.DagTask.builder().task(Log.builder().id("cycle").type(Log.class.getName()).message("").build()).dependsOn(List.of("cycle")).build() + )) + .build()) + ) + .build(); + + var violation = validator.validate(f).stream().findFirst().get(); + assertEquals("tasks[0].tasks", violation.getPropertyPath().toString()); + assertEquals("Cyclic dependency detected: cycle", violation.getMessage()); + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/validation/path/model/AbstractTrigger.java b/test-suite/src/test/java/io/micronaut/docs/validation/path/model/AbstractTrigger.java new file mode 100644 index 00000000..486fd445 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/validation/path/model/AbstractTrigger.java @@ -0,0 +1,41 @@ +package io.micronaut.docs.validation.path.model; + +import io.micronaut.core.annotation.Introspected; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.List; + +@SuperBuilder +@Getter +@NoArgsConstructor +@Introspected +abstract public class AbstractTrigger { + @NotNull + @NotBlank + @Pattern(regexp="[a-zA-Z0-9_-]+") + protected String id; + + @NotNull + @NotBlank + @Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*") + protected String type; + + private String description; + + @Valid + private List conditions; + + @NotNull + @Builder.Default + private boolean disabled = false; + + @Valid + private WorkerGroup workerGroup; +} diff --git a/test-suite/src/test/java/io/micronaut/docs/validation/path/model/Condition.java b/test-suite/src/test/java/io/micronaut/docs/validation/path/model/Condition.java new file mode 100644 index 00000000..65a5298f --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/validation/path/model/Condition.java @@ -0,0 +1,20 @@ +package io.micronaut.docs.validation.path.model; + +import io.micronaut.core.annotation.Introspected; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Introspected +public abstract class Condition { + @NotNull + @Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*") + protected String type; +} diff --git a/test-suite/src/test/java/io/micronaut/docs/validation/path/model/Dag.java b/test-suite/src/test/java/io/micronaut/docs/validation/path/model/Dag.java new file mode 100644 index 00000000..ffb907bd --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/validation/path/model/Dag.java @@ -0,0 +1,105 @@ +package io.micronaut.docs.validation.path.model; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.docs.validation.path.validations.DagTaskValidation; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +@SuperBuilder +@ToString +@EqualsAndHashCode +@Getter +@NoArgsConstructor +@DagTaskValidation +public class Dag extends Task{ + @NotNull + @Builder.Default + private final Integer concurrent = 0; + + @NotEmpty + @Valid + private List tasks; + + @Valid + protected List errors; + + public List dagCheckNotExistTask(List taskDepends) { + List dependenciesIds = taskDepends + .stream() + .map(DagTask::getDependsOn) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .toList(); + + List tasksIds = taskDepends + .stream() + .map(taskDepend -> taskDepend.getTask().getId()) + .toList(); + + return dependenciesIds.stream() + .filter(dependencyId -> !tasksIds.contains(dependencyId)) + .collect(Collectors.toList()); + } + + public ArrayList dagCheckCyclicDependencies(List taskDepends) { + ArrayList cyclicDependency = new ArrayList<>(); + taskDepends.forEach(taskDepend -> { + if (taskDepend.getDependsOn() != null) { + List nestedDependencies = this.nestedDependencies(taskDepend, taskDepends, new ArrayList<>()); + if (nestedDependencies.contains(taskDepend.getTask().getId())) { + cyclicDependency.add(taskDepend.getTask().getId()); + } + } + }); + + return cyclicDependency; + } + + private ArrayList nestedDependencies(DagTask taskDepend, List tasks, List visited) { + final ArrayList localVisited = new ArrayList<>(visited); + if (taskDepend.getDependsOn() != null) { + taskDepend.getDependsOn() + .stream() + .filter(depend -> !localVisited.contains(depend)) + .forEach(depend -> { + localVisited.add(depend); + Optional task = tasks + .stream() + .filter(t -> t.getTask().getId().equals(depend)) + .findFirst(); + + if (task.isPresent()) { + localVisited.addAll(this.nestedDependencies(task.get(), tasks, localVisited)); + } + }); + } + return localVisited; + } + + @SuperBuilder + @ToString + @EqualsAndHashCode + @Getter + @NoArgsConstructor + @Introspected + public static class DagTask { + @NotNull + private Task task; + + private List dependsOn; + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/validation/path/model/DeletedInterface.java b/test-suite/src/test/java/io/micronaut/docs/validation/path/model/DeletedInterface.java new file mode 100644 index 00000000..2c57afd5 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/validation/path/model/DeletedInterface.java @@ -0,0 +1,5 @@ +package io.micronaut.docs.validation.path.model; + +public interface DeletedInterface { + boolean isDeleted(); +} diff --git a/test-suite/src/test/java/io/micronaut/docs/validation/path/model/Flow.java b/test-suite/src/test/java/io/micronaut/docs/validation/path/model/Flow.java new file mode 100644 index 00000000..4d624f80 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/validation/path/model/Flow.java @@ -0,0 +1,201 @@ +package io.micronaut.docs.validation.path.model; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.docs.validation.path.validations.FlowValidation; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.With; +import lombok.experimental.SuperBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@SuperBuilder(toBuilder = true) +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Introspected +@ToString +@EqualsAndHashCode +@FlowValidation +public class Flow implements DeletedInterface { + + @NotNull + @NotBlank + @Pattern(regexp = "[a-zA-Z0-9._-]+") + String id; + + @NotNull + @Pattern(regexp = "[a-z0-9._-]+") + String namespace; + + @With + @Min(value = 1) + Integer revision; + + String description; + + List