From 7eb67d9c84a8417045bff90eac4e5660fc403214 Mon Sep 17 00:00:00 2001 From: 20012796 Date: Thu, 1 Apr 2021 18:45:10 +0200 Subject: [PATCH] feat: base flows --- .gitignore | 103 ++++ .idea/.gitignore | 8 + .idea/.name | 1 + .idea/compiler.xml | 13 + .idea/jarRepositories.xml | 45 ++ .idea/misc.xml | 14 + .idea/uiDesigner.xml | 124 +++++ CONTRIBUTING.md | 0 LICENSE.txt | 21 + README.md | 0 pom.xml | 166 ++++++ .../builder/ConditionalFlowBuilder.java | 92 ++++ .../builder/ParallelFlowBuilder.java | 128 +++++ .../builder/RecoverableFlowBuilder.java | 91 ++++ .../builder/RetryableFlowBuilder.java | 104 ++++ .../builder/SequentialFlowBuilder.java | 78 +++ .../reactorflow/builder/StepFlowBuilder.java | 68 +++ .../builder/SwitchFlowBuilder.java | 150 ++++++ .../exception/FlowBuilderException.java | 20 + .../reactorflow/exception/FlowException.java | 96 ++++ .../exception/FlowExceptionType.java | 7 + .../exception/FlowFunctionalException.java | 20 + .../exception/FlowTechnicalException.java | 20 + .../exception/RecoverableFlowException.java | 8 + .../reactorflow/flow/ConditionalFlow.java | 134 +++++ .../java/fr/jtools/reactorflow/flow/Flow.java | 487 ++++++++++++++++++ .../reactorflow/flow/FlowStatusPolicy.java | 68 +++ .../fr/jtools/reactorflow/flow/NoOpFlow.java | 90 ++++ .../jtools/reactorflow/flow/ParallelFlow.java | 219 ++++++++ .../reactorflow/flow/RecoverableFlow.java | 74 +++ .../reactorflow/flow/RetryableFlow.java | 104 ++++ .../reactorflow/flow/SequentialFlow.java | 166 ++++++ .../java/fr/jtools/reactorflow/flow/Step.java | 16 + .../reactorflow/flow/StepExecution.java | 44 ++ .../fr/jtools/reactorflow/flow/StepFlow.java | 110 ++++ .../jtools/reactorflow/flow/SwitchFlow.java | 153 ++++++ .../jtools/reactorflow/state/FlowContext.java | 79 +++ .../fr/jtools/reactorflow/state/Metadata.java | 54 ++ .../fr/jtools/reactorflow/state/State.java | 196 +++++++ .../reactorflow/utils/ConsoleStyle.java | 87 ++++ .../jtools/reactorflow/utils/LoggerUtils.java | 15 + .../jtools/reactorflow/utils/PrettyPrint.java | 5 + .../jtools/reactorflow/utils/TriFunction.java | 24 + .../reactorflow/ConditionalFlowTest.java | 353 +++++++++++++ .../fr/jtools/reactorflow/NoOpFlowTest.java | 44 ++ .../jtools/reactorflow/ParallelFlowTest.java | 4 + .../reactorflow/RecoverableFlowTest.java | 282 ++++++++++ .../jtools/reactorflow/RetryableFlowTest.java | 4 + .../reactorflow/SequentialFlowTest.java | 4 + .../fr/jtools/reactorflow/StepFlowTest.java | 374 ++++++++++++++ .../fr/jtools/reactorflow/SwitchFlowTest.java | 465 +++++++++++++++++ .../reactorflow/testutils/CustomContext.java | 7 + .../testutils/ErrorMonoStepFlow.java | 35 ++ .../testutils/ErrorRawStepFlow.java | 34 ++ .../reactorflow/testutils/ErrorStepFlow.java | 36 ++ .../testutils/SuccessStepFlow.java | 35 ++ .../reactorflow/testutils/TestUtils.java | 21 + .../testutils/WarningStepFlow.java | 37 ++ 58 files changed, 5237 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/fr/jtools/reactorflow/builder/ConditionalFlowBuilder.java create mode 100644 src/main/java/fr/jtools/reactorflow/builder/ParallelFlowBuilder.java create mode 100644 src/main/java/fr/jtools/reactorflow/builder/RecoverableFlowBuilder.java create mode 100644 src/main/java/fr/jtools/reactorflow/builder/RetryableFlowBuilder.java create mode 100644 src/main/java/fr/jtools/reactorflow/builder/SequentialFlowBuilder.java create mode 100644 src/main/java/fr/jtools/reactorflow/builder/StepFlowBuilder.java create mode 100644 src/main/java/fr/jtools/reactorflow/builder/SwitchFlowBuilder.java create mode 100644 src/main/java/fr/jtools/reactorflow/exception/FlowBuilderException.java create mode 100644 src/main/java/fr/jtools/reactorflow/exception/FlowException.java create mode 100644 src/main/java/fr/jtools/reactorflow/exception/FlowExceptionType.java create mode 100644 src/main/java/fr/jtools/reactorflow/exception/FlowFunctionalException.java create mode 100644 src/main/java/fr/jtools/reactorflow/exception/FlowTechnicalException.java create mode 100644 src/main/java/fr/jtools/reactorflow/exception/RecoverableFlowException.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/ConditionalFlow.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/Flow.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/FlowStatusPolicy.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/NoOpFlow.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/ParallelFlow.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/RecoverableFlow.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/RetryableFlow.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/SequentialFlow.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/Step.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/StepExecution.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/StepFlow.java create mode 100644 src/main/java/fr/jtools/reactorflow/flow/SwitchFlow.java create mode 100644 src/main/java/fr/jtools/reactorflow/state/FlowContext.java create mode 100644 src/main/java/fr/jtools/reactorflow/state/Metadata.java create mode 100644 src/main/java/fr/jtools/reactorflow/state/State.java create mode 100644 src/main/java/fr/jtools/reactorflow/utils/ConsoleStyle.java create mode 100644 src/main/java/fr/jtools/reactorflow/utils/LoggerUtils.java create mode 100644 src/main/java/fr/jtools/reactorflow/utils/PrettyPrint.java create mode 100644 src/main/java/fr/jtools/reactorflow/utils/TriFunction.java create mode 100644 src/test/java/fr/jtools/reactorflow/ConditionalFlowTest.java create mode 100644 src/test/java/fr/jtools/reactorflow/NoOpFlowTest.java create mode 100644 src/test/java/fr/jtools/reactorflow/ParallelFlowTest.java create mode 100644 src/test/java/fr/jtools/reactorflow/RecoverableFlowTest.java create mode 100644 src/test/java/fr/jtools/reactorflow/RetryableFlowTest.java create mode 100644 src/test/java/fr/jtools/reactorflow/SequentialFlowTest.java create mode 100644 src/test/java/fr/jtools/reactorflow/StepFlowTest.java create mode 100644 src/test/java/fr/jtools/reactorflow/SwitchFlowTest.java create mode 100644 src/test/java/fr/jtools/reactorflow/testutils/CustomContext.java create mode 100644 src/test/java/fr/jtools/reactorflow/testutils/ErrorMonoStepFlow.java create mode 100644 src/test/java/fr/jtools/reactorflow/testutils/ErrorRawStepFlow.java create mode 100644 src/test/java/fr/jtools/reactorflow/testutils/ErrorStepFlow.java create mode 100644 src/test/java/fr/jtools/reactorflow/testutils/SuccessStepFlow.java create mode 100644 src/test/java/fr/jtools/reactorflow/testutils/TestUtils.java create mode 100644 src/test/java/fr/jtools/reactorflow/testutils/WarningStepFlow.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f354389 --- /dev/null +++ b/.gitignore @@ -0,0 +1,103 @@ +# Created by https://www.gitignore.io/api/maven + +.idea +### Maven ### +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +*.iml + +# End of https://www.gitignore.io/api/maven + + +# Created by https://www.gitignore.io/api/eclipse + +### Eclipse ### + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +### Eclipse Patch ### +# Eclipse Core +.project + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Annotation Processing +.apt_generated + +.sts4-cache/ + + +# End of https://www.gitignore.io/api/eclipse +*/target/* +*/.vscode/settings.json +.vscode/settings.json +/target/ + +.DS_Store + +reactor-flow.iml +reactor-flow.ipr +reactor-flow.iws +.env diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..83e8831 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/../../../../../../../:\Users\20012796\Desktop\Projects\java-tools\.idea/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..8ded4ef --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +reactor-flow \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..d2eaff6 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..63c9ae9 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d24ea8e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8e6ebae --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Julien GALET + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..71bbba5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,166 @@ + + + 4.0.0 + + fr.jtools + reactor-flow + 0.1.0-SNAPSHOT + + A library to execute flows with reactor. Inspired by https://github.com/j-easy/easy-flows project. + + + https://github.com/juliengalet/reactor-flow + + git@github.com:juliengalet/reactor-flow.git + scm:git:https://github.com/juliengalet/reactor-flow + scm:git:https://github.com/juliengalet/reactor-flow + HEAD + + + + + MIT License + https://github.com/juliengalet/reactor-flow/blob/master/LICENSE.txt + + + + + Github Actions + https://github.com/juliengalet/reactor-flow/actions + + + + + galet.julien@gmail.com + juliengalet + Julien GALET + + + + + 11 + 11 + 11 + UTF-8 + 3.4.4 + 5.7.0 + 3.19.0 + 2.22.1 + 3.8.1 + 2.5.3 + 3.2.1 + 3.2.0 + 3.7.1 + 3.1.1 + + + + + io.projectreactor + reactor-core + ${reactor.version} + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + 5.7.0 + test + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + io.projectreactor + reactor-test + ${reactor.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + src/main/java + + + org.apache.maven.plugins + maven-release-plugin + ${maven-release-plugin.version} + + v@{project.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + true + true + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + ${maven-project-info-reports-plugin.version} + + + + \ No newline at end of file diff --git a/src/main/java/fr/jtools/reactorflow/builder/ConditionalFlowBuilder.java b/src/main/java/fr/jtools/reactorflow/builder/ConditionalFlowBuilder.java new file mode 100644 index 0000000..82c2101 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/builder/ConditionalFlowBuilder.java @@ -0,0 +1,92 @@ +package fr.jtools.reactorflow.builder; + +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.flow.ConditionalFlow; +import fr.jtools.reactorflow.flow.Flow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.State; + +import java.util.Objects; +import java.util.function.Predicate; + +public final class ConditionalFlowBuilder { + public static ConditionalFlowBuilder.Named defaultBuilder() { + return new ConditionalFlowBuilder.BuildSteps<>(); + } + + public static ConditionalFlowBuilder.Named builderForContextOfType(Class contextClass) { + return new ConditionalFlowBuilder.BuildSteps<>(); + } + + private static final class BuildSteps implements ConditionalFlowBuilder.Named, + ConditionalFlowBuilder.Condition, + ConditionalFlowBuilder.CaseTrue, + ConditionalFlowBuilder.CaseFalse, + ConditionalFlowBuilder.Build { + private String name; + private Flow flowCaseTrue; + private Flow flowCaseFalse; + + private Predicate> condition; + + private BuildSteps() { + } + + public final ConditionalFlowBuilder.Condition named(String name) { + if (Objects.isNull(name)) { + throw new FlowBuilderException(ConditionalFlowBuilder.class, "name is mandatory"); + } + this.name = name; + return this; + } + + public final ConditionalFlowBuilder.CaseTrue condition(Predicate> condition) { + if (Objects.isNull(condition)) { + throw new FlowBuilderException(ConditionalFlowBuilder.class, "condition is mandatory"); + } + this.condition = condition; + return this; + } + + public final ConditionalFlowBuilder.CaseFalse caseTrue(Flow flow) { + if (Objects.isNull(flow)) { + throw new FlowBuilderException(ConditionalFlowBuilder.class, "caseTrue is mandatory"); + } + this.flowCaseTrue = flow; + return this; + } + + public final ConditionalFlowBuilder.Build caseFalse(Flow flow) { + if (Objects.isNull(flow)) { + throw new FlowBuilderException(ConditionalFlowBuilder.class, "caseFalse is mandatory"); + } + this.flowCaseFalse = flow; + return this; + } + + public final ConditionalFlow build() { + return ConditionalFlow.create(this.name, this.condition, this.flowCaseTrue, this.flowCaseFalse); + } + } + + public interface CaseTrue { + ConditionalFlowBuilder.CaseFalse caseTrue(Flow flow); + } + + public interface CaseFalse { + ConditionalFlowBuilder.Build caseFalse(Flow flow); + } + + public interface Build { + ConditionalFlow build(); + } + + public interface Named { + ConditionalFlowBuilder.Condition named(String name); + } + + public interface Condition { + ConditionalFlowBuilder.CaseTrue condition(Predicate> condition); + } +} + diff --git a/src/main/java/fr/jtools/reactorflow/builder/ParallelFlowBuilder.java b/src/main/java/fr/jtools/reactorflow/builder/ParallelFlowBuilder.java new file mode 100644 index 0000000..cb2c176 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/builder/ParallelFlowBuilder.java @@ -0,0 +1,128 @@ +package fr.jtools.reactorflow.builder; + +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.flow.Flow; +import fr.jtools.reactorflow.flow.ParallelFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.State; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.BinaryOperator; +import java.util.function.Function; + +public final class ParallelFlowBuilder { + public static BinaryOperator> defaultMergeStrategy() { + return (state1, state2) -> state1; + } + + public static ParallelFlowBuilder.Named defaultBuilder() { + return new ParallelFlowBuilder.BuildSteps<>(); + } + + public static ParallelFlowBuilder.Named builderForContextOfType(Class contextClass) { + return new ParallelFlowBuilder.BuildSteps<>(); + } + + public static ParallelFlowBuilder.Named builderForMetadataType(Class metadataClass) { + return new ParallelFlowBuilder.BuildSteps<>(); + } + + public static ParallelFlowBuilder.Named builderForTypes(Class contextClass, Class metadataClass) { + return new ParallelFlowBuilder.BuildSteps<>(); + } + + private static final class BuildSteps implements ParallelFlowBuilder.Named, + ParallelFlowBuilder.Parallelize, + ParallelFlowBuilder.ParallelizedFlow, + ParallelFlowBuilder.MergeStrategy, + ParallelFlowBuilder.Build { + private final List> flows = new ArrayList<>(); + private Function> parallelizeFromArray; + private Flow flowToParallelize; + private BinaryOperator> mergeStrategy; + private String name; + + private BuildSteps() { + } + + public final ParallelFlowBuilder.Parallelize named(String name) { + if (Objects.isNull(name)) { + throw new FlowBuilderException(ParallelFlowBuilder.class, "name is mandatory"); + } + this.name = name; + return this; + } + + public final ParallelFlowBuilder.MergeStrategy parallelize(List> flows) { + if (Objects.isNull(flows)) { + throw new FlowBuilderException(ParallelFlowBuilder.class, "flows are mandatory"); + } + this.flows.addAll(flows); + return this; + } + + @SafeVarargs + public final ParallelFlowBuilder.MergeStrategy parallelize(Flow... flows) { + if (Objects.isNull(flows)) { + throw new FlowBuilderException(ParallelFlowBuilder.class, "flows are mandatory"); + } + this.flows.addAll(List.of(flows)); + return this; + } + + public final ParallelFlowBuilder.ParallelizedFlow parallelizeFromArray(Function> parallelizeFromArray) { + if (Objects.isNull(parallelizeFromArray)) { + throw new FlowBuilderException(ParallelFlowBuilder.class, "parallelizeFromArray is mandatory"); + } + this.parallelizeFromArray = parallelizeFromArray; + return this; + } + + public final ParallelFlowBuilder.MergeStrategy parallelizedFlow(Flow parallelizedFlow) { + if (Objects.isNull(parallelizedFlow)) { + throw new FlowBuilderException(ParallelFlowBuilder.class, "parallelizedFlow is mandatory"); + } + this.flowToParallelize = parallelizedFlow; + return this; + } + + public final ParallelFlowBuilder.Build mergeStrategy(BinaryOperator> mergeStrategy) { + if (Objects.isNull(mergeStrategy)) { + throw new FlowBuilderException(ParallelFlowBuilder.class, "mergeStrategy is mandatory"); + } + this.mergeStrategy = mergeStrategy; + return this; + } + + public final ParallelFlow build() { + return ParallelFlow.create(this.name, this.flows, this.mergeStrategy, this.parallelizeFromArray, this.flowToParallelize); + } + } + + public interface Named { + ParallelFlowBuilder.Parallelize named(String name); + } + + public interface Parallelize { + ParallelFlowBuilder.MergeStrategy parallelize(List> flows); + + ParallelFlowBuilder.MergeStrategy parallelize(Flow... flows); + + ParallelFlowBuilder.ParallelizedFlow parallelizeFromArray(Function> parallelizeFromArray); + + } + + public interface ParallelizedFlow { + ParallelFlowBuilder.MergeStrategy parallelizedFlow(Flow flow); + } + + public interface MergeStrategy { + ParallelFlowBuilder.Build mergeStrategy(BinaryOperator> mergeStrategy); + } + + public interface Build { + ParallelFlow build(); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/builder/RecoverableFlowBuilder.java b/src/main/java/fr/jtools/reactorflow/builder/RecoverableFlowBuilder.java new file mode 100644 index 0000000..1eeef82 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/builder/RecoverableFlowBuilder.java @@ -0,0 +1,91 @@ +package fr.jtools.reactorflow.builder; + +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.exception.RecoverableFlowException; +import fr.jtools.reactorflow.flow.Flow; +import fr.jtools.reactorflow.flow.RecoverableFlow; +import fr.jtools.reactorflow.state.FlowContext; + +import java.util.Objects; + +public final class RecoverableFlowBuilder { + public static RecoverableFlowBuilder.Named defaultBuilder() { + return new RecoverableFlowBuilder.BuildSteps<>(); + } + + public static RecoverableFlowBuilder.Named builderForContextOfType(Class contextClass) { + return new RecoverableFlowBuilder.BuildSteps<>(); + } + + private static final class BuildSteps implements RecoverableFlowBuilder.Named, + RecoverableFlowBuilder.Try, + RecoverableFlowBuilder.Recover, + RecoverableFlowBuilder.RecoverOn, + RecoverableFlowBuilder.Build { + + private String name; + private Flow flow; + private Flow recover; + private RecoverableFlowException recoverOn; + + + private BuildSteps() { + } + + public final RecoverableFlowBuilder.Try named(String name) { + if (Objects.isNull(name)) { + throw new FlowBuilderException(RecoverableFlowBuilder.class, "name is mandatory"); + } + this.name = name; + return this; + } + + public final RecoverableFlowBuilder.Recover tryFlow(Flow flow) { + if (Objects.isNull(flow)) { + throw new FlowBuilderException(RecoverableFlowBuilder.class, "try flow is mandatory"); + } + this.flow = flow; + return this; + } + + public final RecoverableFlowBuilder.RecoverOn recover(Flow recoveredFlow) { + if (Objects.isNull(recoveredFlow)) { + throw new FlowBuilderException(RecoverableFlowBuilder.class, "recovered flow is mandatory"); + } + this.recover = recoveredFlow; + return this; + } + + public final RecoverableFlowBuilder.Build recoverOn(RecoverableFlowException recoverOn) { + if (Objects.isNull(recoverOn)) { + throw new FlowBuilderException(RecoverableFlowBuilder.class, "recoverOn is mandatory"); + } + this.recoverOn = recoverOn; + return this; + } + + public final RecoverableFlow build() { + return RecoverableFlow.create(this.name, this.flow, this.recover, this.recoverOn); + } + } + + public interface Recover { + RecoverableFlowBuilder.RecoverOn recover(Flow flow); + } + + public interface RecoverOn { + RecoverableFlowBuilder.Build recoverOn(RecoverableFlowException recoverOn); + } + + public interface Try { + RecoverableFlowBuilder.Recover tryFlow(Flow flow); + } + + public interface Build { + RecoverableFlow build(); + } + + public interface Named { + RecoverableFlowBuilder.Try named(String name); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/builder/RetryableFlowBuilder.java b/src/main/java/fr/jtools/reactorflow/builder/RetryableFlowBuilder.java new file mode 100644 index 0000000..b31d950 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/builder/RetryableFlowBuilder.java @@ -0,0 +1,104 @@ +package fr.jtools.reactorflow.builder; + +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.exception.RecoverableFlowException; +import fr.jtools.reactorflow.flow.Flow; +import fr.jtools.reactorflow.flow.RetryableFlow; +import fr.jtools.reactorflow.state.FlowContext; + +import java.util.Objects; + +public final class RetryableFlowBuilder { + public static RetryableFlowBuilder.Named defaultBuilder() { + return new RetryableFlowBuilder.BuildSteps<>(); + } + + public static RetryableFlowBuilder.Named builderForContextOfType(Class contextClass) { + return new RetryableFlowBuilder.BuildSteps<>(); + } + + private static final class BuildSteps implements RetryableFlowBuilder.Named, + RetryableFlowBuilder.Try, + RetryableFlowBuilder.RetryOn, + RetryableFlowBuilder.RetryTimes, + RetryableFlowBuilder.Delay, + RetryableFlowBuilder.Build { + + private String name; + private Flow flow; + private RecoverableFlowException retryOn = RecoverableFlowException.TECHNICAL; + private Integer retryTimes = 1; + private Integer delay = 100; + + private BuildSteps() { + } + + public final RetryableFlowBuilder.Try named(String name) { + if (Objects.isNull(name)) { + throw new FlowBuilderException(RetryableFlowBuilder.class, "name is mandatory"); + } + this.name = name; + return this; + } + + public final RetryableFlowBuilder.RetryOn tryFlow(Flow flow) { + if (Objects.isNull(flow)) { + throw new FlowBuilderException(RetryableFlowBuilder.class, "try flow is mandatory"); + } + this.flow = flow; + return this; + } + + public final RetryableFlowBuilder.RetryTimes retryOn(RecoverableFlowException retryOn) { + if (Objects.isNull(retryOn)) { + throw new FlowBuilderException(RetryableFlowBuilder.class, "retryOn is mandatory"); + } + this.retryOn = retryOn; + return this; + } + + public final RetryableFlowBuilder.Delay retryTimes(Integer retryTimes) { + if (Objects.isNull(retryTimes)) { + throw new FlowBuilderException(RetryableFlowBuilder.class, "retryTimes is mandatory"); + } + this.retryTimes = retryTimes; + return this; + } + + public final RetryableFlowBuilder.Build delay(Integer delay) { + if (Objects.isNull(delay)) { + throw new FlowBuilderException(RetryableFlowBuilder.class, "delay is mandatory"); + } + this.delay = delay; + return this; + } + + public final RetryableFlow build() { + return RetryableFlow.create(this.name, this.flow, this.retryTimes, this.delay, this.retryOn); + } + } + + public interface Delay { + RetryableFlowBuilder.Build delay(Integer delay); + } + + public interface RetryOn { + RetryableFlowBuilder.RetryTimes retryOn(RecoverableFlowException retryOn); + } + + public interface RetryTimes { + RetryableFlowBuilder.Delay retryTimes(Integer retryTimes); + } + + public interface Try { + RetryableFlowBuilder.RetryOn tryFlow(Flow flow); + } + + public interface Build { + RetryableFlow build(); + } + + public interface Named { + RetryableFlowBuilder.Try named(String name); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/builder/SequentialFlowBuilder.java b/src/main/java/fr/jtools/reactorflow/builder/SequentialFlowBuilder.java new file mode 100644 index 0000000..7b18178 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/builder/SequentialFlowBuilder.java @@ -0,0 +1,78 @@ +package fr.jtools.reactorflow.builder; + +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.flow.Flow; +import fr.jtools.reactorflow.flow.SequentialFlow; +import fr.jtools.reactorflow.state.FlowContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class SequentialFlowBuilder { + public static SequentialFlowBuilder.Named defaultBuilder() { + return new SequentialFlowBuilder.BuildSteps<>(); + } + + public static SequentialFlowBuilder.Named builderForContextOfType(Class contextClass) { + return new SequentialFlowBuilder.BuildSteps<>(); + } + + private static final class BuildSteps implements SequentialFlowBuilder.Named, + SequentialFlowBuilder.Then, + SequentialFlowBuilder.Finally, + SequentialFlowBuilder.Build { + + private final List> flows = new ArrayList<>(); + + private Flow finalFlow; + private String name; + + private BuildSteps() { + } + + public final SequentialFlowBuilder.Then named(String name) { + if (Objects.isNull(name)) { + throw new FlowBuilderException(SequentialFlowBuilder.class, "name is mandatory"); + } + this.name = name; + return this; + } + + public final SequentialFlowBuilder.Then then(Flow nextFlow) { + if (Objects.isNull(nextFlow)) { + throw new FlowBuilderException(SequentialFlowBuilder.class, "then flow is mandatory"); + } + this.flows.add(nextFlow); + return this; + } + + public final SequentialFlowBuilder.Build doFinally(Flow finalFlow) { + if (Objects.isNull(finalFlow)) { + throw new FlowBuilderException(SequentialFlowBuilder.class, "final flow is mandatory"); + } + this.finalFlow = finalFlow; + return this; + } + + public final SequentialFlow build() { + return SequentialFlow.create(this.name, this.flows, this.finalFlow); + } + } + + public interface Then extends Finally, Build { + SequentialFlowBuilder.Then then(Flow flow); + } + + public interface Finally { + SequentialFlowBuilder.Build doFinally(Flow flow); + } + + public interface Build { + SequentialFlow build(); + } + + public interface Named { + SequentialFlowBuilder.Then named(String name); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/builder/StepFlowBuilder.java b/src/main/java/fr/jtools/reactorflow/builder/StepFlowBuilder.java new file mode 100644 index 0000000..f6417ad --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/builder/StepFlowBuilder.java @@ -0,0 +1,68 @@ +package fr.jtools.reactorflow.builder; + +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.flow.Step; +import fr.jtools.reactorflow.flow.StepFlow; +import fr.jtools.reactorflow.state.FlowContext; + +import java.util.Objects; + +public final class StepFlowBuilder { + public static StepFlowBuilder.Named defaultBuilder() { + return new StepFlowBuilder.BuildSteps<>(); + } + + public static StepFlowBuilder.Named builderForContextOfType(Class contextClass) { + return new StepFlowBuilder.BuildSteps<>(); + } + + public static StepFlowBuilder.Named builderForMetadataType(Class metadataClass) { + return new StepFlowBuilder.BuildSteps<>(); + } + + public static StepFlowBuilder.Named builderForTypes(Class contextClass, Class metadataClass) { + return new StepFlowBuilder.BuildSteps<>(); + } + + private static final class BuildSteps implements StepFlowBuilder.Named, + StepFlowBuilder.Execution, + StepFlowBuilder.Build { + private Step execution; + private String name; + + private BuildSteps() { + } + + public final StepFlowBuilder.Execution named(String name) { + if (Objects.isNull(name)) { + throw new FlowBuilderException(StepFlowBuilder.class, "name is mandatory"); + } + this.name = name; + return this; + } + + public final StepFlowBuilder.Build execution(Step execution) { + if (Objects.isNull(execution)) { + throw new FlowBuilderException(StepFlowBuilder.class, "execution is mandatory"); + } + this.execution = execution; + return this; + } + + public final StepFlow build() { + return StepFlow.create(this.name, this.execution); + } + } + + public interface Build { + StepFlow build(); + } + + public interface Named { + StepFlowBuilder.Execution named(String name); + } + + public interface Execution { + StepFlowBuilder.Build execution(Step execution); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/builder/SwitchFlowBuilder.java b/src/main/java/fr/jtools/reactorflow/builder/SwitchFlowBuilder.java new file mode 100644 index 0000000..113d153 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/builder/SwitchFlowBuilder.java @@ -0,0 +1,150 @@ +package fr.jtools.reactorflow.builder; + +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.flow.Flow; +import fr.jtools.reactorflow.flow.SwitchFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.State; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * Class used to build a {@link SwitchFlow}. + */ +public final class SwitchFlowBuilder { + /** + * Get a default builder for {@link FlowContext} context. + * + * @param Context type + * @return {@link SwitchFlowBuilder.Named} builder step + */ + public static SwitchFlowBuilder.Named defaultBuilder() { + return new SwitchFlowBuilder.BuildSteps<>(); + } + + /** + * Get a builder for a specified context class. + * + * @param contextClass Context class that will be inferred to {@link T} + * @param Context type + * @return {@link SwitchFlowBuilder.Named} builder step + */ + public static SwitchFlowBuilder.Named builderForContextOfType(Class contextClass) { + return new SwitchFlowBuilder.BuildSteps<>(); + } + + private static final class BuildSteps implements SwitchFlowBuilder.Named, + SwitchFlowBuilder.SwitchCondition, + SwitchFlowBuilder.SwitchCase, + SwitchFlowBuilder.DefaultCase, + SwitchFlowBuilder.Build { + /** + * The name. + */ + private String name; + /** + * A {@link Map} containing switch case keys and {@link Flow}s to execute. + */ + private final Map> flows = new HashMap<>(); + /** + * The default {@link Flow} is no switch case matches. + */ + private Flow defaultFlow; + /** + * The switch condition. + */ + private Function, String> switchCondition; + + private BuildSteps() { + } + + public final SwitchCondition named(String name) { + if (Objects.isNull(name)) { + throw new FlowBuilderException(SwitchFlowBuilder.class, "name is mandatory"); + } + this.name = name; + return this; + } + + public final SwitchCase switchCondition(Function, String> switchCondition) { + if (Objects.isNull(switchCondition)) { + throw new FlowBuilderException(SwitchFlowBuilder.class, "switchCondition is mandatory"); + } + this.switchCondition = switchCondition; + return this; + } + + public final SwitchFlowBuilder.SwitchCase switchCase(String key, Flow flow) { + if (Objects.isNull(flow) || Objects.isNull(key)) { + throw new FlowBuilderException(SwitchFlowBuilder.class, "key and switchCase flow are mandatory"); + } + this.flows.put(key, flow); + return this; + } + + public final SwitchFlowBuilder.Build defaultCase(Flow flow) { + if (Objects.isNull(flow)) { + throw new FlowBuilderException(SwitchFlowBuilder.class, "default flow is mandatory"); + } + this.defaultFlow = flow; + return this; + } + + public final SwitchFlow build() { + return SwitchFlow.create(this.name, this.switchCondition, this.flows, this.defaultFlow); + } + } + + public interface SwitchCase extends DefaultCase { + /** + * Define a switch case flow, executed if {@link SwitchFlowBuilder.SwitchCondition} returns the matching key. + * + * @param key A key + * @param flow A {@link Flow} + * @return {@link SwitchFlowBuilder.SwitchCase} builder step + */ + SwitchFlowBuilder.SwitchCase switchCase(String key, Flow flow); + } + + public interface DefaultCase { + /** + * Define the default case, if switch condition match none of the switch cases. + * + * @param flow A {@link Flow} + * @return {@link SwitchFlowBuilder.Build} builder step + */ + SwitchFlowBuilder.Build defaultCase(Flow flow); + } + + public interface Build { + /** + * Build the {@link SwitchFlow}. + * + * @return Built {@link SwitchFlow} + */ + SwitchFlow build(); + } + + public interface Named { + /** + * Define flow name. + * + * @param name The name + * @return {@link SwitchFlowBuilder.SwitchCondition} builder step + */ + SwitchFlowBuilder.SwitchCondition named(String name); + } + + public interface SwitchCondition { + /** + * Define the switch condition that will decide which flow should be executed. + * + * @param switchCondition The switchCondition, a function returning a {@link String} mapped from {@link State} + * @return {@link SwitchFlowBuilder.SwitchCase} builder step + */ + SwitchFlowBuilder.SwitchCase switchCondition(Function, String> switchCondition); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/exception/FlowBuilderException.java b/src/main/java/fr/jtools/reactorflow/exception/FlowBuilderException.java new file mode 100644 index 0000000..506cf54 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/exception/FlowBuilderException.java @@ -0,0 +1,20 @@ +package fr.jtools.reactorflow.exception; + +public final class FlowBuilderException extends FlowException { + public static String mapMessage(Class builder, String message) { + return String.format("%s: %s", builder.getSimpleName(), message); + } + + @Override + public FlowExceptionType getType() { + return FlowExceptionType.BUILDER; + } + + public FlowBuilderException(Class builder, String message) { + super(FlowBuilderException.mapMessage(builder, message)); + } + + public FlowBuilderException(Throwable cause, Class builder, String message) { + super(cause, FlowBuilderException.mapMessage(builder, message)); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/exception/FlowException.java b/src/main/java/fr/jtools/reactorflow/exception/FlowException.java new file mode 100644 index 0000000..ad41960 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/exception/FlowException.java @@ -0,0 +1,96 @@ +package fr.jtools.reactorflow.exception; + +import fr.jtools.reactorflow.flow.Flow; +import fr.jtools.reactorflow.utils.ConsoleStyle; +import fr.jtools.reactorflow.utils.PrettyPrint; + +import java.util.Objects; + +import static fr.jtools.reactorflow.utils.LoggerUtils.colorize; + +public abstract class FlowException extends RuntimeException implements PrettyPrint { + public abstract FlowExceptionType getType(); + + private transient Flow flowConcerned; + + public Flow getFlowConcerned() { + return this.flowConcerned; + } + + protected void setFlowConcerned(Flow flow) { + this.flowConcerned = flow; + } + + public FlowException(String message) { + super(message); + } + + public FlowException(Throwable cause, String message) { + super(message, cause); + } + + public FlowException(Flow flow, String message) { + super(message); + this.setFlowConcerned(flow); + } + + public FlowException(Flow flow, Throwable cause, String message) { + super(message, cause); + this.setFlowConcerned(flow); + } + + + @Override + public String toString() { + if (Objects.isNull(this.getFlowConcerned())) { + return String.format( + "%s exception occurred with message %s", + this.getType().name(), + this.getMessage() + ); + } + return String.format( + "%s exception occurred on %s named %s with message %s (%s)", + this.getType().name(), + this.getFlowConcerned().getClass().getSimpleName(), + this.getFlowConcerned().getName(), + this.getMessage(), + this.getFlowConcerned().hashCode() + ); + } + + @Override + public String toPrettyString() { + if (Objects.isNull(this.getFlowConcerned())) { + return String.format( + "%s exception occurred with message %s", + colorize(this.getType().name(), ConsoleStyle.CYAN_BOLD), + colorize(this.getMessage(), ConsoleStyle.MAGENTA_BOLD) + ); + } + return String.format( + "%s exception occurred on %s named %s with message %s (%s)", + colorize(this.getType().name(), ConsoleStyle.CYAN_BOLD), + colorize(this.getFlowConcerned().getClass().getSimpleName(), ConsoleStyle.BLUE_BOLD), + colorize(this.getFlowConcerned().getName(), ConsoleStyle.WHITE_BOLD), + colorize(this.getMessage(), ConsoleStyle.MAGENTA_BOLD), + colorize(String.valueOf(this.getFlowConcerned().hashCode()), ConsoleStyle.BLACK_BOLD) + ); + } + + public final boolean isRecoverable(RecoverableFlowException recoverable) { + if (recoverable == RecoverableFlowException.ALL) { + return true; + } + + if (recoverable == RecoverableFlowException.FUNCTIONAL && this.getType() == FlowExceptionType.FUNCTIONAL) { + return true; + } + + if (recoverable == RecoverableFlowException.TECHNICAL && this.getType() == FlowExceptionType.TECHNICAL) { + return true; + } + + return false; + } +} diff --git a/src/main/java/fr/jtools/reactorflow/exception/FlowExceptionType.java b/src/main/java/fr/jtools/reactorflow/exception/FlowExceptionType.java new file mode 100644 index 0000000..aedd029 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/exception/FlowExceptionType.java @@ -0,0 +1,7 @@ +package fr.jtools.reactorflow.exception; + +public enum FlowExceptionType { + TECHNICAL, + FUNCTIONAL, + BUILDER +} diff --git a/src/main/java/fr/jtools/reactorflow/exception/FlowFunctionalException.java b/src/main/java/fr/jtools/reactorflow/exception/FlowFunctionalException.java new file mode 100644 index 0000000..c2fe8d4 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/exception/FlowFunctionalException.java @@ -0,0 +1,20 @@ +package fr.jtools.reactorflow.exception; + +import fr.jtools.reactorflow.flow.Flow; + +public final class FlowFunctionalException extends FlowException { + @Override + public FlowExceptionType getType() { + return FlowExceptionType.FUNCTIONAL; + } + + public FlowFunctionalException(Flow flow, String message) { + super(message); + this.setFlowConcerned(flow); + } + + public FlowFunctionalException(Flow flow, Throwable cause, String message) { + super(cause, message); + this.setFlowConcerned(flow); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/exception/FlowTechnicalException.java b/src/main/java/fr/jtools/reactorflow/exception/FlowTechnicalException.java new file mode 100644 index 0000000..ac85524 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/exception/FlowTechnicalException.java @@ -0,0 +1,20 @@ +package fr.jtools.reactorflow.exception; + +import fr.jtools.reactorflow.flow.Flow; + +public final class FlowTechnicalException extends FlowException { + @Override + public FlowExceptionType getType() { + return FlowExceptionType.TECHNICAL; + } + + public FlowTechnicalException(Flow flow, String message) { + super(message); + this.setFlowConcerned(flow); + } + + public FlowTechnicalException(Flow flow, Throwable cause, String message) { + super(cause, message); + this.setFlowConcerned(flow); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/exception/RecoverableFlowException.java b/src/main/java/fr/jtools/reactorflow/exception/RecoverableFlowException.java new file mode 100644 index 0000000..b6a6002 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/exception/RecoverableFlowException.java @@ -0,0 +1,8 @@ +package fr.jtools.reactorflow.exception; + +public enum RecoverableFlowException { + ALL, + TECHNICAL, + FUNCTIONAL, + NONE +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/ConditionalFlow.java b/src/main/java/fr/jtools/reactorflow/flow/ConditionalFlow.java new file mode 100644 index 0000000..269314d --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/ConditionalFlow.java @@ -0,0 +1,134 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.exception.FlowTechnicalException; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.function.Predicate; + +/** + * Class managing a conditional {@link Flow}. + * + * @param Context type + */ +public final class ConditionalFlow extends Flow { + /** + * The name. + */ + private final String name; + /** + * {@link Flow} executed if {@link ConditionalFlow#condition} returns true. + */ + private final Flow flowCaseTrue; + /** + * {@link Flow} executed if {@link ConditionalFlow#condition} returns false. + */ + private final Flow flowCaseFalse; + /** + * The condition. + */ + private final Predicate> condition; + + /** + * Static method used to create a {@link ConditionalFlow}. + * + * @param name {@link ConditionalFlow#name} + * @param condition {@link ConditionalFlow#condition} + * @param flowCaseTrue {@link ConditionalFlow#flowCaseTrue} + * @param flowCaseFalse {@link ConditionalFlow#flowCaseFalse} + * @param Context type + * @return A {@link ConditionalFlow} + */ + public static ConditionalFlow create(String name, Predicate> condition, Flow flowCaseTrue, Flow flowCaseFalse) { + return new ConditionalFlow<>(name, condition, flowCaseTrue, flowCaseFalse); + } + + private ConditionalFlow(String name, Predicate> condition, Flow flowCaseTrue, Flow flowCaseFalse) { + this.name = name; + this.condition = condition; + this.flowCaseTrue = flowCaseTrue; + this.flowCaseFalse = flowCaseFalse; + } + + /** + * Get the {@link ConditionalFlow} name. + * + * @return The name + */ + @Override + public final String getName() { + return name; + } + + /** + * {@link ConditionalFlow} execution. + * It chooses between {@link ConditionalFlow#flowCaseTrue} and {@link ConditionalFlow#flowCaseFalse} depending on {@link ConditionalFlow#condition}. + * + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return The new {@link State} + */ + @Override + protected final Mono> execution(State previousState, Metadata metadata) { + return Mono + .defer(() -> Mono.just(this.condition.test(previousState))) + .onErrorMap(error -> new FlowTechnicalException( + this, + error, + String.format("Error occurred during condition evaluation: %s", error.getMessage()) + )) + .flatMap(result -> Boolean.TRUE.equals(result) ? + this.flowCaseTrue.execute(previousState, Metadata.from(metadata)) : + this.flowCaseFalse.execute(previousState, Metadata.from(metadata)) + ); + } + + /** + * Clone the {@link ConditionalFlow} with a new name. + * + * @param newName {@link ConditionalFlow} new name + * @return Cloned {@link ConditionalFlow} + */ + @Override + public ConditionalFlow cloneFlow(String newName) { + return ConditionalFlow.create( + newName, + this.condition, + this.flowCaseTrue.cloneFlow(), + this.flowCaseFalse.cloneFlow() + ); + } + + /** + * Clone the {@link ConditionalFlow}. + * + * @return Cloned {@link ConditionalFlow} + */ + @Override + public ConditionalFlow cloneFlow() { + return this.cloneFlow(this.getName()); + } + + /** + * Get {@link ConditionalFlow} children, aka the {@link ConditionalFlow#flowCaseTrue} and {@link ConditionalFlow#flowCaseFalse}. + * + * @return A {@link List} containing children {@link Flow}s + */ + @Override + protected final List> getChildren() { + return List.of(this.flowCaseTrue, this.flowCaseFalse); + } + + /** + * Has error policy : a {@link ConditionalFlow} is in error if none of its children succeeded. + * + * @return A {@link Boolean} + */ + @Override + protected boolean flowOrChildrenHasError() { + return !FlowStatusPolicy.flowAndOneChildSucceeded().test(this); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/Flow.java b/src/main/java/fr/jtools/reactorflow/flow/Flow.java new file mode 100644 index 0000000..4c3b9d7 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/Flow.java @@ -0,0 +1,487 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.exception.FlowException; +import fr.jtools.reactorflow.exception.FlowTechnicalException; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import fr.jtools.reactorflow.utils.ConsoleStyle; +import fr.jtools.reactorflow.utils.PrettyPrint; +import reactor.core.publisher.Mono; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import static fr.jtools.reactorflow.utils.LoggerUtils.colorize; + +/** + * Abstract class managing all the {@link Flow}s. + * + * @param Context type + */ +public abstract class Flow implements PrettyPrint { + /** + * The end time in nanosecond. + */ + protected Long endTime = 0L; + /** + * The start time in nanosecond. + */ + protected Long startTime = 0L; + /** + * The status. + * Default is {@link State.Status#IGNORED} as the {@link Flow} is not executed. + */ + protected State.Status status = State.Status.IGNORED; + /** + * A {@link List} containing all the errors. + */ + private final List errors = Collections.synchronizedList(new ArrayList<>()); + /** + * A {@link List} containing all the warnings. + */ + private final List warnings = Collections.synchronizedList(new ArrayList<>()); + /** + * A {@link List} containing all the recovered errors. + */ + private final List recoveredErrors = Collections.synchronizedList(new ArrayList<>()); + + /** + * Abstract method that should return the {@link Flow} name. + * + * @return {@link Flow} name + */ + public abstract String getName(); + + /** + * Abstract method that should implement how the {@link Flow} should be cloned with a new name. + * + * @param newName {@link Flow} new name + * @return Cloned {@link Flow} + */ + public abstract Flow cloneFlow(String newName); + + /** + * Abstract method that should implement how the {@link Flow} should be cloned. + * It should call {@link Flow#cloneFlow(String)} with {@link Flow#getName()} as parameter. + * + * @return Cloned {@link Flow} + */ + public abstract Flow cloneFlow(); + + /** + * Abstract method that should describe how the {@link Flow} is executed. + * + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return A {@link Mono} containing the new {@link State} + */ + protected abstract Mono> execution(State previousState, Metadata metadata); + + /** + * Abstract method that should return a {@link List} containing the actual {@link Flow} children. + * It is mandatory to be able to deduce the resulting status of the {@link Flow}. + * + * @return A {@link List} of {@link Flow} + */ + protected abstract List> getChildren(); + + /** + * Set {@link Flow} end time. + */ + private void setEndTime() { + this.endTime = System.nanoTime(); + } + + /** + * Set {@link Flow} start time. + */ + private void setStartTime() { + this.startTime = System.nanoTime(); + } + + /** + * Set {@link Flow} status. + * + * @param status A {@link State.Status} + */ + private void setStatus(State.Status status) { + this.status = status; + } + + /** + * Get {@link Flow} status. + * + * @return The {@link Flow} {@link State.Status} + */ + public final State.Status getStatus() { + return this.status; + } + + /** + * Get the {@link Flow} execution duration in nanoseconds. + * + * @return A {@link Long} + */ + public final Long getDuration() { + return this.endTime - this.startTime; + } + + /** + * Get the {@link Flow} execution duration in milliseconds. + * + * @return A {@link Double} + */ + public final Double getDurationInMillis() { + return BigDecimal.valueOf(getDuration()).divide(BigDecimal.valueOf(1000000), 2, RoundingMode.HALF_EVEN).doubleValue(); + } + + /** + * Add a {@link FlowException} in {@link Flow#errors}. + * + * @param exception A {@link FlowException} + */ + public final void addError(FlowException exception) { + this.errors.add(exception); + } + + /** + * Add a {@link List} of {@link FlowException} in {@link Flow#errors}. + * + * @param exceptions A {@link List} of {@link FlowException} + */ + public final void addErrors(List exceptions) { + this.errors.addAll(exceptions); + } + + /** + * Add a {@link FlowException} in {@link Flow#warnings}. + * + * @param exception A {@link FlowException} + */ + public final void addWarning(FlowException exception) { + this.warnings.add(exception); + } + + /** + * Add a {@link List} of {@link FlowException} in {@link Flow#warnings}. + * + * @param exceptions A {@link List} of {@link FlowException} + */ + public final void addWarnings(List exceptions) { + this.warnings.addAll(exceptions); + } + + /** + * Add a {@link List} of {@link FlowException} in {@link Flow#recoveredErrors}. + * + * @param exceptions A {@link List} of {@link FlowException} + */ + private void addRecoveredErrors(List exceptions) { + this.recoveredErrors.addAll(exceptions); + } + + /** + * Get a copy of the actual {@link Flow#errors} {@link List}. + * + * @return The copied {@link List} + */ + protected final List getErrors() { + return List.copyOf(this.errors); + } + + /** + * Get a copy of the actual {@link Flow#warnings} {@link List}. + * + * @return The copied {@link List} + */ + protected final List getWarnings() { + return List.copyOf(this.warnings); + } + + /** + * Get a copy of the actual {@link Flow#recoveredErrors} {@link List}. + * + * @return The copied {@link List} + */ + protected final List getRecoveredErrors() { + return List.copyOf(this.recoveredErrors); + } + + /** + * The actual {@link Flow} execution. + * It runs {@link Flow#execution(State, Metadata)} and : + *
    + *
  • calls {@link Flow#setStartTime()}
  • + *
  • adds {@link FlowException} in {@link Flow#errors} in case of raw error
  • + *
  • calls {@link Flow#setEndTime()}
  • + *
  • + * calls {@link Flow#setStatus(State.Status)} with {@link State.Status#SUCCESS}, {@link State.Status#WARNING} or {@link State.Status#ERROR}, + * by calling {@link Flow#flowOrChildrenHasError()} and {@link Flow#flowOrChildrenHasWarning()} + *
  • + *
+ * + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return The new {@link State} + */ + protected final Mono> execute(State previousState, Metadata metadata) { + this.setStartTime(); + return execution(previousState, metadata) + .onErrorResume(throwable -> { + if (throwable instanceof FlowException) { + this.addError((FlowException) throwable); + } else { + this.addError(new FlowTechnicalException(this, throwable, throwable.getMessage())); + } + this.setEndTime(); + this.setStatus(State.Status.ERROR); + return Mono.just(previousState); + }) + .doOnNext(state -> { + this.setEndTime(); + this.setStatus(flowOrChildrenHasError() ? + State.Status.ERROR : + flowOrChildrenHasWarning() ? + State.Status.WARNING : + State.Status.SUCCESS + ); + }); + } + + /** + * Run the {@link Flow} and all its children with an initial {@link T} context. + * + * @param initialContext The initial context + * @return A {@link Mono} containing the resulting {@link State} + */ + public final Mono> run(T initialContext) { + State initialState = State.initiate(initialContext); + initialState.setRoot(this); + return this.execute(initialState, Metadata.empty()); + } + + /** + * Get a {@link String}, representing the actual {@link Flow}, without its children. + * + * @return The {@link String} + */ + @Override + public final String toString() { + return String.format( + "%s named %s (%s)", + this.getClass().getSimpleName(), + this.getName(), + this.hashCode() + ); + } + + /** + * Get a {@link String}, colorized, representing the actual {@link Flow}, without its children. + * + * @return The {@link String} + */ + @Override + public final String toPrettyString() { + return String.format( + "%s named %s (%s)", + colorize(this.getClass().getSimpleName(), ConsoleStyle.BLUE_BOLD), + colorize(this.getName(), ConsoleStyle.WHITE_BOLD), + colorize(String.valueOf(this.hashCode()), ConsoleStyle.BLACK_BOLD) + ); + } + + /** + * Get a {@link String}, colorized, representing the actual {@link Flow} and its children. + * + * @return The {@link String} + */ + public final String toPrettyTreeString() { + return String.format( + "%s%n%s%n", + colorize("Flow tree", ConsoleStyle.MAGENTA_BOLD), + this.printPrettyTree(0) + ); + } + + /** + * Get a {@link String} representing the actual {@link Flow} and its children. + * + * @return The {@link String} + */ + public final String toTreeString() { + return String.format( + "Flow tree%n%s%n", + this.printTree(0) + ); + } + + /** + * Get a {@link String}, colorized, representing the actual {@link Flow} and its children, with a fixed increment. + * + * @param increment An increment + * @return The {@link String} + */ + private String printPrettyTree(int increment) { + StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append(" ".repeat(Math.max(0, increment))); + stringBuilder.append(String.format( + Locale.US, + "%s - %s named %s ended in %s (%s)", + colorize(this.getStatus().name(), State.getStatusConsoleStyle(this.getStatus())), + colorize(this.getClass().getSimpleName(), ConsoleStyle.BLUE_BOLD), + colorize(this.getName(), ConsoleStyle.WHITE_BOLD), + colorize(String.format(Locale.US, "%.2f ms", this.getDurationInMillis()), ConsoleStyle.MAGENTA_BOLD), + colorize(String.valueOf(this.hashCode()), ConsoleStyle.BLACK_BOLD) + )); + + for (Flow child : this.getChildren()) { + stringBuilder.append("\n").append(child.printPrettyTree(increment + 4)); + } + return stringBuilder.toString(); + } + + /** + * Get a {@link String} representing the actual {@link Flow} and its children, with a fixed increment. + * + * @param increment An increment + * @return The {@link String} + */ + private String printTree(int increment) { + StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append(" ".repeat(Math.max(0, increment))); + stringBuilder.append(String.format( + Locale.US, + "%s - %s named %s ended in %s (%s)", + this.getStatus().name(), + this.getClass().getSimpleName(), + this.getName(), + String.format(Locale.US, "%.2f ms", this.getDurationInMillis()), + this.hashCode() + )); + + for (Flow child : this.getChildren()) { + stringBuilder.append("\n").append(child.printTree(increment + 4)); + } + return stringBuilder.toString(); + } + + /** + * Get all errors ({@link FlowException}s) for the actual {@link Flow} and its children. + * + * @return A {@link List} containing all the {@link FlowException}s + */ + public final List getErrorsForFlowAndChildren() { + return this.getErrorsForFlowAndChildren(this, new ArrayList<>()); + } + + /** + * Get all errors ({@link FlowException}s) for a {@link Flow} and its children. + * + * @param flow A {@link Flow} + * @param initList A mutable {@link List} that will contains the {@link FlowException}s + * @return A {@link List} containing all the {@link FlowException}s + */ + private List getErrorsForFlowAndChildren(Flow flow, List initList) { + initList.addAll(flow.errors); + + flow.getChildren().forEach(child -> this.getErrorsForFlowAndChildren(child, initList)); + + return initList; + } + + /** + * Clean all errors ({@link FlowException}s) for the actual {@link Flow} and its children, + * and add it to {@link Flow#recoveredErrors}. + */ + protected final void cleanErrorsForFlowAndChildren() { + this.cleanErrorsForFlowAndChildren(this); + } + + /** + * Clean all errors ({@link FlowException}s) for a {@link Flow} and its children, + * and add it to its {@link Flow#recoveredErrors}. + * + * @param flow A {@link Flow} + */ + private void cleanErrorsForFlowAndChildren(Flow flow) { + flow.addRecoveredErrors(flow.errors); + flow.errors.clear(); + + flow.getChildren().forEach(this::cleanErrorsForFlowAndChildren); + } + + /** + * Get all warnings ({@link FlowException}s) for the actual {@link Flow} and its children. + * + * @return A {@link List} containing all the {@link FlowException}s + */ + public final List getWarningsForFlowAndChildren() { + return this.getWarningsForFlowAndChildren(this, new ArrayList<>()); + } + + /** + * Get all warnings ({@link FlowException}s) for a {@link Flow} and its children. + * + * @param flow A {@link Flow} + * @param initList A mutable {@link List} that will contains the {@link FlowException}s + * @return A {@link List} containing all the {@link FlowException}s + */ + private List getWarningsForFlowAndChildren(Flow flow, List initList) { + initList.addAll(flow.warnings); + + flow.getChildren().forEach(child -> this.getWarningsForFlowAndChildren(child, initList)); + + return initList; + } + + /** + * Get all recovered errors ({@link FlowException}s) for the actual {@link Flow} and its children. + * + * @return A {@link List} containing all the {@link FlowException}s + */ + public final List getRecoveredErrorsForFlowAndChildren() { + return this.getRecoveredErrorsForFlowAndChildren(this, new ArrayList<>()); + } + + /** + * Get all recovered errors ({@link FlowException}s) for a {@link Flow} and its children. + * + * @param flow A {@link Flow} + * @param initList A mutable {@link List} that will contains the {@link FlowException}s + * @return A {@link List} containing all the {@link FlowException}s + */ + private List getRecoveredErrorsForFlowAndChildren(Flow flow, List initList) { + initList.addAll(flow.recoveredErrors); + + flow.getChildren().forEach(child -> this.getRecoveredErrorsForFlowAndChildren(child, initList)); + + return initList; + } + + /** + * Overridable method to know if the actual {@link Flow} has errors (by also checking its children). + * Default is that actual {@link Flow} and all its children should have no error to return false. + * + * @return A {@link Boolean} + */ + protected boolean flowOrChildrenHasError() { + return !FlowStatusPolicy.flowAndAllChildrenSucceeded().test(this); + } + + /** + * Overridable method to know if the actual {@link Flow} has warnings (by also checking its children). + * Default is that actual {@link Flow} and all its children should have no warning to return false. + * + * @return A {@link Boolean} + */ + protected boolean flowOrChildrenHasWarning() { + return FlowStatusPolicy.flowOrChildHasWarning().test(this); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/FlowStatusPolicy.java b/src/main/java/fr/jtools/reactorflow/flow/FlowStatusPolicy.java new file mode 100644 index 0000000..90936b3 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/FlowStatusPolicy.java @@ -0,0 +1,68 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.state.State; + +import java.util.function.Predicate; + +/** + * Class used to manage {@link Flow#status}. + */ +public final class FlowStatusPolicy implements Predicate> { + /** + * A {@link Predicate} that returns true or false. + */ + private final Predicate> hasStatus; + + /** + * {@link FlowStatusPolicy} that checks {@link Flow} and at least one of its children have succeeded. + * + * @return A {@link FlowStatusPolicy} + */ + public static FlowStatusPolicy flowAndOneChildSucceeded() { + return new FlowStatusPolicy( + flow -> { + Predicate> isSuccessOrWarning = f -> (f.getStatus().equals(State.Status.SUCCESS) || f.getStatus().equals(State.Status.WARNING)); + return flow.getErrors().isEmpty() && flow.getChildren().stream().anyMatch(isSuccessOrWarning); + } + ); + } + + /** + * {@link FlowStatusPolicy} that checks {@link Flow} and all its children have succeeded. + * + * @return A {@link FlowStatusPolicy} + */ + public static FlowStatusPolicy flowAndAllChildrenSucceeded() { + return new FlowStatusPolicy( + flow -> { + Predicate> isSuccessOrWarning = f -> (f.getStatus().equals(State.Status.SUCCESS) || f.getStatus().equals(State.Status.WARNING)); + return flow.getErrors().isEmpty() && flow.getChildren().stream().allMatch(isSuccessOrWarning); + } + ); + } + + /** + * {@link FlowStatusPolicy} that checks {@link Flow} or at least one of its children have a warning. + * + * @return A {@link FlowStatusPolicy} + */ + public static FlowStatusPolicy flowOrChildHasWarning() { + return new FlowStatusPolicy( + flow -> !flow.getWarnings().isEmpty() || flow.getChildren().stream().anyMatch(f -> f.getStatus().equals(State.Status.WARNING)) + ); + } + + /** + * Construct a {@link FlowStatusPolicy}. + * + * @param hasStatus A {@link Predicate} + */ + public FlowStatusPolicy(Predicate> hasStatus) { + this.hasStatus = hasStatus; + } + + @Override + public boolean test(Flow flow) { + return this.hasStatus.test(flow); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/NoOpFlow.java b/src/main/java/fr/jtools/reactorflow/flow/NoOpFlow.java new file mode 100644 index 0000000..a52f124 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/NoOpFlow.java @@ -0,0 +1,90 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.List; + +/** + * Class managing a no op {@link Flow} (aka a {@link Flow} that does nothing). + * + * @param Context type + */ +public final class NoOpFlow extends Flow { + /** + * The name. + */ + private final String name; + + /** + * Static method used to create a {@link NoOpFlow}. + * + * @param name {@link NoOpFlow#name} + * @param Context type + * @return A {@link NoOpFlow} + */ + public static NoOpFlow named(String name) { + return new NoOpFlow<>(name); + } + + private NoOpFlow(String name) { + this.name = name; + } + + /** + * Get the {@link ConditionalFlow} name. + * + * @return The name + */ + @Override + public final String getName() { + return name; + } + + /** + * {@link NoOpFlow} execution. + * It just returns the previous {@link State}. + * + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return The new {@link State} + */ + @Override + protected final Mono> execution(State previousState, Metadata metadata) { + return Mono.just(previousState); + } + + /** + * Clone the {@link NoOpFlow} with a new name. + * + * @param newName {@link NoOpFlow} new name + * @return Cloned {@link NoOpFlow} + */ + @Override + public NoOpFlow cloneFlow(String newName) { + return NoOpFlow.named(newName); + } + + /** + * Clone the {@link NoOpFlow}. + * + * @return Cloned {@link NoOpFlow} + */ + @Override + public NoOpFlow cloneFlow() { + return this.cloneFlow(this.getName()); + } + + /** + * Get {@link NoOpFlow} children, aka {@link Collections#emptyList()}. + * + * @return An empty list + */ + @Override + protected List> getChildren() { + return Collections.emptyList(); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/ParallelFlow.java b/src/main/java/fr/jtools/reactorflow/flow/ParallelFlow.java new file mode 100644 index 0000000..c028517 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/ParallelFlow.java @@ -0,0 +1,219 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.builder.ParallelFlowBuilder; +import fr.jtools.reactorflow.exception.FlowTechnicalException; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Class managing a parallel {@link Flow}. + * + * @param Context type + * @param Metadata type + */ +public final class ParallelFlow extends Flow { + /** + * The name. + */ + private final String name; + /** + * The list of {@link Flow} to parallelize, if {@link ParallelFlow#parallelizeFromArray} is not defined. + */ + private final List> flows; + /** + * The merge strategy used for merging all {@link State} states after all {@link Flow}s execution. + */ + private final BinaryOperator> mergeStrategy; + /** + * The function used to extract an array from {@link T} context, + * in order to execute {@link ParallelFlow#flowToParallelize}, with items from extracted array as {@link M} metadata. + */ + private final Function> parallelizeFromArray; + /** + * The {@link Flow} to parallelize, if {@link ParallelFlow#parallelizeFromArray} is defined. + */ + private final Flow flowToParallelize; + + /** + * Field used to store copied {@link Flow}s from {@link ParallelFlow#flowToParallelize} when {@link ParallelFlow#parallelizeFromArray} is defined. + * It is used in {@link ParallelFlow#getChildren()}. + */ + private final List> flowsToParallelizeFromArray = new ArrayList<>(); + + /** + * Static method used to create a {@link ParallelFlow}. + * + * @param name {@link ParallelFlow#name} + * @param flows {@link ParallelFlow#flows} + * @param mergeStrategy {@link ParallelFlow#mergeStrategy} + * @param parallelizeFromArray {@link ParallelFlow#parallelizeFromArray} + * @param flowToParallelize {@link ParallelFlow#flowToParallelize} + * @param Context type + * @param Metadata type + * @return A {@link ParallelFlow} + */ + public static ParallelFlow create(String name, List> flows, BinaryOperator> mergeStrategy, Function> parallelizeFromArray, Flow flowToParallelize) { + return new ParallelFlow<>(name, flows, mergeStrategy, parallelizeFromArray, flowToParallelize); + } + + private ParallelFlow(String name, List> flows, BinaryOperator> mergeStrategy, Function> parallelizeFromArray, Flow flowToParallelize) { + this.name = name; + this.flows = flows; + this.mergeStrategy = mergeStrategy; + this.parallelizeFromArray = parallelizeFromArray; + this.flowToParallelize = flowToParallelize; + } + + /** + * Get the {@link ParallelFlow} name. + * + * @return The name + */ + @Override + public final String getName() { + return name; + } + + /** + * {@link ParallelFlow} execution. + * It chooses between {@link ParallelFlow#executeFlows} and {@link ParallelFlow#executeFlowOnArray}, + * if {@link ParallelFlow#parallelizeFromArray} is defined or not. + * + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return The new {@link State} + */ + @Override + protected final Mono> execution(State previousState, Metadata metadata) { + if (Objects.nonNull(this.parallelizeFromArray)) { + return executeFlowOnArray(previousState, metadata); + } + + return executeFlows(previousState, metadata); + } + + /** + * {@link ParallelFlow#flows} execution. + * It executes all the {@link Flow} in {@link ParallelFlow#flows} array, in parallel. + * + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return The new {@link State} + */ + private Mono> executeFlows(State previousState, Metadata metadata) { + return Flux + .merge(this.flows.stream().map(flow -> flow.execute(previousState, Metadata.from(metadata))).collect(Collectors.toList())) + .collectList() + .map(newReports -> this.mergeReports(newReports, previousState)); + } + + /** + * {@link ParallelFlow#flowToParallelize} execution. + * It executes copies of {@link ParallelFlow#flowToParallelize} for each item contains in the return of {@link ParallelFlow#flowsToParallelizeFromArray}. + * It also adds as {@link M} metadata the item from the array. + * + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return The new {@link State} + */ + private Mono> executeFlowOnArray(State previousState, Metadata metadata) { + AtomicInteger counter = new AtomicInteger(); + + return Mono + .defer(() -> Mono.just(this.parallelizeFromArray.apply(previousState.getContext()))) + .onErrorMap(error -> new FlowTechnicalException( + this, + error, + String.format("Error occurred during parallelizeFromArray evaluation: %s", error.getMessage()) + )) + .flatMapMany(Flux::fromIterable) + .flatMap(data -> { + Flow copiedFlow = this.flowToParallelize.cloneFlow(String.format("%s (%d)", this.flowToParallelize.getName(), counter.incrementAndGet())); + this.flowsToParallelizeFromArray.add(copiedFlow); + + return copiedFlow.execute( + previousState, + Metadata.create(data).addErrors(metadata.getErrors()).addWarnings(metadata.getWarnings()) + ); + }) + .collectList() + .map(newReports -> this.mergeReports(newReports, previousState)); + } + + /** + * Clone the {@link ParallelFlow} with a new name. + * + * @param newName {@link ParallelFlow} new name + * @return Cloned {@link ParallelFlow} + */ + @Override + public ParallelFlow cloneFlow(String newName) { + return ParallelFlow.create( + newName, + this.flows.stream().map(Flow::cloneFlow).collect(Collectors.toList()), + this.mergeStrategy, + this.parallelizeFromArray, + Objects.nonNull(this.flowToParallelize) ? this.flowToParallelize.cloneFlow() : null + ); + } + + /** + * Clone the {@link ParallelFlow}. + * + * @return Cloned {@link ParallelFlow} + */ + @Override + public final ParallelFlow cloneFlow() { + return this.cloneFlow(this.getName()); + } + + /** + * Get {@link ParallelFlow} children, aka : + *
    + *
  • {@link ParallelFlow#flows}, if {@link ParallelFlow#parallelizeFromArray} is defined
  • + *
  • + * {@link ParallelFlow#flowsToParallelizeFromArray} (copies of {@link ParallelFlow#flowToParallelize}), + * if {@link ParallelFlow#parallelizeFromArray} is not defined + *
  • + *
+ * + * @return A {@link List} containing children {@link Flow}s + */ + @Override + protected final List> getChildren() { + return Objects.nonNull(parallelizeFromArray) ? this.flowsToParallelizeFromArray : this.flows; + } + + /** + * Method used to merge all the {@link State} resulting of the {@link ParallelFlow execution}. + * The default use case is to use {@link ParallelFlowBuilder#defaultMergeStrategy()}, that will keep a random one. + * As {@link T} context should be thread safe, all executed {@link Flow} will populate it, and we can keep any of the {@link State}. + * + * @param newStates The new {@link State}s + * @param previousState The previous {@link State}, if {@link ParallelFlow#mergeStrategy} fails + * @return The merged {@link State} + */ + private State mergeReports(List> newStates, State previousState) { + try { + return newStates.stream().reduce(previousState, this.mergeStrategy); + } catch (Exception exception) { + this.addWarning(new FlowTechnicalException( + this, + exception, + String.format("Error occurred during mergeStrategy evaluation: %s", exception.getMessage()) + )); + return previousState; + } + } +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/RecoverableFlow.java b/src/main/java/fr/jtools/reactorflow/flow/RecoverableFlow.java new file mode 100644 index 0000000..8beb2e4 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/RecoverableFlow.java @@ -0,0 +1,74 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.exception.FlowException; +import fr.jtools.reactorflow.exception.RecoverableFlowException; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Objects; + +public final class RecoverableFlow extends Flow { + private final String name; + private final Flow flow; + private Flow recover = NoOpFlow.named("Default"); + private RecoverableFlowException recoverOn = RecoverableFlowException.TECHNICAL; + + public static RecoverableFlow create(String name, Flow flow, Flow recover, RecoverableFlowException recoverOn) { + return new RecoverableFlow<>(name, flow, recover, recoverOn); + } + + private RecoverableFlow(String name, Flow flow, Flow recover, RecoverableFlowException recoverOn) { + this.name = name; + this.flow = flow; + if (Objects.nonNull(recover)) { + this.recover = recover; + } + if (Objects.nonNull(recoverOn)) { + this.recoverOn = recoverOn; + } + } + + @Override + public final String getName() { + return this.name; + } + + @Override + protected final Mono> execution(State previousState, Metadata metadata) { + return this.flow.execute(previousState, Metadata.from(metadata)) + .flatMap(state -> { + List exceptionsForFlow = this.flow.getErrorsForFlowAndChildren(); + if ( + !exceptionsForFlow.isEmpty() && + exceptionsForFlow.stream().allMatch(exception -> exception.isRecoverable(this.recoverOn)) + ) { + this.flow.cleanErrorsForFlowAndChildren(); + return this.recover.execute(state, Metadata.from(metadata)); + } + return Mono.just(state); + }); + } + + @Override + public final RecoverableFlow cloneFlow(String newName) { + return RecoverableFlow.create(newName, this.flow.cloneFlow(), this.recover.cloneFlow(), this.recoverOn); + } + + @Override + public final RecoverableFlow cloneFlow() { + return this.cloneFlow(this.getName()); + } + + @Override + protected final List> getChildren() { + return List.of(this.flow, this.recover); + } + + @Override + protected boolean flowOrChildrenHasError() { + return !FlowStatusPolicy.flowAndOneChildSucceeded().test(this); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/RetryableFlow.java b/src/main/java/fr/jtools/reactorflow/flow/RetryableFlow.java new file mode 100644 index 0000000..58cd1b0 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/RetryableFlow.java @@ -0,0 +1,104 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.exception.FlowException; +import fr.jtools.reactorflow.exception.RecoverableFlowException; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class RetryableFlow extends Flow { + private final Flow flow; + private final List> flowsToRetry; + private final String name; + private RecoverableFlowException retryOn = RecoverableFlowException.TECHNICAL; + private Integer retryTimes = 1; + private Integer delay = 100; + + public static RetryableFlow create(String name, Flow flow, Integer retryTimes, Integer delay, RecoverableFlowException retryOn) { + return new RetryableFlow<>(name, flow, retryTimes, delay, retryOn); + } + + private RetryableFlow(String name, Flow flow, Integer retryTimes, Integer delay, RecoverableFlowException retryOn) { + this.name = name; + this.flow = flow; + if (Objects.nonNull(retryTimes)) { + this.retryTimes = retryTimes; + } + if (Objects.nonNull(delay)) { + this.delay = delay; + } + if (Objects.nonNull(retryOn)) { + this.retryOn = retryOn; + } + + ArrayList> toRetry = new ArrayList<>(); + for (int c = 0; c < this.retryTimes; c++) { + toRetry.add(this.flow.cloneFlow(String.format("%s (Retry %d)", this.flow.getName(), c + 1))); + } + this.flowsToRetry = toRetry; + } + + @Override + public final String getName() { + return name; + } + + @Override + protected final Mono> execution(State previousState, Metadata metadata) { + return this.tryExecution(previousState, new AtomicInteger(0), this.flow, metadata); + } + + @Override + public final RetryableFlow cloneFlow(String newName) { + return RetryableFlow.create(newName, this.flow.cloneFlow(), this.retryTimes, this.delay, this.retryOn); + } + + @Override + public final RetryableFlow cloneFlow() { + return this.cloneFlow(this.getName()); + } + + @Override + protected final List> getChildren() { + return Stream.concat(Stream.of(flow), this.flowsToRetry.stream()).collect(Collectors.toList()); + } + + @Override + protected boolean flowOrChildrenHasError() { + return !FlowStatusPolicy.flowAndOneChildSucceeded().test(this); + } + + private Mono> tryExecution(State previousState, AtomicInteger counter, Flow flow, Metadata metadata) { + return flow.execute(previousState, Metadata.from(metadata)) + .flatMap(state -> { + int count = counter.getAndIncrement(); + List exceptionsForFlow = flow.getErrorsForFlowAndChildren(); + if ( + !exceptionsForFlow.isEmpty() && + count < this.retryTimes && + exceptionsForFlow.stream().allMatch(exception -> exception.isRecoverable(this.retryOn)) + ) { + flow.cleanErrorsForFlowAndChildren(); + return Mono + .delay(Duration.ofMillis(delay)) + .flatMap(unused -> this.tryExecution( + state, + counter, + this.flowsToRetry.get(count), + Metadata.from(metadata) + )); + } + + return Mono.just(state); + }); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/SequentialFlow.java b/src/main/java/fr/jtools/reactorflow/flow/SequentialFlow.java new file mode 100644 index 0000000..0d49ad0 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/SequentialFlow.java @@ -0,0 +1,166 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Class managing a sequential {@link Flow}. + * + * @param Context type + */ +public final class SequentialFlow extends Flow { + /** + * {@link List} of {@link Flow}s to execute sequentially. + */ + private final List> flows; + /** + * Final {@link Flow} to execute, even if a previous {@link Flow} has an error. + */ + private final Flow finalFlow; + /** + * The name. + */ + private final String name; + + /** + * Static method used to create a {@link SequentialFlow}. + * + * @param name {@link SequentialFlow#name} + * @param flows {@link SequentialFlow#flows} + * @param finalFlow {@link SequentialFlow#finalFlow} + * @param Context type + * @return A {@link SequentialFlow} + */ + public static SequentialFlow create(String name, List> flows, Flow finalFlow) { + return new SequentialFlow<>(name, flows, finalFlow); + } + + private SequentialFlow(String name, List> flows, Flow finalFlow) { + this.name = name; + this.flows = flows; + this.finalFlow = finalFlow; + } + + /** + * Get the {@link SequentialFlow} name. + * + * @return The name + */ + @Override + public final String getName() { + return name; + } + + /** + * {@link SequentialFlow} execution. + * It sequentially executes {@link SequentialFlow#flows}, + * and executes {@link SequentialFlow#finalFlow} when all {@link SequentialFlow#flows} are executed, + * or after the first one with an error. + * + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return The new {@link State} + */ + @Override + protected final Mono> execution(State previousState, Metadata metadata) { + Mono> newState = Mono.just(previousState); + + for (Flow flow : this.flows) { + newState = newState + .flatMap(state -> this.executeFlow(flow, state, Metadata.from(metadata))); + } + + return newState + .flatMap(stateBeforeFinalFlow -> Objects.isNull(this.finalFlow) ? + Mono.just(stateBeforeFinalFlow) : + this.executeFinalFlow( + this.finalFlow, + stateBeforeFinalFlow, + Metadata.from(metadata) + .addErrors(this.getErrorsForFlowAndChildren()) + .addWarnings(this.getWarningsForFlowAndChildren()) + ) + ); + } + + /** + * Clone the {@link SequentialFlow} with a new name. + * + * @param newName {@link SequentialFlow} new name + * @return Cloned {@link SequentialFlow} + */ + @Override + public final SequentialFlow cloneFlow(String newName) { + return SequentialFlow.create( + newName, + this.flows.stream().map(Flow::cloneFlow).collect(Collectors.toList()), + Objects.nonNull(this.finalFlow) ? this.finalFlow.cloneFlow() : null + ); + } + + /** + * Clone the {@link SequentialFlow}. + * + * @return Cloned {@link SequentialFlow} + */ + @Override + public final SequentialFlow cloneFlow() { + return this.cloneFlow(this.getName()); + } + + /** + * Get {@link SequentialFlow} children, aka : + *
    + *
  • {@link SequentialFlow#flows}
  • + *
  • {@link SequentialFlow#finalFlow}, if defined
  • + *
+ * + * @return A {@link List} containing children {@link Flow}s + */ + @Override + protected final List> getChildren() { + List> children = new ArrayList<>(this.flows); + + if (Objects.nonNull(this.finalFlow)) { + children.add(this.finalFlow); + } + + return children; + } + + /** + * Executes the next {@link Flow}, + * or return the previous {@link State} if an error had occurred previously in the {@link SequentialFlow}. + * + * @param flow The next {@link Flow} + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return The new {@link State} + */ + private Mono> executeFlow(Flow flow, State previousState, Metadata metadata) { + if (!this.getErrorsForFlowAndChildren().isEmpty()) { + return Mono.just(previousState); + } + + return flow.execute(previousState, metadata); + } + + /** + * Executes the final {@link Flow}. + * + * @param finalFlow The final {@link Flow} + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return The new {@link State} + */ + private Mono> executeFinalFlow(Flow finalFlow, State previousState, Metadata metadata) { + return finalFlow.execute(previousState, metadata); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/Step.java b/src/main/java/fr/jtools/reactorflow/flow/Step.java new file mode 100644 index 0000000..730fd93 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/Step.java @@ -0,0 +1,16 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import fr.jtools.reactorflow.utils.TriFunction; +import reactor.core.publisher.Mono; + +/** + * An interface defining a {@link StepFlow} execution. + * + * @param Context type + * @param Metadata type + */ +public interface Step extends TriFunction, State, Metadata, Mono>> { +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/StepExecution.java b/src/main/java/fr/jtools/reactorflow/flow/StepExecution.java new file mode 100644 index 0000000..0801c26 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/StepExecution.java @@ -0,0 +1,44 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.builder.StepFlowBuilder; +import fr.jtools.reactorflow.state.FlowContext; + +/** + * Abstract class used to create custom step executions. + * Can be extended in any application, with dependency injection. + * + * @param Context type + * @param Metadata type + */ +public abstract class StepExecution implements Step { + /** + * Static method used to a {@link StepFlow} from a {@link Step} (or a {@link StepExecution}) and a name. + * + * @param step An object implementing {@link Step} interface + * @param name {@link StepFlow} name + * @param Context type + * @param Metadata type + * @return A {@link StepFlow} + */ + public static StepFlow buildFlow(Step step, String name) { + return StepFlowBuilder + .defaultBuilder() + .named(name) + .execution(step) + .build(); + } + + /** + * Build a {@link StepFlow} from the {@link StepExecution}. + * + * @param name {@link StepFlow} name + * @return A {@link StepFlow} + */ + public final StepFlow buildFlowNamed(String name) { + return StepFlowBuilder + .defaultBuilder() + .named(name) + .execution(this) + .build(); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/StepFlow.java b/src/main/java/fr/jtools/reactorflow/flow/StepFlow.java new file mode 100644 index 0000000..594c61a --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/StepFlow.java @@ -0,0 +1,110 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.exception.FlowTechnicalException; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.List; + +/** + * Class managing a step {@link Flow} (aka a real execution, not other {@link Flow}s management). + * + * @param Context type + * @param Metadata type + */ +public final class StepFlow extends Flow { + /** + * The execution. + */ + private final Step execution; + /** + * The name. + */ + private final String name; + + /** + * Static method used to create a {@link StepFlow}. + * + * @param name {@link StepFlow#name} + * @param execution {@link StepFlow#execution} + * @param Context type + * @param Metadata type + * @return A {@link StepFlow} + */ + public static StepFlow create(String name, Step execution) { + return new StepFlow<>(name, execution); + } + + private StepFlow(String name, Step execution) { + this.name = name; + this.execution = execution; + } + + /** + * Get the {@link StepFlow} name. + * + * @return The name + */ + @Override + public final String getName() { + return name; + } + + /** + * Clone the {@link StepFlow} with a new name. + * + * @param newName {@link StepFlow} new name + * @return Cloned {@link StepFlow} + */ + @Override + public final StepFlow cloneFlow(String newName) { + return StepFlow.create(newName, this.execution); + } + + /** + * Clone the {@link StepFlow}. + * + * @return Cloned {@link StepFlow} + */ + @Override + public final StepFlow cloneFlow() { + return this.cloneFlow(this.getName()); + } + + /** + * {@link ParallelFlow} execution. + * Executes {@link StepFlow#execution}. + * + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return The new {@link State} + */ + @Override + @SuppressWarnings("unchecked") + protected final Mono> execution(State previousState, Metadata metadata) { + return Mono + .defer(() -> Mono.just(((Metadata) metadata))) + .flatMap(meta -> execution.apply( + this, + previousState, + Metadata.create(meta.getData()).addErrors(meta.getErrors()).addWarnings(meta.getWarnings()) + )) + .onErrorResume(ClassCastException.class, error -> { + this.addError(new FlowTechnicalException(this, error, String.format("Can not convert metadata to target type: %s", error.getMessage().split(" \\(")[0]))); + return Mono.just(previousState); + }); + } + + /** + * Get {@link StepFlow} children, aka {@link Collections#emptyList()}. + * + * @return An empty list + */ + @Override + protected final List> getChildren() { + return Collections.emptyList(); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/flow/SwitchFlow.java b/src/main/java/fr/jtools/reactorflow/flow/SwitchFlow.java new file mode 100644 index 0000000..8791dcd --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/flow/SwitchFlow.java @@ -0,0 +1,153 @@ +package fr.jtools.reactorflow.flow; + +import fr.jtools.reactorflow.exception.FlowTechnicalException; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Class managing a switch {@link Flow}. + * + * @param Context type + */ +public final class SwitchFlow extends Flow { + /** + * The name. + */ + private final String name; + /** + * A {@link Map} containing switch case keys and {@link Flow}s to execute. + */ + private final Map> flows; + /** + * The default {@link Flow} is no switch case matches. + */ + private final Flow defaultFlow; + /** + * The switch condition. + */ + private final Function, String> switchCondition; + + /** + * Static method used to create a {@link SwitchFlow}. + * + * @param name {@link SwitchFlow#name} + * @param switchCondition {@link SwitchFlow#switchCondition} + * @param flows {@link SwitchFlow#flows} + * @param defaultFlow {@link SwitchFlow#defaultFlow} + * @param Context type + * @return A {@link SwitchFlow} + */ + public static SwitchFlow create(String name, Function, String> switchCondition, Map> flows, Flow defaultFlow) { + return new SwitchFlow<>(name, switchCondition, flows, defaultFlow); + } + + private SwitchFlow(String name, Function, String> switchCondition, Map> flows, Flow defaultFlow) { + this.name = name; + this.switchCondition = switchCondition; + this.flows = flows; + this.defaultFlow = defaultFlow; + } + + + /** + * Get the {@link SwitchFlow} name. + * + * @return The name + */ + @Override + public String getName() { + return this.name; + } + + /** + * Clone the {@link SwitchFlow} with a new name. + * + * @param newName {@link SwitchFlow} new name + * @return Cloned {@link SwitchFlow} + */ + @Override + public SwitchFlow cloneFlow(String newName) { + return SwitchFlow.create( + newName, + this.switchCondition, + this.flows + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().cloneFlow())), + this.defaultFlow.cloneFlow() + ); + } + + /** + * Clone the {@link SwitchFlow}. + * + * @return Cloned {@link SwitchFlow} + */ + @Override + public SwitchFlow cloneFlow() { + return this.cloneFlow(this.getName()); + } + + /** + * {@link SwitchFlow} execution. + * It chooses the matching {@link Flow} in {@link SwitchFlow#flows} {@link Map} depending on {@link SwitchFlow#switchCondition}, + * and executes it, or executes {@link SwitchFlow#defaultFlow} if there is no match. + * + * @param previousState The previous {@link State} + * @param metadata A {@link Metadata} object + * @return The new {@link State} + */ + @Override + protected Mono> execution(State previousState, Metadata metadata) { + return Mono + .defer(() -> Mono.just(this.switchCondition.apply(previousState))) + .onErrorMap(error -> new FlowTechnicalException( + this, + error, + String.format("Error occurred during switchCondition evaluation: %s", error.getMessage()) + )) + .flatMap(switchResult -> { + Flow toExecute = this.flows.get(switchResult); + + if (Objects.nonNull(toExecute)) { + return toExecute.execute(previousState, metadata); + } + + return this.defaultFlow.execute(previousState, metadata); + }); + } + + /** + * Get {@link SwitchFlow} children, aka the {@link Flow}s in {@link SwitchFlow#flows} map and the {@link SwitchFlow#defaultFlow}. + * + * @return A {@link List} containing children {@link Flow}s + */ + @Override + protected List> getChildren() { + return Stream + .concat( + this.flows.values().stream(), + Stream.of(this.defaultFlow) + ) + .collect(Collectors.toList()); + } + + /** + * Has error policy : a {@link SwitchFlow} is in error if none of its children succeeded. + * + * @return A {@link Boolean} + */ + @Override + protected boolean flowOrChildrenHasError() { + return !FlowStatusPolicy.flowAndOneChildSucceeded().test(this); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/state/FlowContext.java b/src/main/java/fr/jtools/reactorflow/state/FlowContext.java new file mode 100644 index 0000000..198c6e1 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/state/FlowContext.java @@ -0,0 +1,79 @@ +package fr.jtools.reactorflow.state; + +import fr.jtools.reactorflow.utils.ConsoleStyle; +import fr.jtools.reactorflow.utils.PrettyPrint; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static fr.jtools.reactorflow.utils.LoggerUtils.colorize; + +/** + * Default context, using a ConcurrentHashMap, in order to be thread safe during ParallelFlow. + * Be careful about the objects inside the ConcurrentHashMap as they can not be thread safe. + * If you prefer use your own context, be careful about thread safe considerations for ParallelFlow. + */ +public class FlowContext implements PrettyPrint { + private Map context = new ConcurrentHashMap<>(); + + public static FlowContext create() { + return new FlowContext(); + } + + public static FlowContext createFrom(Map initialMap) { + return new FlowContext(initialMap); + } + + public FlowContext() { + } + + private FlowContext(Map initialMap) { + context = new ConcurrentHashMap<>(initialMap); + } + + public void put(String key, Object value) { + this.context.put(key, value); + } + + public Object get(String key) { + return this.context.get(key); + } + + public Set> getEntrySet() { + return this.context.entrySet(); + } + + @Override + public String toString() { + return String.format( + "Context%n%s%n", + this.getEntrySet() + .stream() + .map(entry -> String.format( + "%s - %s", + entry.getKey(), + Objects.nonNull(entry.getKey()) ? entry.getValue().toString() : "null" + )) + .collect(Collectors.joining("\n")) + ); + } + + @Override + public String toPrettyString() { + return String.format( + "%s%n%s%n", + colorize("Context", ConsoleStyle.MAGENTA_BOLD), + this.getEntrySet() + .stream() + .map(entry -> String.format( + "%s - %s", + colorize(entry.getKey(), ConsoleStyle.BLUE_BOLD), + Objects.nonNull(entry.getKey()) ? entry.getValue().toString() : "null" + )) + .collect(Collectors.joining("\n")) + ); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/state/Metadata.java b/src/main/java/fr/jtools/reactorflow/state/Metadata.java new file mode 100644 index 0000000..4f85e60 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/state/Metadata.java @@ -0,0 +1,54 @@ +package fr.jtools.reactorflow.state; + +import fr.jtools.reactorflow.exception.FlowException; + +import java.util.ArrayList; +import java.util.List; + +public class Metadata { + private final List errors = new ArrayList<>(); + private final List warnings = new ArrayList<>(); + private final M data; + + public static Metadata create(M data) { + return new Metadata<>(data); + } + + public static Metadata empty() { + return new Metadata<>(null); + } + + public static Metadata from(Metadata metadata) { + return Metadata.from(metadata, metadata.getData()); + } + + public static Metadata from(Metadata metadata, M newData) { + return new Metadata<>(newData).addErrors(metadata.getErrors()).addWarnings(metadata.getWarnings()); + } + + private Metadata(M data) { + this.data = data; + } + + public Metadata addErrors(List exceptions) { + this.errors.addAll(exceptions); + return this; + } + + public Metadata addWarnings(List exceptions) { + this.warnings.addAll(exceptions); + return this; + } + + public List getErrors() { + return this.errors; + } + + public List getWarnings() { + return this.warnings; + } + + public M getData() { + return this.data; + } +} diff --git a/src/main/java/fr/jtools/reactorflow/state/State.java b/src/main/java/fr/jtools/reactorflow/state/State.java new file mode 100644 index 0000000..3c9411e --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/state/State.java @@ -0,0 +1,196 @@ +package fr.jtools.reactorflow.state; + +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.exception.FlowException; +import fr.jtools.reactorflow.flow.Flow; +import fr.jtools.reactorflow.utils.ConsoleStyle; +import fr.jtools.reactorflow.utils.PrettyPrint; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static fr.jtools.reactorflow.utils.LoggerUtils.colorize; + +public class State implements PrettyPrint { + private final T context; + private Flow root; + + public static State initiate(T initialContext) { + return new State<>(initialContext); + } + + public static State initiateDefaultFrom(FlowContext initialContext) { + return new State<>(initialContext); + } + + public static State initiateDefault() { + return new State<>(FlowContext.create()); + } + + public State(T initialContext) { + this.context = initialContext; + } + + public T getContext() { + return this.context; + } + + public Status getStatus() { + return this.root.getStatus(); + } + + public List getAllErrors() { + return List.copyOf(this.root.getErrorsForFlowAndChildren()); + } + + public List getAllWarnings() { + return List.copyOf(this.root.getWarningsForFlowAndChildren()); + } + + public List getAllRecoveredErrors() { + return List.copyOf(this.root.getRecoveredErrorsForFlowAndChildren()); + } + + public String getName() { + return this.root.getName(); + } + + public void setRoot(Flow flow) { + if (Objects.nonNull(this.root)) { + throw new FlowBuilderException(State.class, "root can not be set two times"); + } + this.root = flow; + } + + @Override + public String toString() { + return String.format( + "Summary%n%s - %s named %s ended in %s (%s)%n", + this.root.getStatus().name(), + this.root.getClass().getSimpleName(), + this.root.getName(), + String.format(Locale.US, "%.2f ms", this.root.getDurationInMillis()), + this.root.hashCode() + ); + } + + @Override + public String toPrettyString() { + return String.format( + "%s%n%s - %s named %s ended in %s (%s)%n", + colorize("Summary", ConsoleStyle.MAGENTA_BOLD), + colorize(this.root.getStatus().name(), State.getStatusConsoleStyle(this.root.getStatus())), + colorize(this.root.getClass().getSimpleName(), ConsoleStyle.BLUE_BOLD), + colorize(this.root.getName(), ConsoleStyle.WHITE_BOLD), + colorize(String.format(Locale.US, "%.2f ms", this.root.getDurationInMillis()), ConsoleStyle.MAGENTA_BOLD), + colorize(String.valueOf(this.root.hashCode()), ConsoleStyle.BLACK_BOLD) + ); + } + + public String toTreeString() { + return this.root.toTreeString(); + } + + public String toPrettyTreeString() { + return this.root.toPrettyTreeString(); + } + + public String toPrettyErrorsAndWarningsString() { + List exceptionMessages = Stream + .concat( + this.root.getErrorsForFlowAndChildren() + .stream() + .map(error -> String.format( + "%s - %s", + colorize(Status.ERROR.name(), ConsoleStyle.RED_BOLD), + error.toPrettyString() + )), + Stream.concat( + this.root.getWarningsForFlowAndChildren() + .stream() + .map(error -> String.format( + "%s - %s", + colorize(Status.WARNING.name(), ConsoleStyle.YELLOW_BOLD), + error.toPrettyString() + )), + this.root.getRecoveredErrorsForFlowAndChildren() + .stream() + .map(error -> String.format( + "%s - %s", + colorize("RECOVERED", ConsoleStyle.WHITE_BOLD), + error.toPrettyString() + )) + ) + ) + .collect(Collectors.toList()); + + return String.format( + "%s%n%s%n", + colorize("Errors and warnings", ConsoleStyle.MAGENTA_BOLD), + exceptionMessages.isEmpty() ? + colorize("No error or warning", ConsoleStyle.GREEN_BOLD) : + String.join("\n", exceptionMessages) + ); + } + + public String toErrorsAndWarningsString() { + List exceptionMessages = Stream + .concat( + this.root.getErrorsForFlowAndChildren() + .stream() + .map(error -> String.format( + "%s - %s", + Status.ERROR.name(), + error.toString() + )), + Stream.concat( + this.root.getWarningsForFlowAndChildren() + .stream() + .map(error -> String.format( + "%s - %s", + Status.WARNING.name(), + error.toString() + )), + this.root.getRecoveredErrorsForFlowAndChildren() + .stream() + .map(error -> String.format( + "%s - %s", + "RECOVERED", + error.toString() + )) + ) + ) + .collect(Collectors.toList()); + + return String.format( + "%s%n%s%n", + "Errors and warnings", + exceptionMessages.isEmpty() ? + "No error or warning" : + String.join("\n", exceptionMessages) + ); + } + + public static ConsoleStyle getStatusConsoleStyle(State.Status status) { + switch (status) { + case SUCCESS: + return ConsoleStyle.GREEN_BOLD; + case ERROR: + return ConsoleStyle.RED_BOLD; + case WARNING: + return ConsoleStyle.YELLOW_BOLD; + default: + return ConsoleStyle.WHITE_BOLD; + } + } + + public enum Status { + IGNORED, + WARNING, + SUCCESS, + ERROR + } +} diff --git a/src/main/java/fr/jtools/reactorflow/utils/ConsoleStyle.java b/src/main/java/fr/jtools/reactorflow/utils/ConsoleStyle.java new file mode 100644 index 0000000..9de7209 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/utils/ConsoleStyle.java @@ -0,0 +1,87 @@ +package fr.jtools.reactorflow.utils; + +public enum ConsoleStyle { + //Color end string, color reset + RESET("\033[0m"), + + // Regular Colors. Normal color, no bold, background color etc. + BLACK("\033[0;30m"), // BLACK + RED("\033[0;31m"), // RED + GREEN("\033[0;32m"), // GREEN + YELLOW("\033[0;33m"), // YELLOW + BLUE("\033[0;34m"), // BLUE + MAGENTA("\033[0;35m"), // MAGENTA + CYAN("\033[0;36m"), // CYAN + WHITE("\033[0;37m"), // WHITE + + // Bold + BLACK_BOLD("\033[1;30m"), // BLACK + RED_BOLD("\033[1;31m"), // RED + GREEN_BOLD("\033[1;32m"), // GREEN + YELLOW_BOLD("\033[1;33m"), // YELLOW + BLUE_BOLD("\033[1;34m"), // BLUE + MAGENTA_BOLD("\033[1;35m"), // MAGENTA + CYAN_BOLD("\033[1;36m"), // CYAN + WHITE_BOLD("\033[1;37m"), // WHITE + + // Underline + BLACK_UNDERLINED("\033[4;30m"), // BLACK + RED_UNDERLINED("\033[4;31m"), // RED + GREEN_UNDERLINED("\033[4;32m"), // GREEN + YELLOW_UNDERLINED("\033[4;33m"), // YELLOW + BLUE_UNDERLINED("\033[4;34m"), // BLUE + MAGENTA_UNDERLINED("\033[4;35m"), // MAGENTA + CYAN_UNDERLINED("\033[4;36m"), // CYAN + WHITE_UNDERLINED("\033[4;37m"), // WHITE + + // Background + BLACK_BACKGROUND("\033[40m"), // BLACK + RED_BACKGROUND("\033[41m"), // RED + GREEN_BACKGROUND("\033[42m"), // GREEN + YELLOW_BACKGROUND("\033[43m"), // YELLOW + BLUE_BACKGROUND("\033[44m"), // BLUE + MAGENTA_BACKGROUND("\033[45m"), // MAGENTA + CYAN_BACKGROUND("\033[46m"), // CYAN + WHITE_BACKGROUND("\033[47m"), // WHITE + + // High Intensity + BLACK_BRIGHT("\033[0;90m"), // BLACK + RED_BRIGHT("\033[0;91m"), // RED + GREEN_BRIGHT("\033[0;92m"), // GREEN + YELLOW_BRIGHT("\033[0;93m"), // YELLOW + BLUE_BRIGHT("\033[0;94m"), // BLUE + MAGENTA_BRIGHT("\033[0;95m"), // MAGENTA + CYAN_BRIGHT("\033[0;96m"), // CYAN + WHITE_BRIGHT("\033[0;97m"), // WHITE + + // Bold High Intensity + BLACK_BOLD_BRIGHT("\033[1;90m"), // BLACK + RED_BOLD_BRIGHT("\033[1;91m"), // RED + GREEN_BOLD_BRIGHT("\033[1;92m"), // GREEN + YELLOW_BOLD_BRIGHT("\033[1;93m"), // YELLOW + BLUE_BOLD_BRIGHT("\033[1;94m"), // BLUE + MAGENTA_BOLD_BRIGHT("\033[1;95m"), // MAGENTA + CYAN_BOLD_BRIGHT("\033[1;96m"), // CYAN + WHITE_BOLD_BRIGHT("\033[1;97m"), // WHITE + + // High Intensity backgrounds + BLACK_BACKGROUND_BRIGHT("\033[0;100m"), // BLACK + RED_BACKGROUND_BRIGHT("\033[0;101m"), // RED + GREEN_BACKGROUND_BRIGHT("\033[0;102m"), // GREEN + YELLOW_BACKGROUND_BRIGHT("\033[0;103m"), // YELLOW + BLUE_BACKGROUND_BRIGHT("\033[0;104m"), // BLUE + MAGENTA_BACKGROUND_BRIGHT("\033[0;105m"), // MAGENTA + CYAN_BACKGROUND_BRIGHT("\033[0;106m"), // CYAN + WHITE_BACKGROUND_BRIGHT("\033[0;107m"); // WHITE + + private final String code; + + ConsoleStyle(String code) { + this.code = code; + } + + @Override + public String toString() { + return code; + } +} \ No newline at end of file diff --git a/src/main/java/fr/jtools/reactorflow/utils/LoggerUtils.java b/src/main/java/fr/jtools/reactorflow/utils/LoggerUtils.java new file mode 100644 index 0000000..2394a83 --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/utils/LoggerUtils.java @@ -0,0 +1,15 @@ +package fr.jtools.reactorflow.utils; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public class LoggerUtils { + private LoggerUtils() { + } + + public static String colorize(String string, ConsoleStyle... consoleStyles) { + String attributes = Arrays.stream(consoleStyles).map(ConsoleStyle::toString).collect(Collectors.joining()); + String reset = Arrays.stream(consoleStyles).map(unused -> ConsoleStyle.RESET.toString()).collect(Collectors.joining()); + return String.format("%s%s%s", attributes, string, reset); + } +} diff --git a/src/main/java/fr/jtools/reactorflow/utils/PrettyPrint.java b/src/main/java/fr/jtools/reactorflow/utils/PrettyPrint.java new file mode 100644 index 0000000..98c726c --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/utils/PrettyPrint.java @@ -0,0 +1,5 @@ +package fr.jtools.reactorflow.utils; + +public interface PrettyPrint { + String toPrettyString(); +} diff --git a/src/main/java/fr/jtools/reactorflow/utils/TriFunction.java b/src/main/java/fr/jtools/reactorflow/utils/TriFunction.java new file mode 100644 index 0000000..e07e08c --- /dev/null +++ b/src/main/java/fr/jtools/reactorflow/utils/TriFunction.java @@ -0,0 +1,24 @@ +package fr.jtools.reactorflow.utils; + +import java.util.Objects; +import java.util.function.Function; + +/** + * A tri function implementation. + * + * @param First parameter type + * @param Second parameter type + * @param Third parameter type + * @param Result type + */ +@FunctionalInterface +public interface TriFunction { + + R apply(A a, B b, C c); + + default TriFunction andThen( + Function after) { + Objects.requireNonNull(after); + return (A a, B b, C c) -> after.apply(apply(a, b, c)); + } +} diff --git a/src/test/java/fr/jtools/reactorflow/ConditionalFlowTest.java b/src/test/java/fr/jtools/reactorflow/ConditionalFlowTest.java new file mode 100644 index 0000000..e41877d --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/ConditionalFlowTest.java @@ -0,0 +1,353 @@ +package fr.jtools.reactorflow; + +import fr.jtools.reactorflow.builder.ConditionalFlowBuilder; +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.flow.ConditionalFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.State; +import fr.jtools.reactorflow.testutils.CustomContext; +import fr.jtools.reactorflow.testutils.ErrorMonoStepFlow; +import fr.jtools.reactorflow.testutils.ErrorRawStepFlow; +import fr.jtools.reactorflow.testutils.ErrorStepFlow; +import fr.jtools.reactorflow.testutils.SuccessStepFlow; +import fr.jtools.reactorflow.testutils.WarningStepFlow; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import static fr.jtools.reactorflow.testutils.TestUtils.assertAndLog; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +final class ConditionalFlowTest { + @Test + final void givenSuccessCaseTrueFlow_conditionalFlow_shouldSuccess() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.TRUE) + .caseTrue(SuccessStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getContext().get("Case True")).isEqualTo("Case True"); + assertThat(state.getContext().get("Case False")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenClonedWithNewNameSuccessCaseTrueFlow_conditionalFlow_shouldSuccess() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.TRUE) + .caseTrue(SuccessStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.cloneFlow("Test copy").run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test copy"); + assertThat(state.getContext().get("Case True")).isEqualTo("Case True"); + assertThat(state.getContext().get("Case False")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + + @Test + final void givenClonedSuccessCaseTrueFlow_conditionalFlow_shouldSuccess() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.TRUE) + .caseTrue(SuccessStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.cloneFlow().run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getContext().get("Case True")).isEqualTo("Case True"); + assertThat(state.getContext().get("Case False")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenSuccessCaseTrueFlowWithCustomContext_conditionalFlow_shouldSuccess() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .builderForContextOfType(CustomContext.class) + .named("Test") + .condition(state -> Boolean.TRUE) + .caseTrue(SuccessStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.run(new CustomContext())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().customField).isNotNull(); + assertThat(state.getContext().get("Case True")).isEqualTo("Case True"); + assertThat(state.getContext().get("Case False")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenFailedCaseTrueFlow_conditionalFlow_shouldError() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.TRUE) + .caseTrue(ErrorStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case True")).isNull(); + assertThat(state.getContext().get("Case False")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenRawFailedCaseTrueFlow_conditionalFlow_shouldError() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.TRUE) + .caseTrue(ErrorRawStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case True")).isNull(); + assertThat(state.getContext().get("Case False")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenMonoFailedCaseTrueFlow_conditionalFlow_shouldError() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.TRUE) + .caseTrue(ErrorMonoStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case True")).isNull(); + assertThat(state.getContext().get("Case False")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenWarningCaseTrueFlow_conditionalFlow_shouldWarning() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.TRUE) + .caseTrue(WarningStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case True")).isEqualTo("Case True"); + assertThat(state.getContext().get("Case False")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.WARNING); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).hasSize(1); + })) + .verifyComplete(); + } + + @Test + final void givenSuccessCaseFalseFlow_conditionalFlow_shouldSuccess() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.FALSE) + .caseTrue(SuccessStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getContext().get("Case False")).isEqualTo("Case False"); + assertThat(state.getContext().get("Case True")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenFailedCaseFalseFlow_conditionalFlow_shouldError() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.FALSE) + .caseTrue(SuccessStepFlow.flowNamed("Case True")) + .caseFalse(ErrorStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case True")).isNull(); + assertThat(state.getContext().get("Case False")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenWarningCaseFalseFlow_conditionalFlow_shouldWarning() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.FALSE) + .caseTrue(SuccessStepFlow.flowNamed("Case True")) + .caseFalse(WarningStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case False")).isEqualTo("Case False"); + assertThat(state.getContext().get("Case True")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.WARNING); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).hasSize(1); + })) + .verifyComplete(); + } + + @Test + final void givenErrorInCondition_conditionalFlow_shouldWarning() { + ConditionalFlow conditionalFlow = ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> { + throw new RuntimeException("Raw error"); + }) + .caseTrue(SuccessStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build(); + + StepVerifier + .create(conditionalFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case False")).isNull(); + assertThat(state.getContext().get("Case True")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenNullName_conditionalFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> ConditionalFlowBuilder + .defaultBuilder() + .named(null) + .condition(state -> Boolean.TRUE) + .caseTrue(SuccessStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build() + ); + } + + @Test + final void givenNullCaseFalse_conditionalFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.TRUE) + .caseTrue(SuccessStepFlow.flowNamed("Case True")) + .caseFalse(null) + .build() + ); + } + + @Test + final void givenNullCaseTrue_conditionalFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(state -> Boolean.TRUE) + .caseTrue(null) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build() + ); + } + + @Test + final void givenNullCondition_conditionalFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> ConditionalFlowBuilder + .defaultBuilder() + .named("Test") + .condition(null) + .caseTrue(SuccessStepFlow.flowNamed("Case True")) + .caseFalse(SuccessStepFlow.flowNamed("Case False")) + .build() + ); + } +} diff --git a/src/test/java/fr/jtools/reactorflow/NoOpFlowTest.java b/src/test/java/fr/jtools/reactorflow/NoOpFlowTest.java new file mode 100644 index 0000000..ce80424 --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/NoOpFlowTest.java @@ -0,0 +1,44 @@ +package fr.jtools.reactorflow; + +import fr.jtools.reactorflow.flow.NoOpFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.testutils.CustomContext; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import static fr.jtools.reactorflow.testutils.TestUtils.assertAndLog; +import static org.assertj.core.api.Assertions.assertThat; + +final class NoOpFlowTest { + @Test + final void noOpFlow_shouldKeepState() { + StepVerifier + .create(NoOpFlow.named("Test").run(FlowContext.create())) + .expectNextCount(1) + .verifyComplete(); + } + + @Test + final void noOpFlow_shouldKeepCustomState() { + StepVerifier + .create(NoOpFlow.named("Test").run(new CustomContext())) + .assertNext(assertAndLog(state -> assertThat(state.getContext().customField).isEqualTo("CUSTOM"))) + .verifyComplete(); + } + + @Test + final void givenClonedWithNewName_noOpFlow_shouldKeepState() { + StepVerifier + .create(NoOpFlow.named("Test").cloneFlow("Test copy").run(FlowContext.create())) + .assertNext(assertAndLog(state -> assertThat(state.getName()).isEqualTo("Test copy"))) + .verifyComplete(); + } + + @Test + final void givenCloned_noOpFlow_shouldKeepState() { + StepVerifier + .create(NoOpFlow.named("Test").cloneFlow().run(FlowContext.create())) + .assertNext(assertAndLog(state -> assertThat(state.getName()).isEqualTo("Test"))) + .verifyComplete(); + } +} diff --git a/src/test/java/fr/jtools/reactorflow/ParallelFlowTest.java b/src/test/java/fr/jtools/reactorflow/ParallelFlowTest.java new file mode 100644 index 0000000..74d32f6 --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/ParallelFlowTest.java @@ -0,0 +1,4 @@ +package fr.jtools.reactorflow; + +public class ParallelFlowTest { +} diff --git a/src/test/java/fr/jtools/reactorflow/RecoverableFlowTest.java b/src/test/java/fr/jtools/reactorflow/RecoverableFlowTest.java new file mode 100644 index 0000000..d5e447a --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/RecoverableFlowTest.java @@ -0,0 +1,282 @@ +package fr.jtools.reactorflow; + +import fr.jtools.reactorflow.builder.RecoverableFlowBuilder; +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.exception.RecoverableFlowException; +import fr.jtools.reactorflow.flow.RecoverableFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.State; +import fr.jtools.reactorflow.testutils.CustomContext; +import fr.jtools.reactorflow.testutils.ErrorMonoStepFlow; +import fr.jtools.reactorflow.testutils.ErrorRawStepFlow; +import fr.jtools.reactorflow.testutils.ErrorStepFlow; +import fr.jtools.reactorflow.testutils.SuccessStepFlow; +import fr.jtools.reactorflow.testutils.WarningStepFlow; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import static fr.jtools.reactorflow.testutils.TestUtils.assertAndLog; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +final class RecoverableFlowTest { + @Test + final void givenSuccessTryFlow_recoverableFlow_shouldSuccess() { + RecoverableFlow recoverableFlow = RecoverableFlowBuilder + .defaultBuilder() + .named("Test") + .tryFlow(SuccessStepFlow.flowNamed("Tried")) + .recover(SuccessStepFlow.flowNamed("Recover")) + .recoverOn(RecoverableFlowException.ALL) + .build(); + + StepVerifier + .create(recoverableFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getContext().get("Tried")).isEqualTo("Tried"); + assertThat(state.getContext().get("Recover")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenClonedWithNewNameSuccessTryFlow_recoverableFlow_shouldSuccess() { + RecoverableFlow recoverableFlow = RecoverableFlowBuilder + .defaultBuilder() + .named("Test") + .tryFlow(SuccessStepFlow.flowNamed("Tried")) + .recover(SuccessStepFlow.flowNamed("Recover")) + .recoverOn(RecoverableFlowException.ALL) + .build(); + + StepVerifier + .create(recoverableFlow.cloneFlow("Test copy").run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test copy"); + assertThat(state.getContext().get("Tried")).isEqualTo("Tried"); + assertThat(state.getContext().get("Recover")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + + @Test + final void givenClonedSuccessTryFlow_recoverableFlow_shouldSuccess() { + RecoverableFlow recoverableFlow = RecoverableFlowBuilder + .defaultBuilder() + .named("Test") + .tryFlow(SuccessStepFlow.flowNamed("Tried")) + .recover(SuccessStepFlow.flowNamed("Recover")) + .recoverOn(RecoverableFlowException.ALL) + .build(); + + StepVerifier + .create(recoverableFlow.cloneFlow().run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getContext().get("Tried")).isEqualTo("Tried"); + assertThat(state.getContext().get("Recover")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenSuccessTryFlowWithCustomContext_recoverableFlow_shouldSuccess() { + RecoverableFlow recoverableFlow = RecoverableFlowBuilder + .builderForContextOfType(CustomContext.class) + .named("Test") + .tryFlow(SuccessStepFlow.flowNamed("Tried")) + .recover(SuccessStepFlow.flowNamed("Recover")) + .recoverOn(RecoverableFlowException.ALL) + .build(); + + StepVerifier + .create(recoverableFlow.run(new CustomContext())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().customField).isNotNull(); + assertThat(state.getContext().get("Tried")).isEqualTo("Tried"); + assertThat(state.getContext().get("Recover")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenFailedTryFlowAndRecoverMatching_recoverableFlow_shouldSuccess() { + RecoverableFlow recoverableFlow = RecoverableFlowBuilder + .defaultBuilder() + .named("Test") + .tryFlow(ErrorStepFlow.flowNamed("Tried")) + .recover(SuccessStepFlow.flowNamed("Recover")) + .recoverOn(RecoverableFlowException.ALL) + .build(); + + StepVerifier + .create(recoverableFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Tried")).isNull(); + assertThat(state.getContext().get("Recover")).isEqualTo("Recover"); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).hasSize(1); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenRawFailedTryFlowAndRecoverMatching_recoverableFlow_shouldSuccess() { + RecoverableFlow recoverableFlow = RecoverableFlowBuilder + .defaultBuilder() + .named("Test") + .tryFlow(ErrorRawStepFlow.flowNamed("Tried")) + .recover(SuccessStepFlow.flowNamed("Recover")) + .recoverOn(RecoverableFlowException.ALL) + .build(); + + StepVerifier + .create(recoverableFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Tried")).isNull(); + assertThat(state.getContext().get("Recover")).isEqualTo("Recover"); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).hasSize(1); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenMonoFailedTryFlowAndRecoverMatching_recoverableFlow_shouldSuccess() { + RecoverableFlow recoverableFlow = RecoverableFlowBuilder + .defaultBuilder() + .named("Test") + .tryFlow(ErrorMonoStepFlow.flowNamed("Tried")) + .recover(SuccessStepFlow.flowNamed("Recover")) + .recoverOn(RecoverableFlowException.ALL) + .build(); + + StepVerifier + .create(recoverableFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Tried")).isNull(); + assertThat(state.getContext().get("Recover")).isEqualTo("Recover"); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).hasSize(1); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenFailedTryFlowAndRecoverMatchingWithWarning_recoverableFlow_shouldWarning() { + RecoverableFlow recoverableFlow = RecoverableFlowBuilder + .defaultBuilder() + .named("Test") + .tryFlow(ErrorStepFlow.flowNamed("Tried")) + .recover(WarningStepFlow.flowNamed("Recover")) + .recoverOn(RecoverableFlowException.ALL) + .build(); + + StepVerifier + .create(recoverableFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Tried")).isNull(); + assertThat(state.getContext().get("Recover")).isEqualTo("Recover"); + assertThat(state.getStatus()).isEqualTo(State.Status.WARNING); + assertThat(state.getAllRecoveredErrors()).hasSize(1); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).hasSize(1); + })) + .verifyComplete(); + } + + @Test + final void givenFailedTryFlowAndRecoverNotMatching_recoverableFlow_shouldError() { + RecoverableFlow recoverableFlow = RecoverableFlowBuilder + .defaultBuilder() + .named("Test") + .tryFlow(ErrorStepFlow.flowNamed("Tried")) + .recover(SuccessStepFlow.flowNamed("Recover")) + .recoverOn(RecoverableFlowException.FUNCTIONAL) + .build(); + + StepVerifier + .create(recoverableFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Tried")).isNull(); + assertThat(state.getContext().get("Recover")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenNullName_recoverableFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> RecoverableFlowBuilder + .defaultBuilder() + .named(null) + .tryFlow(SuccessStepFlow.flowNamed("Tried")) + .recover(SuccessStepFlow.flowNamed("Recover")) + .recoverOn(RecoverableFlowException.ALL) + .build() + ); + } + + @Test + final void givenNullTryFlow_recoverableFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> RecoverableFlowBuilder + .defaultBuilder() + .named("Test") + .tryFlow(null) + .recover(SuccessStepFlow.flowNamed("Recover")) + .recoverOn(RecoverableFlowException.ALL) + .build() + ); + } + + @Test + final void givenNullRecover_recoverableFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> RecoverableFlowBuilder + .defaultBuilder() + .named("Test") + .tryFlow(SuccessStepFlow.flowNamed("Tried")) + .recover(null) + .recoverOn(RecoverableFlowException.ALL) + .build() + ); + } + + @Test + final void givenNullRecoverOn_recoverableFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> RecoverableFlowBuilder + .defaultBuilder() + .named("Test") + .tryFlow(SuccessStepFlow.flowNamed("Tried")) + .recover(SuccessStepFlow.flowNamed("Recover")) + .recoverOn(null) + .build() + ); + } +} diff --git a/src/test/java/fr/jtools/reactorflow/RetryableFlowTest.java b/src/test/java/fr/jtools/reactorflow/RetryableFlowTest.java new file mode 100644 index 0000000..6e11f93 --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/RetryableFlowTest.java @@ -0,0 +1,4 @@ +package fr.jtools.reactorflow; + +public class RetryableFlowTest { +} diff --git a/src/test/java/fr/jtools/reactorflow/SequentialFlowTest.java b/src/test/java/fr/jtools/reactorflow/SequentialFlowTest.java new file mode 100644 index 0000000..07c07f2 --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/SequentialFlowTest.java @@ -0,0 +1,4 @@ +package fr.jtools.reactorflow; + +public class SequentialFlowTest { +} diff --git a/src/test/java/fr/jtools/reactorflow/StepFlowTest.java b/src/test/java/fr/jtools/reactorflow/StepFlowTest.java new file mode 100644 index 0000000..aceddbf --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/StepFlowTest.java @@ -0,0 +1,374 @@ +package fr.jtools.reactorflow; + +import fr.jtools.reactorflow.builder.ParallelFlowBuilder; +import fr.jtools.reactorflow.builder.StepFlowBuilder; +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.exception.FlowExceptionType; +import fr.jtools.reactorflow.exception.FlowFunctionalException; +import fr.jtools.reactorflow.exception.FlowTechnicalException; +import fr.jtools.reactorflow.flow.ParallelFlow; +import fr.jtools.reactorflow.flow.StepExecution; +import fr.jtools.reactorflow.flow.StepFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import fr.jtools.reactorflow.testutils.CustomContext; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.io.StreamTokenizer; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static fr.jtools.reactorflow.testutils.TestUtils.assertAndLog; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +final class StepFlowTest { + @Test + final void givenNullName_stepFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> StepFlowBuilder + .defaultBuilder() + .named(null) + .execution((flow, state, metadata) -> Mono.just(state)) + .build() + ); + } + + @Test + final void givenNullExecution_stepFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> StepFlowBuilder + .defaultBuilder() + .named("Test") + .execution(null) + .build() + ); + } + + @Test + final void givenSuccessLambdaExecution_stepFlow_shouldSuccess() { + StepFlow stepFlow = StepFlowBuilder + .defaultBuilder() + .named("Test") + .execution((flow, state, metadata) -> { + state.getContext().put("Test", "Test"); + return Mono.just(state); + }) + .build(); + + StepVerifier + .create(stepFlow.run(FlowContext.create())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getContext().get("Test")).isEqualTo("Test"); + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + }) + .verifyComplete(); + } + + @Test + final void givenClonedWithNewNameSuccessLambdaExecution_stepFlow_shouldSuccess() { + StepFlow stepFlow = StepFlowBuilder + .defaultBuilder() + .named("Test") + .execution((flow, state, metadata) -> { + state.getContext().put("Test", "Test"); + return Mono.just(state); + }) + .build(); + + StepVerifier + .create(stepFlow.cloneFlow("Test cloned").run(FlowContext.create())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getContext().get("Test")).isEqualTo("Test"); + assertThat(state.getName()).isEqualTo("Test cloned"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + }) + .verifyComplete(); + } + + @Test + final void givenClonedSuccessLambdaExecution_stepFlow_shouldSuccess() { + StepFlow stepFlow = StepFlowBuilder + .defaultBuilder() + .named("Test") + .execution((flow, state, metadata) -> { + state.getContext().put("Test", "Test"); + return Mono.just(state); + }) + .build(); + + StepVerifier + .create(stepFlow.cloneFlow().run(FlowContext.create())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getContext().get("Test")).isEqualTo("Test"); + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + }) + .verifyComplete(); + } + + @Test + final void givenSuccessClassExecution_stepFlow_shouldSuccess() { + StepFlow stepFlow = StepFlowBuilder + .defaultBuilder() + .named("Test") + .execution(new TestWork()) + .build(); + + StepVerifier + .create(stepFlow.run(FlowContext.create())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getContext().get("Test")).isEqualTo("Test"); + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + }) + .verifyComplete(); + } + + @Test + final void givenSuccessLambdaExecutionAndCustomMeta_stepFlow_shouldSuccess() { + StepFlow stepFlow = StepFlowBuilder + .builderForMetadataType(String.class) + .named("Test") + .execution((flow, state, metadata) -> { + state.getContext().put("Test", "Test"); + return Mono.just(state); + }) + .build(); + + StepVerifier + .create(stepFlow.run(FlowContext.create())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getContext().get("Test")).isEqualTo("Test"); + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + }) + .verifyComplete(); + } + + @Test + final void givenSuccessLambdaExecutionAndCustomContext_stepFlow_shouldSuccess() { + StepFlow stepFlow = StepFlowBuilder + .builderForContextOfType(CustomContext.class) + .named("Test") + .execution((flow, state, metadata) -> { + state.getContext().put("Test", "Test"); + return Mono.just(state); + }) + .build(); + + StepVerifier + .create(stepFlow.run(new CustomContext())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getContext().customField).isNotNull(); + assertThat(state.getContext().get("Test")).isEqualTo("Test"); + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + }) + .verifyComplete(); + } + + @Test + final void givenSuccessLambdaExecutionAndCustomContextAndCustomMeta_stepFlow_shouldSuccess() { + StepFlow stepFlow = StepFlowBuilder + .builderForTypes(CustomContext.class, String.class) + .named("Test") + .execution((flow, state, metadata) -> { + state.getContext().put("Test", "Test"); + return Mono.just(state); + }) + .build(); + + StepVerifier + .create(stepFlow.run(new CustomContext())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getContext().customField).isNotNull(); + assertThat(state.getContext().get("Test")).isEqualTo("Test"); + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @SuppressWarnings("unchecked") + final void givenTypeErrorInMetadataStep_stepFlow_shouldError() { + ParallelFlow testWithMetadata = ParallelFlowBuilder + .builderForMetadataType(Date.class) + .named("Test") + .parallelizeFromArray(flowContext -> (List) flowContext.get("data")) + .parallelizedFlow(StepFlowBuilder + .builderForMetadataType(StreamTokenizer.class) + .named("Step") + .execution(new TestWorkWithMetadata()) + .build() + ) + .mergeStrategy(ParallelFlowBuilder.defaultMergeStrategy()) + .build(); + + StepVerifier + .create(testWithMetadata.run(FlowContext.createFrom(Map.of("data", List.of(new Date()))))) + .assertNext(assertAndLog(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenErrorLambdaExecution_stepFlow_shouldError() { + StepFlow stepFlow = StepFlowBuilder + .defaultBuilder() + .named("Test") + .execution((flow, state, metadata) -> { + flow.addErrors(List.of(new FlowTechnicalException(flow, "Error 1"), new FlowFunctionalException(flow, "Error 2"))); + return Mono.just(state); + }) + .build(); + + StepVerifier + .create(stepFlow.run(FlowContext.create())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).hasSize(2); + assertThat(state.getAllWarnings()).isEmpty(); + }) + .verifyComplete(); + } + + @Test + final void givenWarningLambdaExecution_stepFlow_shouldWarning() { + StepFlow stepFlow = StepFlowBuilder + .defaultBuilder() + .named("Test") + .execution((flow, state, metadata) -> { + state.getContext().put("Test", "Test"); + flow.addWarnings(List.of(new FlowTechnicalException(flow, "Warning 1"), new FlowFunctionalException(flow, "Warning 2"))); + return Mono.just(state); + }) + .build(); + + StepVerifier + .create(stepFlow.run(FlowContext.create())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.WARNING); + assertThat(state.getContext().get("Test")).isEqualTo("Test"); + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).hasSize(2); + }) + .verifyComplete(); + } + + @Test + final void givenWarningAndErrorLambdaExecution_stepFlow_shouldError() { + StepFlow stepFlow = StepFlowBuilder + .defaultBuilder() + .named("Test") + .execution((flow, state, metadata) -> { + flow.addErrors(List.of(new FlowTechnicalException(flow, "Error 1"), new FlowFunctionalException(flow, "Error 2"))); + flow.addWarnings(List.of(new FlowTechnicalException(flow, "Warning 1"), new FlowFunctionalException(flow, "Warning 2"))); + return Mono.just(state); + }) + .build(); + + StepVerifier + .create(stepFlow.run(FlowContext.create())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).hasSize(2); + assertThat(state.getAllWarnings()).hasSize(2); + }) + .verifyComplete(); + } + + @Test + final void givenRawErrorLambdaExecution_stepFlow_shouldError() { + StepFlow stepFlow = StepFlowBuilder + .defaultBuilder() + .named("Test") + .execution((flow, state, metadata) -> Mono.error(new RuntimeException("Raw error"))) + .build(); + + StepVerifier + .create(stepFlow.run(FlowContext.create())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllWarnings()).isEmpty(); + assertThat(state.getAllErrors().get(0).getType()).isEqualTo(FlowExceptionType.TECHNICAL); + }) + .verifyComplete(); + } + + @Test + final void givenRawFlowErrorLambdaExecution_stepFlow_shouldError() { + StepFlow stepFlow = StepFlowBuilder + .defaultBuilder() + .named("Test") + .execution((flow, state, metadata) -> Mono.error(new FlowFunctionalException(flow, "Raw"))) + .build(); + + StepVerifier + .create(stepFlow.run(FlowContext.create())) + .assertNext(state -> { + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllWarnings()).isEmpty(); + assertThat(state.getAllErrors().get(0).getType()).isEqualTo(FlowExceptionType.FUNCTIONAL); + }) + .verifyComplete(); + } + + static final class TestWork extends StepExecution { + @Override + public Mono> apply(StepFlow flow, State state, Metadata metadata) { + state.getContext().put("Test", "Test"); + return Mono.just(state); + } + } + + static final class TestWorkWithMetadata extends StepExecution { + @Override + public Mono> apply(StepFlow flow, State state, Metadata metadata) { + System.out.println(metadata.getData().getClass().getSimpleName()); + state.getContext().put("Test", "Test"); + return Mono.just(state); + } + } +} diff --git a/src/test/java/fr/jtools/reactorflow/SwitchFlowTest.java b/src/test/java/fr/jtools/reactorflow/SwitchFlowTest.java new file mode 100644 index 0000000..f1c318a --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/SwitchFlowTest.java @@ -0,0 +1,465 @@ +package fr.jtools.reactorflow; + +import fr.jtools.reactorflow.builder.SwitchFlowBuilder; +import fr.jtools.reactorflow.exception.FlowBuilderException; +import fr.jtools.reactorflow.flow.SwitchFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.State; +import fr.jtools.reactorflow.testutils.CustomContext; +import fr.jtools.reactorflow.testutils.ErrorMonoStepFlow; +import fr.jtools.reactorflow.testutils.ErrorRawStepFlow; +import fr.jtools.reactorflow.testutils.ErrorStepFlow; +import fr.jtools.reactorflow.testutils.SuccessStepFlow; +import fr.jtools.reactorflow.testutils.WarningStepFlow; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import static fr.jtools.reactorflow.testutils.TestUtils.assertAndLog; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +final class SwitchFlowTest { + @Test + final void givenSuccessCaseOneFlow_switchFlow_shouldSuccess() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 1") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getContext().get("Case 1")).isEqualTo("Case 1"); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenClonedWithNewNameSuccessCaseOneFlow_switchFlow_shouldSuccess() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 1") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.cloneFlow("Test copy").run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test copy"); + assertThat(state.getContext().get("Case 1")).isEqualTo("Case 1"); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + + @Test + final void givenClonedSuccessCaseOneFlow_switchFlow_shouldSuccess() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 1") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.cloneFlow().run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getContext().get("Case 1")).isEqualTo("Case 1"); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenSuccessCaseOneFlowWithCustomContext_switchFlow_shouldSuccess() { + SwitchFlow switchFlow = SwitchFlowBuilder + .builderForContextOfType(CustomContext.class) + .named("Test") + .switchCondition(state -> "Case 1") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(new CustomContext())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().customField).isNotNull(); + assertThat(state.getContext().get("Case 1")).isEqualTo("Case 1"); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenFailedCaseOneFlow_switchFlow_shouldError() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 1") + .switchCase("Case 1", ErrorStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case 1")).isNull(); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenRawFailedCaseOneFlow_switchFlow_shouldError() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 1") + .switchCase("Case 1", ErrorRawStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case 1")).isNull(); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenMonoFailedCaseOneFlow_switchFlow_shouldError() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 1") + .switchCase("Case 1", ErrorMonoStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case 1")).isNull(); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenWarningCaseOneFlow_switchFlow_shouldWarning() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 1") + .switchCase("Case 1", WarningStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case 1")).isEqualTo("Case 1"); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.WARNING); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).hasSize(1); + })) + .verifyComplete(); + } + + @Test + final void givenSuccessCaseTwoFlow_switchFlow_shouldSuccess() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 2") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getContext().get("Case 2")).isEqualTo("Case 2"); + assertThat(state.getContext().get("Case 1")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenFailedCaseTwoFlow_switchFlow_shouldError() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 2") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", ErrorStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case 1")).isNull(); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenWarningCaseTwoFlow_switchFlow_shouldWarning() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 2") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", WarningStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case 2")).isEqualTo("Case 2"); + assertThat(state.getContext().get("Case 1")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.WARNING); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).hasSize(1); + })) + .verifyComplete(); + } + + @Test + final void givenSuccessDefaultCaseFlow_switchFlow_shouldSuccess() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 3") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getName()).isEqualTo("Test"); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Case 1")).isNull(); + assertThat(state.getContext().get("Default Case")).isEqualTo("Default Case"); + assertThat(state.getStatus()).isEqualTo(State.Status.SUCCESS); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenFailedDefaultCaseFlow_switchFlow_shouldError() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 3") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(ErrorStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case 1")).isNull(); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenWarningDefaultCaseFlow_switchFlow_shouldWarning() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 3") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", SuccessStepFlow.flowNamed("Case 2")) + .defaultCase(WarningStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Case 1")).isNull(); + assertThat(state.getContext().get("Default Case")).isEqualTo("Default Case"); + assertThat(state.getStatus()).isEqualTo(State.Status.WARNING); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).isEmpty(); + assertThat(state.getAllWarnings()).hasSize(1); + })) + .verifyComplete(); + } + + @Test + final void givenErrorInSwitchCondition_switchFlow_shouldWarning() { + SwitchFlow switchFlow = SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> { + throw new RuntimeException("Raw error"); + }) + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .switchCase("Case 2", WarningStepFlow.flowNamed("Case 2")) + .defaultCase(SuccessStepFlow.flowNamed("Default Case")) + .build(); + + StepVerifier + .create(switchFlow.run(FlowContext.create())) + .assertNext(assertAndLog(state -> { + assertThat(state.getContext().get("Case 1")).isNull(); + assertThat(state.getContext().get("Case 2")).isNull(); + assertThat(state.getContext().get("Default Case")).isNull(); + assertThat(state.getStatus()).isEqualTo(State.Status.ERROR); + assertThat(state.getAllRecoveredErrors()).isEmpty(); + assertThat(state.getAllErrors()).hasSize(1); + assertThat(state.getAllWarnings()).isEmpty(); + })) + .verifyComplete(); + } + + @Test + final void givenNullName_switchFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> SwitchFlowBuilder + .defaultBuilder() + .named(null) + .switchCondition(state -> "Case 1") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .defaultCase(SuccessStepFlow.flowNamed("Default False")) + .build() + ); + } + + @Test + final void givenNullDefaultCase_switchFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 1") + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .defaultCase(null) + .build() + ); + } + + @Test + final void givenNullSwitchCaseKey_switchFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 1") + .switchCase(null, SuccessStepFlow.flowNamed("Case 1")) + .defaultCase(SuccessStepFlow.flowNamed("Default False")) + .build() + ); + } + + @Test + final void givenNullSwitchCaseFlow_switchFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(state -> "Case 1") + .switchCase("Case 1", null) + .defaultCase(SuccessStepFlow.flowNamed("Default False")) + .build() + ); + } + + @Test + final void givenNullSwitchCondition_switchFlow_shouldNotBuild() { + assertThatExceptionOfType(FlowBuilderException.class).isThrownBy(() -> SwitchFlowBuilder + .defaultBuilder() + .named("Test") + .switchCondition(null) + .switchCase("Case 1", SuccessStepFlow.flowNamed("Case 1")) + .defaultCase(SuccessStepFlow.flowNamed("Default False")) + .build() + ); + } +} diff --git a/src/test/java/fr/jtools/reactorflow/testutils/CustomContext.java b/src/test/java/fr/jtools/reactorflow/testutils/CustomContext.java new file mode 100644 index 0000000..848845a --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/testutils/CustomContext.java @@ -0,0 +1,7 @@ +package fr.jtools.reactorflow.testutils; + +import fr.jtools.reactorflow.state.FlowContext; + +public final class CustomContext extends FlowContext { + public String customField = "CUSTOM"; +} diff --git a/src/test/java/fr/jtools/reactorflow/testutils/ErrorMonoStepFlow.java b/src/test/java/fr/jtools/reactorflow/testutils/ErrorMonoStepFlow.java new file mode 100644 index 0000000..3f33f42 --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/testutils/ErrorMonoStepFlow.java @@ -0,0 +1,35 @@ +package fr.jtools.reactorflow.testutils; + + +import fr.jtools.reactorflow.builder.StepFlowBuilder; +import fr.jtools.reactorflow.flow.StepExecution; +import fr.jtools.reactorflow.flow.StepFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +public final class ErrorMonoStepFlow extends StepExecution { + private final String name; + + public static StepFlow flowNamed(String name) { + return StepFlowBuilder + .defaultBuilder() + .named(name) + .execution(new ErrorMonoStepFlow<>(name)) + .build(); + } + + public static ErrorMonoStepFlow named(String name) { + return new ErrorMonoStepFlow<>(name); + } + + private ErrorMonoStepFlow(String name) { + this.name = name; + } + + @Override + public Mono> apply(StepFlow thisFlow, State state, Metadata metadata) { + return Mono.error(new RuntimeException(name + " mono error")); + } +} diff --git a/src/test/java/fr/jtools/reactorflow/testutils/ErrorRawStepFlow.java b/src/test/java/fr/jtools/reactorflow/testutils/ErrorRawStepFlow.java new file mode 100644 index 0000000..d4ed5dc --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/testutils/ErrorRawStepFlow.java @@ -0,0 +1,34 @@ +package fr.jtools.reactorflow.testutils; + +import fr.jtools.reactorflow.builder.StepFlowBuilder; +import fr.jtools.reactorflow.flow.StepExecution; +import fr.jtools.reactorflow.flow.StepFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +public final class ErrorRawStepFlow extends StepExecution { + private final String name; + + public static StepFlow flowNamed(String name) { + return StepFlowBuilder + .defaultBuilder() + .named(name) + .execution(new ErrorRawStepFlow<>(name)) + .build(); + } + + public static ErrorRawStepFlow named(String name) { + return new ErrorRawStepFlow<>(name); + } + + private ErrorRawStepFlow(String name) { + this.name = name; + } + + @Override + public Mono> apply(StepFlow thisFlow, State state, Metadata metadata) { + throw new RuntimeException(this.name + " raw error"); + } +} diff --git a/src/test/java/fr/jtools/reactorflow/testutils/ErrorStepFlow.java b/src/test/java/fr/jtools/reactorflow/testutils/ErrorStepFlow.java new file mode 100644 index 0000000..e6ae2de --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/testutils/ErrorStepFlow.java @@ -0,0 +1,36 @@ +package fr.jtools.reactorflow.testutils; + +import fr.jtools.reactorflow.builder.StepFlowBuilder; +import fr.jtools.reactorflow.exception.FlowTechnicalException; +import fr.jtools.reactorflow.flow.StepExecution; +import fr.jtools.reactorflow.flow.StepFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +public final class ErrorStepFlow extends StepExecution { + private final String name; + + public static StepFlow flowNamed(String name) { + return StepFlowBuilder + .defaultBuilder() + .named(name) + .execution(new ErrorStepFlow<>(name)) + .build(); + } + + public static ErrorStepFlow named(String name) { + return new ErrorStepFlow<>(name); + } + + private ErrorStepFlow(String name) { + this.name = name; + } + + @Override + public Mono> apply(StepFlow thisFlow, State state, Metadata metadata) { + thisFlow.addError(new FlowTechnicalException(thisFlow, this.name)); + return Mono.just(state); + } +} \ No newline at end of file diff --git a/src/test/java/fr/jtools/reactorflow/testutils/SuccessStepFlow.java b/src/test/java/fr/jtools/reactorflow/testutils/SuccessStepFlow.java new file mode 100644 index 0000000..1a166fc --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/testutils/SuccessStepFlow.java @@ -0,0 +1,35 @@ +package fr.jtools.reactorflow.testutils; + +import fr.jtools.reactorflow.builder.StepFlowBuilder; +import fr.jtools.reactorflow.flow.StepExecution; +import fr.jtools.reactorflow.flow.StepFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +public final class SuccessStepFlow extends StepExecution { + private final String name; + + public static StepFlow flowNamed(String name) { + return StepFlowBuilder + .defaultBuilder() + .named(name) + .execution(new SuccessStepFlow<>(name)) + .build(); + } + + public static SuccessStepFlow named(String name) { + return new SuccessStepFlow<>(name); + } + + private SuccessStepFlow(String name) { + this.name = name; + } + + @Override + public Mono> apply(StepFlow thisFlow, State state, Metadata metadata) { + state.getContext().put(this.name, this.name); + return Mono.just(state); + } +} diff --git a/src/test/java/fr/jtools/reactorflow/testutils/TestUtils.java b/src/test/java/fr/jtools/reactorflow/testutils/TestUtils.java new file mode 100644 index 0000000..2ce4b10 --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/testutils/TestUtils.java @@ -0,0 +1,21 @@ +package fr.jtools.reactorflow.testutils; + +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.State; + +import java.util.function.Consumer; + +public final class TestUtils { + private TestUtils() { + } + + public static Consumer> assertAndLog(Consumer> assertions) { + return state -> { + System.out.println(state.toPrettyString()); + System.out.println(state.toPrettyErrorsAndWarningsString()); + System.out.println(state.getContext().toPrettyString()); + System.out.println(state.toPrettyTreeString()); + assertions.accept(state); + }; + } +} diff --git a/src/test/java/fr/jtools/reactorflow/testutils/WarningStepFlow.java b/src/test/java/fr/jtools/reactorflow/testutils/WarningStepFlow.java new file mode 100644 index 0000000..ded775c --- /dev/null +++ b/src/test/java/fr/jtools/reactorflow/testutils/WarningStepFlow.java @@ -0,0 +1,37 @@ +package fr.jtools.reactorflow.testutils; + +import fr.jtools.reactorflow.builder.StepFlowBuilder; +import fr.jtools.reactorflow.exception.FlowTechnicalException; +import fr.jtools.reactorflow.flow.StepExecution; +import fr.jtools.reactorflow.flow.StepFlow; +import fr.jtools.reactorflow.state.FlowContext; +import fr.jtools.reactorflow.state.Metadata; +import fr.jtools.reactorflow.state.State; +import reactor.core.publisher.Mono; + +public final class WarningStepFlow extends StepExecution { + private final String name; + + public static StepFlow flowNamed(String name) { + return StepFlowBuilder + .defaultBuilder() + .named(name) + .execution(new WarningStepFlow<>(name)) + .build(); + } + + public static WarningStepFlow named(String name) { + return new WarningStepFlow<>(name); + } + + private WarningStepFlow(String name) { + this.name = name; + } + + @Override + public Mono> apply(StepFlow thisFlow, State state, Metadata metadata) { + thisFlow.addWarning(new FlowTechnicalException(thisFlow, this.name)); + state.getContext().put(this.name, this.name); + return Mono.just(state); + } +}