From bbe052ff72f112587cfbae008ab960b28b50b60e Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Tue, 9 Feb 2021 07:59:53 -0500 Subject: [PATCH] New flowz model that incorporates Behaviors, Steps, and Flowz (#87) * Step changes * Try and get Steps to respect the passed in state * Rename Step to Stage to start new effort * More renaming * Shuffling things so we can work on new design of Step and Stage * Towards recursion schemes * Embracing the use of Behaviors * Trying to expand on Behavior type * Behavior related updates * Continue building out Behavior * Continued build out of the Behavior type * Refining behavior and flow usage * Refining the definition of behavior * Refining the definition of behavior * Clear out quite a bit of the prior art * Add some docs and a comment about the changing API * working on flow execution: * Setup the structure for flow execution * Flow execution and Instrumentation * More work on instrumentation and properties * Prepping to change the definition of a Behavior * expanding behavior * Add more operators and constructors to Behavior * Support up to an arity of 8 for mapN and mapParN * Change Behavior to be Step... * Change Behavior to be Step... * Additional renames * Add the ability to instrument tests * Move experimental and not fully baked items into the experimental package --- .github/workflows/ci.yml | 2 +- .mill-version | 2 +- build.sc | 126 +-- morphir/flowz/ReadMe.md | 2 + .../morphir/flowz/spark/DatasetModule.scala | 34 +- .../flowz/spark/SparkModuleExports.scala | 2 +- .../src/morphir/flowz/spark/SparkStep.scala | 100 +- .../src/morphir/flowz/spark/package.scala | 4 +- .../sample/heroes/HeroesSampleMain.scala | 15 +- .../presidential/PresidentialSampleMain.scala | 2 +- .../morphir/flowz/NeedsInputState.scala | 23 + .../src-2.11/morphir/flowz/NeedsMsg.scala | 23 + .../morphir/flowz/NeedsInputState.scala | 24 + .../src-2.12/morphir/flowz/NeedsMsg.scala | 22 + .../morphir/flowz/NeedsInputState.scala | 24 + .../src-2.13/morphir/flowz/NeedsMsg.scala | 22 + .../src/morphir/flowz/AbstractFlow.scala | 3 - .../morphir/flowz/AbstractStatelessStep.scala | 6 + .../flowz/src/morphir/flowz/Activity.scala | 11 - .../src/morphir/flowz/ActivityExports.scala | 32 - morphir/flowz/src/morphir/flowz/Api.scala | 3 - morphir/flowz/src/morphir/flowz/Context.scala | 13 - .../src/morphir/flowz/ContextSetup.scala | 199 ---- .../flowz/src/morphir/flowz/FiberSyntax.scala | 36 +- .../src/morphir/flowz/FlowArguments.scala | 3 + morphir/flowz/src/morphir/flowz/FlowDsl.scala | 187 ---- .../flowz/src/morphir/flowz/FlowHost.scala | 65 -- .../flowz/src/morphir/flowz/FlowInfo.scala | 3 - .../flowz/src/morphir/flowz/NodePath.scala | 6 + .../flowz/src/morphir/flowz/Property.scala | 60 ++ .../flowz/src/morphir/flowz/PropertyMap.scala | 44 + morphir/flowz/src/morphir/flowz/Step.scala | 866 ++++++++++-------- .../flowz/src/morphir/flowz/StepArities.scala | 443 +++++++++ .../src/morphir/flowz/StepCompanion.scala | 292 ------ .../flowz/src/morphir/flowz/StepContext.scala | 70 -- .../flowz/src/morphir/flowz/StepExports.scala | 36 - .../flowz/src/morphir/flowz/StepFailure.scala | 8 + .../flowz/src/morphir/flowz/StepInfo.scala | 3 - .../flowz/src/morphir/flowz/StepInputs.scala | 45 - .../flowz/src/morphir/flowz/StepOutputs.scala | 49 - .../flowz/src/morphir/flowz/StepSuccess.scala | 33 + morphir/flowz/src/morphir/flowz/StepUid.scala | 8 + .../src/morphir/flowz/StepUidGenerator.scala | 9 + .../src/morphir/flowz/StepVisibility.scala | 13 - morphir/flowz/src/morphir/flowz/TODO.md | 55 -- .../flowz/src/morphir/flowz/ZBehavior.scala | 42 + .../src/morphir/flowz/ZBehaviorSyntax.scala | 20 + .../experimental/AbstractRunnableFlow.scala | 18 + .../flowz/experimental/ExecutedFlow.scala | 41 + .../src/morphir/flowz/experimental/Flow.scala | 150 +++ .../flowz/experimental/FlowBaseEnv.scala | 11 + .../flowz/experimental/FlowExecutor.scala | 23 + .../flowz/experimental/FlowFailure.scala | 20 + .../flowz/experimental/FlowRunner.scala | 51 ++ .../flowz/experimental/FlowSuccess.scala | 9 + .../morphir/flowz/experimental/Message.scala | 5 + .../flowz/experimental/instrumentor.scala | 35 + .../morphir/flowz/experimental/output.scala | 17 + .../morphir/flowz/experimental/package.scala | 8 + .../morphir/flowz/experimental/params.scala | 16 + .../morphir/flowz/experimental/state.scala | 32 + morphir/flowz/src/morphir/flowz/iLog.scala | 50 + .../InstrumentationEvent.scala | 84 ++ .../InstrumentationLogging.scala | 138 +++ .../flowz/instrumentation/package.scala | 10 + morphir/flowz/src/morphir/flowz/package.scala | 164 +++- .../src/morphir/flowz/uidGenerator.scala | 86 ++ .../src/morphir/flowz/AnnotationsSpec.scala | 26 + .../test/src/morphir/flowz/FlowHostSpec.scala | 8 - .../src/morphir/flowz/StepContextSpec.scala | 15 - .../test/src/morphir/flowz/StepSpec.scala | 128 +-- .../morphir/flowz/sample/GreetingFlow.scala | 18 +- .../src/morphir/flowz/sample/HelloWorld.scala | 11 +- .../morphir/flowz/sample/SummingFlow.scala | 35 +- .../SummingFlowWithEffectfulSetup.scala | 40 +- morphir/ir/src/morphir/ir/recursions.scala | 71 ++ .../test/src/morphir/sdk/ExampleSpec.scala | 33 + .../core/test/src/morphir/sdk/RuleSpec.scala | 2 +- 78 files changed, 2673 insertions(+), 1772 deletions(-) create mode 100644 morphir/flowz/src-2.11/morphir/flowz/NeedsInputState.scala create mode 100644 morphir/flowz/src-2.11/morphir/flowz/NeedsMsg.scala create mode 100644 morphir/flowz/src-2.12/morphir/flowz/NeedsInputState.scala create mode 100644 morphir/flowz/src-2.12/morphir/flowz/NeedsMsg.scala create mode 100644 morphir/flowz/src-2.13/morphir/flowz/NeedsInputState.scala create mode 100644 morphir/flowz/src-2.13/morphir/flowz/NeedsMsg.scala delete mode 100644 morphir/flowz/src/morphir/flowz/AbstractFlow.scala create mode 100644 morphir/flowz/src/morphir/flowz/AbstractStatelessStep.scala delete mode 100644 morphir/flowz/src/morphir/flowz/Activity.scala delete mode 100644 morphir/flowz/src/morphir/flowz/ActivityExports.scala delete mode 100644 morphir/flowz/src/morphir/flowz/Api.scala delete mode 100644 morphir/flowz/src/morphir/flowz/Context.scala delete mode 100644 morphir/flowz/src/morphir/flowz/ContextSetup.scala create mode 100644 morphir/flowz/src/morphir/flowz/FlowArguments.scala delete mode 100644 morphir/flowz/src/morphir/flowz/FlowDsl.scala delete mode 100644 morphir/flowz/src/morphir/flowz/FlowHost.scala delete mode 100644 morphir/flowz/src/morphir/flowz/FlowInfo.scala create mode 100644 morphir/flowz/src/morphir/flowz/NodePath.scala create mode 100644 morphir/flowz/src/morphir/flowz/Property.scala create mode 100644 morphir/flowz/src/morphir/flowz/PropertyMap.scala create mode 100644 morphir/flowz/src/morphir/flowz/StepArities.scala delete mode 100644 morphir/flowz/src/morphir/flowz/StepCompanion.scala delete mode 100644 morphir/flowz/src/morphir/flowz/StepContext.scala delete mode 100644 morphir/flowz/src/morphir/flowz/StepExports.scala create mode 100644 morphir/flowz/src/morphir/flowz/StepFailure.scala delete mode 100644 morphir/flowz/src/morphir/flowz/StepInfo.scala delete mode 100644 morphir/flowz/src/morphir/flowz/StepInputs.scala delete mode 100644 morphir/flowz/src/morphir/flowz/StepOutputs.scala create mode 100644 morphir/flowz/src/morphir/flowz/StepSuccess.scala create mode 100644 morphir/flowz/src/morphir/flowz/StepUid.scala create mode 100644 morphir/flowz/src/morphir/flowz/StepUidGenerator.scala delete mode 100644 morphir/flowz/src/morphir/flowz/StepVisibility.scala delete mode 100644 morphir/flowz/src/morphir/flowz/TODO.md create mode 100644 morphir/flowz/src/morphir/flowz/ZBehavior.scala create mode 100644 morphir/flowz/src/morphir/flowz/ZBehaviorSyntax.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/AbstractRunnableFlow.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/ExecutedFlow.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/Flow.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/FlowBaseEnv.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/FlowExecutor.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/FlowFailure.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/FlowRunner.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/FlowSuccess.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/Message.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/instrumentor.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/output.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/package.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/params.scala create mode 100644 morphir/flowz/src/morphir/flowz/experimental/state.scala create mode 100644 morphir/flowz/src/morphir/flowz/iLog.scala create mode 100644 morphir/flowz/src/morphir/flowz/instrumentation/InstrumentationEvent.scala create mode 100644 morphir/flowz/src/morphir/flowz/instrumentation/InstrumentationLogging.scala create mode 100644 morphir/flowz/src/morphir/flowz/instrumentation/package.scala create mode 100644 morphir/flowz/src/morphir/flowz/uidGenerator.scala create mode 100644 morphir/flowz/test/src/morphir/flowz/AnnotationsSpec.scala delete mode 100644 morphir/flowz/test/src/morphir/flowz/FlowHostSpec.scala delete mode 100644 morphir/flowz/test/src/morphir/flowz/StepContextSpec.scala create mode 100644 morphir/ir/src/morphir/ir/recursions.scala create mode 100644 morphir/sdk/core/test/src/morphir/sdk/ExampleSpec.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfbd099f..d01d619e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: - name: Checks run: | git config --global user.name "CI" - # ./mill all __.checkFormat __.docJar __.test + ./mill all __.checkFormat __.docJar __.test # ./mill all __.checkFormat "__.fix --check" __.docJar __.test - name: Status Check diff --git a/.mill-version b/.mill-version index b3ec1638..03834411 100644 --- a/.mill-version +++ b/.mill-version @@ -1 +1 @@ -0.9.3 \ No newline at end of file +0.9.5 \ No newline at end of file diff --git a/build.sc b/build.sc index 961ec64c..e7abf776 100644 --- a/build.sc +++ b/build.sc @@ -31,28 +31,29 @@ object Deps { val silencer = "1.7.1" val scalaCollectionsCompat = "2.3.1" - val zio = "1.0.4" + val zio = "1.0.4-2" val zioConfig = "1.0.0-RC32" - val zioLogging = "0.5.5" + val zioLogging = "0.5.6" + val zioMagic = "0.1.8" val zioNio = "1.0.0-RC10" val zioPrelude = "1.0.0-RC1" val zioProcess = "0.2.0" val newtype = "0.4.4" - def decline(scalaVersion:String) = scalaVersion match { + def decline(scalaVersion: String) = scalaVersion match { case version if version.startsWith("2.11") => "1.2.0" - case _ => "1.3.0" + case _ => "1.3.0" } - val pprint = "0.5.9" - val scalameta = "4.3.18" - val directories = "11" - val enumeratum = "1.6.1" - val macroParadise = "2.1.1" - val upickle = "1.1.0" - val slf4zio = "1.0.0" - val scalactic = "3.1.2" - val scalaUri = "2.2.2" - val oslib = "0.6.2" - val quill = "3.6.0-RC3" + val pprint = "0.5.9" + val scalameta = "4.3.18" + val directories = "11" + val enumeratum = "1.6.1" + val macroParadise = "2.1.1" + val upickle = "1.1.0" + val slf4zio = "1.0.0" + val scalactic = "3.1.2" + val scalaUri = "2.2.2" + val oslib = "0.6.2" + val quill = "3.6.0-RC3" } } @@ -66,13 +67,13 @@ trait MorphirScalaModule extends ScalaModule with TpolecatModule { self => trait MorphirScalafixModule extends ScalafixModule trait MorphirPublishModule extends GitVersionedPublishModule { - def packageDescription = T(artifactName()) - def pomSettings = PomSettings( + def packageDescription = T(artifactName()) + def pomSettings = PomSettings( description = packageDescription(), organization = "org.morphir", - url = "https://github.com/MorganStanley/morphir-jvm", + url = "https://github.com/finos/morphir-jvm", licenses = Seq(License.`Apache-2.0`), - versionControl = VersionControl.github("MorganStanley", "morphir-jvm"), + versionControl = VersionControl.github("finos", "morphir-jvm"), developers = Seq( Developer( "DamianReeves", @@ -136,8 +137,8 @@ trait CommonJvmModule extends MorphirCommonModule { trait CommonJsModule extends MorphirCommonModule with ScalaJSModule { def platformSegment = "js" def crossScalaJSVersion: String - def scalaJSVersion = crossScalaJSVersion - def millSourcePath = super.millSourcePath / os.up / os.up + def scalaJSVersion = crossScalaJSVersion + def millSourcePath = super.millSourcePath / os.up / os.up trait Tests extends super.Tests with MorphirTestModule { def platformSegment = "js" def scalaJSVersion = crossScalaJSVersion @@ -160,7 +161,7 @@ trait MorphirTestModule extends MorphirScalaModule with TestModule { Seq("zio.test.sbt.ZTestFramework") def offset: os.RelPath = os.rel - def sources = T.sources( + def sources = T.sources( super .sources() .++( @@ -202,7 +203,7 @@ object morphir extends Module { /*with MorphirScalafixModule*/ { self => def artifactName = "morphir-ir" - def ivyDeps = Agg( + def ivyDeps = Agg( ivy"dev.zio::zio:${Versions.zio}", ivy"dev.zio::zio-streams:${Versions.zio}", ivy"com.lihaoyi::upickle:${Versions.upickle}", @@ -276,10 +277,10 @@ object morphir extends Module { with CommonJvmModule with MorphirPublishModule { self => - def artifactName = "morphir-flowz" + def artifactName = "morphir-flowz" def scalacPluginIvyDeps = Agg(ivy"com.github.ghik:::silencer-plugin:${Versions.silencer}") def compileIvyDeps = Agg(ivy"com.github.ghik:::silencer-lib:${Versions.silencer}") - def ivyDeps = Agg( + def ivyDeps = Agg( ivy"org.scala-lang.modules::scala-collection-compat:${Versions.scalaCollectionsCompat}", ivy"com.github.mlangc:slf4zio_2.11:${Versions.slf4zio}", ivy"dev.zio::zio:${Versions.zio}", @@ -295,43 +296,48 @@ object morphir extends Module { object test extends Tests { def platformSegment: String = self.platformSegment def crossScalaVersion = JvmMorphirFlowz.this.crossScalaVersion - override def ivyDeps = super.ivyDeps() ++ Agg(ivy"com.monovore::decline:${Versions.decline(crossScalaVersion)}") - } - } - - object spark extends Module { - object jvm - extends Cross[JvmMorphirFlowzSpark]( - Versions.scala212, - Versions.scala211 - ) - class JvmMorphirFlowzSpark(val crossScalaVersion: String) - extends CrossScalaModule - with CommonJvmModule - with MorphirPublishModule { self => - - def artifactName = "morphir-flowz-spark" - def moduleDeps = Seq(morphir.flowz.jvm(crossScalaVersion)) - - def scalacPluginIvyDeps = Agg(ivy"com.github.ghik:::silencer-plugin:${Versions.silencer}") - def compileIvyDeps = Agg(ivy"com.github.ghik:::silencer-lib:${Versions.silencer}") - def ivyDeps = Agg( - ivy"org.scala-lang.modules::scala-collection-compat:${Versions.scalaCollectionsCompat}", - ivy"com.github.mlangc:slf4zio_2.11:${Versions.slf4zio}", - ivy"io.getquill::quill-spark:${Versions.quill}", - ivy"dev.zio::zio:${Versions.zio}", - ivy"dev.zio::zio-streams:${Versions.zio}", - ivy"dev.zio::zio-prelude:${Versions.zioPrelude}" + override def ivyDeps = super.ivyDeps() ++ Agg( + ivy"com.monovore::decline:${Versions.decline(crossScalaVersion)}", + ivy"io.github.kitlangton::zio-magic:${Versions.zioMagic}" ) - - object test extends Tests { - def platformSegment: String = self.platformSegment - def crossScalaVersion = JvmMorphirFlowzSpark.this.crossScalaVersion - override def ivyDeps = super.ivyDeps() ++ - Agg(ivy"dev.zio::zio-logging:${Versions.zioLogging}", - ivy"dev.zio::zio-logging-slf4j:${Versions.zioLogging}") - } } } + +// object spark extends Module { +// object jvm +// extends Cross[JvmMorphirFlowzSpark]( +// Versions.scala212, +// Versions.scala211 +// ) +// class JvmMorphirFlowzSpark(val crossScalaVersion: String) +// extends CrossScalaModule +// with CommonJvmModule +// with MorphirPublishModule { self => +// +// def artifactName = "morphir-flowz-spark" +// def moduleDeps = Seq(morphir.flowz.jvm(crossScalaVersion)) +// +// def scalacPluginIvyDeps = Agg(ivy"com.github.ghik:::silencer-plugin:${Versions.silencer}") +// def compileIvyDeps = Agg(ivy"com.github.ghik:::silencer-lib:${Versions.silencer}") +// def ivyDeps = Agg( +// ivy"org.scala-lang.modules::scala-collection-compat:${Versions.scalaCollectionsCompat}", +// ivy"com.github.mlangc:slf4zio_2.11:${Versions.slf4zio}", +// ivy"io.getquill::quill-spark:${Versions.quill}", +// ivy"dev.zio::zio:${Versions.zio}", +// ivy"dev.zio::zio-streams:${Versions.zio}", +// ivy"dev.zio::zio-prelude:${Versions.zioPrelude}" +// ) +// +// object test extends Tests { +// def platformSegment: String = self.platformSegment +// def crossScalaVersion = JvmMorphirFlowzSpark.this.crossScalaVersion +// override def ivyDeps = super.ivyDeps() ++ +// Agg( +// ivy"dev.zio::zio-logging:${Versions.zioLogging}", +// ivy"dev.zio::zio-logging-slf4j:${Versions.zioLogging}" +// ) +// } +// } +// } } } diff --git a/morphir/flowz/ReadMe.md b/morphir/flowz/ReadMe.md index b559e149..29810c00 100644 --- a/morphir/flowz/ReadMe.md +++ b/morphir/flowz/ReadMe.md @@ -1,5 +1,7 @@ # Flowz +## Under Construction - API is changing + The Flowz module in morphir, provides a library for functionally composing small units of executable content. At its core flowz is about composing individual `Step`s into a workflow. A `Step` allows you to model a potentially stateful operation in a pure functional way. diff --git a/morphir/flowz/spark/src/morphir/flowz/spark/DatasetModule.scala b/morphir/flowz/spark/src/morphir/flowz/spark/DatasetModule.scala index ecbaca1b..3023bc1a 100644 --- a/morphir/flowz/spark/src/morphir/flowz/spark/DatasetModule.scala +++ b/morphir/flowz/spark/src/morphir/flowz/spark/DatasetModule.scala @@ -9,22 +9,26 @@ trait DatasetModule { self => def filterDataset[State, DataRow, Exclude: Encoder: TypeTag, Include: Encoder: TypeTag]( func: SparkSession => (State, DataRow) => (State, FilterResult[Exclude, Include]) - ): Step[State, State, SparkModule, Dataset[DataRow], Throwable, Dataset[FilterResult[Exclude, Include]]] = - Step[State, State, SparkModule, Dataset[DataRow], Throwable, Dataset[FilterResult[Exclude, Include]]]( - ZIO.environment[StepContext[SparkModule, State, Dataset[DataRow]]].mapEffect { ctx => - val spark = ctx.environment.get.sparkSession - var outputState = ctx.inputs.state - val inputData = ctx.inputs.params - - import spark.implicits._ - val dataset = inputData.map { row => - val (nextState, filterRow) = func(spark)(outputState, row) - outputState = nextState - filterRow + ): Behavior[State, State, Dataset[DataRow], SparkModule, Throwable, Dataset[FilterResult[Exclude, Include]]] = + new Behavior[State, State, Dataset[DataRow], SparkModule, Throwable, Dataset[FilterResult[Exclude, Include]]] { + protected def behavior( + state: State, + message: Dataset[DataRow] + ): ZIO[SparkModule, Throwable, BehaviorResult[State, Dataset[FilterResult[Exclude, Include]]]] = + ZIO.environment[SparkModule].mapEffect { sparkModule => + val spark = sparkModule.get.sparkSession + var outputState = state + val inputData = message + import spark.implicits._ + val dataset = inputData.map { row => + val (nextState, filterRow) = func(spark)(outputState, row) + outputState = nextState + filterRow + } + BehaviorResult(state = outputState, value = dataset) } - StepOutputs(state = outputState, value = dataset) - } - ) + } + } trait DatasetExports { exports => } diff --git a/morphir/flowz/spark/src/morphir/flowz/spark/SparkModuleExports.scala b/morphir/flowz/spark/src/morphir/flowz/spark/SparkModuleExports.scala index 56973aab..a3cf18a5 100644 --- a/morphir/flowz/spark/src/morphir/flowz/spark/SparkModuleExports.scala +++ b/morphir/flowz/spark/src/morphir/flowz/spark/SparkModuleExports.scala @@ -11,7 +11,7 @@ trait SparkModuleExports { val SparkModule: morphir.flowz.spark.sparkModule.SparkModule.type = morphir.flowz.spark.sparkModule.SparkModule final type SparkStep[-StateIn, +StateOut, -Env, -Params, +Err, +Value] = - morphir.flowz.spark.SparkStep[StateIn, StateOut, Env, Params, Err, Value] + morphir.flowz.spark.SparkBehavior[StateIn, StateOut, Env, Params, Err, Value] val SparkStep: morphir.flowz.spark.SparkStep.type = morphir.flowz.spark.SparkStep def sparkStep[Params, A](func: SparkSession => Params => A): SparkStep[Any, A, Any, Params, Throwable, A] = diff --git a/morphir/flowz/spark/src/morphir/flowz/spark/SparkStep.scala b/morphir/flowz/spark/src/morphir/flowz/spark/SparkStep.scala index 2f8e169d..6844e1be 100644 --- a/morphir/flowz/spark/src/morphir/flowz/spark/SparkStep.scala +++ b/morphir/flowz/spark/src/morphir/flowz/spark/SparkStep.scala @@ -1,7 +1,7 @@ package morphir.flowz.spark import morphir.flowz.spark.sparkModule.SparkModule -import morphir.flowz.{ Step, StepContext, StepOutputs } +import morphir.flowz.{ Act, StepOutputs } import org.apache.spark.broadcast.Broadcast import org.apache.spark.sql._ import zio._ @@ -12,11 +12,11 @@ import scala.reflect.runtime.universe.TypeTag object SparkStep { def apply[StateIn, StateOut, Env, Params, Err, Value]( - effect: ZIO[StepContext[Env with SparkModule, StateIn, Params], Err, StepOutputs[StateOut, Value]], + effect: ZIO[StageContext[Env with SparkModule, StateIn, Params], Err, StepOutputs[StateOut, Value]], name: Option[String] = None, description: Option[String] = None - ): SparkStep[StateIn, StateOut, Env, Params, Err, Value] = - Step[StateIn, StateOut, Env with SparkModule, Params, Err, Value]( + ): SparkBehavior[StateIn, StateOut, Env, Params, Err, Value] = + Act[StateIn, StateOut, Env with SparkModule, Params, Err, Value]( rawEffect = effect, name = name, description = description @@ -24,15 +24,15 @@ object SparkStep { def apply[Env, Params, Err, Out]( func: Params => ZIO[Env with SparkModule, Err, Out] - ): Step[Any, Unit, Env with SparkModule, Params, Err, Out] = - Step.context[Env with SparkModule, Any, Params].flatMap { ctx => - Step(func(ctx.inputs.params).provide(ctx.environment).map(out => StepOutputs.fromValue(out))) + ): Act[Any, Unit, Env with SparkModule, Params, Err, Out] = + Act.context[Env with SparkModule, Any, Params].flatMap { ctx => + Act(func(ctx.inputs.params).provide(ctx.environment).map(out => StepOutputs.fromValue(out))) } def broadcast[State, Params, A: ClassTag]( func: (SparkSession, State, Params) => A - ): Step[State, State, SparkModule, Params, Throwable, Broadcast[A]] = - Step.context[SparkModule, State, Params].transformEff { case (_, ctx) => + ): Act[State, State, SparkModule, Params, Throwable, Broadcast[A]] = + Act.context[SparkModule, State, Params].transformEff { case (_, ctx) => val spark = ctx.environment.get.sparkSession val value = func(spark, ctx.inputs.state, ctx.inputs.params) val broadcast = spark.sparkContext.broadcast(value) @@ -41,10 +41,10 @@ object SparkStep { def createDataset[A <: Product: ClassTag: TypeTag]( func: SparkSession => Encoder[A] => Dataset[A] - ): SparkStep[Any, Dataset[A], Any, Any, Throwable, Dataset[A]] = + ): SparkBehavior[Any, Dataset[A], Any, Any, Throwable, Dataset[A]] = SparkStep( ZIO - .environment[StepContext.having.Environment[SparkModule]] + .environment[StageContext.having.Environment[SparkModule]] .mapEffect { ctx => val spark = ctx.environment.get.sparkSession StepOutputs.setBoth(func(spark)(spark.implicits.newProductEncoder)) @@ -53,80 +53,82 @@ object SparkStep { def createDataset[A <: Product: ClassTag: TypeTag]( data: => Seq[A] - ): SparkStep[Any, Dataset[A], Any, Any, Throwable, Dataset[A]] = + ): SparkBehavior[Any, Dataset[A], Any, Any, Throwable, Dataset[A]] = SparkStep( ZIO - .environment[StepContext.having.Environment[SparkModule]] + .environment[StageContext.having.Environment[SparkModule]] .mapEffect { ctx => val spark = ctx.environment.get.sparkSession StepOutputs.setBoth(spark.createDataset(data)(spark.implicits.newProductEncoder)) } ) - def environment[Env]: SparkStep[Any, Env, Nothing, Any, Nothing, Env with SparkModule] = + def environment[Env]: SparkBehavior[Any, Env, Nothing, Any, Nothing, Env with SparkModule] = SparkStep[Any, Env, Nothing, Any, Nothing, Env with SparkModule]( ZIO - .environment[StepContext.having.Environment[Env with SparkModule]] + .environment[StageContext.having.Environment[Env with SparkModule]] .map(ctx => StepOutputs.setBoth(ctx.environment)) ) def makeStep[Env, Params, Err, Out]( func: Params => ZIO[Env with SparkModule, Err, Out] - ): Step[Any, Unit, Env with SparkModule, Params, Err, Out] = - Step.parameters[Params].flatMap { params => - Step.fromEffect(func(params)) + ): Act[Any, Any, Env with SparkModule, Params, Err, Out] = + Act.parameters[Params].flatMap { params => + Act.fromEffect(func(params)) } def mapDataset[A, B <: Product: ClassTag: TypeTag]( func: A => B - ): Step[Any, Dataset[A], Any, Dataset[A], Throwable, Dataset[B]] = - Step.parameters[Dataset[A]].mapEffect { dataset: Dataset[A] => + ): Act[Any, Dataset[B], Any, Dataset[A], Throwable, Dataset[B]] = + Act.accessParametersM { dataset: Dataset[A] => import dataset.sparkSession.implicits._ - dataset.map(func) + Act.fromEffect(ZIO.effect(dataset.map(func))).mapOutputs { case (_, ds) => (ds, ds) } } - def parameters[Params]: SparkStep[Any, Params, Any, Params, Nothing, Params] = - SparkStep(ZIO.environment[StepContext[SparkModule, Any, Params]].map(ctx => StepOutputs.setBoth(ctx.inputs.params))) + def parameters[Params]: SparkBehavior[Any, Params, Any, Params, Nothing, Params] = + SparkStep( + ZIO.environment[StageContext[SparkModule, Any, Params]].map(ctx => StepOutputs.setBoth(ctx.inputs.params)) + ) - def showDataset[A](): Step[Any, Dataset[A], Any, Dataset[A], Throwable, Dataset[A]] = - Step.parameters[Dataset[A]].tapValue { dataset => + def showDataset[A](): Act[Any, Any, Any, Dataset[A], Throwable, Dataset[A]] = + Act.parameters[Dataset[A]].tapValue { dataset => ZIO.effect(dataset.show()) } - def showDataset[A](truncate: Boolean): Step[Any, Dataset[A], Any, Dataset[A], Throwable, Dataset[A]] = - Step.parameters[Dataset[A]].tapValue { dataset => + def showDataset[A](truncate: Boolean): Act[Any, Any, Any, Dataset[A], Throwable, Dataset[A]] = + Act.parameters[Dataset[A]].tapValue { dataset => ZIO.effect(dataset.show(truncate)) } - def showDataset[A](numRows: Int, truncate: Boolean): Step[Any, Dataset[A], Any, Dataset[A], Throwable, Dataset[A]] = - Step.parameters[Dataset[A]].tapValue { dataset => + def showDataset[A](numRows: Int, truncate: Boolean): Act[Any, Any, Any, Dataset[A], Throwable, Dataset[A]] = + Act.parameters[Dataset[A]].tapValue { dataset => ZIO.effect(dataset.show(numRows, truncate)) } - def showDataset[A](numRows: Int, truncate: Int): Step[Any, Dataset[A], Any, Dataset[A], Throwable, Dataset[A]] = - Step.parameters[Dataset[A]].tapValue { dataset => + def showDataset[A](numRows: Int, truncate: Int): Act[Any, Any, Any, Dataset[A], Throwable, Dataset[A]] = + Act.parameters[Dataset[A]].tapValue { dataset => ZIO.effect(dataset.show(numRows, truncate)) } - val sparkSession: Step[Any, SparkSession, SparkModule, Any, Throwable, SparkSession] = - Step.environment[SparkModule].transformEff { (_, sparkMod) => + val sparkSession: Act[Any, SparkSession, SparkModule, Any, Throwable, SparkSession] = + Act.environment[SparkModule].transformEff { (_, sparkMod) => val sparkSession = sparkMod.get.sparkSession (sparkSession, sparkSession) } - def sparkStep[Params, A](func: SparkSession => Params => A): SparkStep[Any, A, Any, Params, Throwable, A] = + def sparkStep[Params, A](func: SparkSession => Params => A): SparkBehavior[Any, A, Any, Params, Throwable, A] = SparkStep( ZIO - .environment[StepContext[SparkModule, Any, Params]] + .environment[StageContext[SparkModule, Any, Params]] .mapEffect(ctx => StepOutputs.setBoth(func(ctx.environment.get.sparkSession)(ctx.inputs.params))) ) def sparkStepEffect[Env, Params, Err, A]( func: SparkSession => Params => ZIO[Env with SparkModule, Err, A] - ): SparkStep[Any, A, Nothing, Params, Err, A] = + ): SparkBehavior[Any, A, Nothing, Params, Err, A] = SparkStep[Any, A, Env, Params, Err, A]( ZIO - .environment[StepContext[Env with SparkModule, Any, Params]] + .environment[StageContext[Env with SparkModule, Any, Params]] .flatMap(ctx => func(ctx.environment.get.sparkSession)(ctx.inputs.params) .flatMap(value => ZIO.succeed(StepOutputs.unified(value))) @@ -134,38 +136,38 @@ object SparkStep { ) ) - def state[State]: SparkStep[State, State, Any, Any, Nothing, State] = SparkStep( - ZIO.access[StepContext[SparkModule, State, Any]](ctx => StepOutputs.setBoth(ctx.inputs.state)) + def state[State]: SparkBehavior[State, State, Any, Any, Nothing, State] = SparkStep( + ZIO.access[StageContext[SparkModule, State, Any]](ctx => StepOutputs.setBoth(ctx.inputs.state)) ) def stateM[StateIn, StateOut, Env, Params, Err, Value]( - func: StateIn => Step[Any, StateOut, Env with SparkModule, Params, Err, Value] - ): SparkStep[StateIn, StateOut, Env, Params, Err, Value] = SparkStep[StateIn, StateOut, Env, Params, Err, Value]( - ZIO.accessM[StepContext[Env with SparkModule, StateIn, Params]](ctx => func(ctx.inputs.state).effect) + func: StateIn => Act[Any, StateOut, Env with SparkModule, Params, Err, Value] + ): SparkBehavior[StateIn, StateOut, Env, Params, Err, Value] = SparkStep[StateIn, StateOut, Env, Params, Err, Value]( + ZIO.accessM[StageContext[Env with SparkModule, StateIn, Params]](ctx => func(ctx.inputs.state).effect) ) - def toDataFrame[A]: SparkStep[Any, Unit, Any, Dataset[A], Throwable, DataFrame] = SparkStep { data: Dataset[A] => + def toDataFrame[A]: SparkBehavior[Any, Unit, Any, Dataset[A], Throwable, DataFrame] = SparkStep { data: Dataset[A] => ZIO.effect(data.toDF()) } def transformDataset[A, B <: Product: ClassTag: TypeTag, S1, S2]( func: (S1, Dataset[A]) => (S2, Dataset[B]) - ): Step[S1, S2, Any, Dataset[A], Throwable, Dataset[B]] = - Step.statefulEffect(func) + ): Act[S1, S2, Any, Dataset[A], Throwable, Dataset[B]] = + Act.statefulEffect(func) - def withSpark[A](func: SparkSession => A): SparkStep[Any, A, Any, Any, Throwable, A] = + def withSpark[A](func: SparkSession => A): SparkBehavior[Any, A, Any, Any, Throwable, A] = SparkStep( ZIO - .environment[StepContext.having.Environment[SparkModule]] + .environment[StageContext.having.Environment[SparkModule]] .mapEffect(ctx => StepOutputs.setBoth(func(ctx.environment.get.sparkSession))) ) def withSparkEffect[Env, Err, A]( func: SparkSession => ZIO[Env, Err, A] - ): SparkStep[Any, A, Env, Any, Err, A] = + ): SparkBehavior[Any, A, Env, Any, Err, A] = SparkStep[Any, A, Env, Any, Err, A]( ZIO - .environment[StepContext.having.Environment[Env with SparkModule]] + .environment[StageContext.having.Environment[Env with SparkModule]] .flatMap(ctx => func(ctx.environment.get.sparkSession).map(StepOutputs.unified(_)).provide(ctx.environment)) ) } diff --git a/morphir/flowz/spark/src/morphir/flowz/spark/package.scala b/morphir/flowz/spark/src/morphir/flowz/spark/package.scala index a1bf3658..d3c8864c 100644 --- a/morphir/flowz/spark/src/morphir/flowz/spark/package.scala +++ b/morphir/flowz/spark/src/morphir/flowz/spark/package.scala @@ -3,8 +3,8 @@ package morphir.flowz import morphir.flowz.spark.sparkModule.SparkModule package object spark { - type SparkStep[-StateIn, +StateOut, -Env, -Params, +Err, +Value] = - Step[StateIn, StateOut, Env with SparkModule, Params, Err, Value] + type SparkBehavior[-StateIn, +StateOut, -Env, -Params, +Err, +Value] = + Behavior[StateIn, StateOut, Env with SparkModule, Params, Err, Value] object api extends SparkApi } diff --git a/morphir/flowz/spark/test/src/morphir/flowz/spark/sample/heroes/HeroesSampleMain.scala b/morphir/flowz/spark/test/src/morphir/flowz/spark/sample/heroes/HeroesSampleMain.scala index 47c673d0..25841f9b 100644 --- a/morphir/flowz/spark/test/src/morphir/flowz/spark/sample/heroes/HeroesSampleMain.scala +++ b/morphir/flowz/spark/test/src/morphir/flowz/spark/sample/heroes/HeroesSampleMain.scala @@ -87,12 +87,13 @@ object HeroesSampleMain extends App { heroAbilities <- loadHeroAbilities people <- loadPeople alterEgos <- loadAlterEgos - } yield DataSources( - abilities = abilities, - heroAbilities = heroAbilities, - people = people, - alterEgos = alterEgos - )).valueAsState + data = DataSources( + abilities = abilities, + heroAbilities = heroAbilities, + people = people, + alterEgos = alterEgos.value + ) + } yield StepOutputs(data, data)) val loadDataSources = SparkStep.parameters[Options].flatMap { options: Options => @@ -197,7 +198,7 @@ object HeroesSampleMain extends App { data <- sparkModule.createDataset(data).delay(delay) _ <- console.putStrLn(s"Created/loaded Dataset of type ${tag.tag.longName}") _ <- sparkModule.withSpark(_ => data.show(false)) - } yield data + } yield StepOutputs(data, data) } final case class DataSources( diff --git a/morphir/flowz/spark/test/src/morphir/flowz/spark/sample/presidential/PresidentialSampleMain.scala b/morphir/flowz/spark/test/src/morphir/flowz/spark/sample/presidential/PresidentialSampleMain.scala index dde754f0..b1af6673 100644 --- a/morphir/flowz/spark/test/src/morphir/flowz/spark/sample/presidential/PresidentialSampleMain.scala +++ b/morphir/flowz/spark/test/src/morphir/flowz/spark/sample/presidential/PresidentialSampleMain.scala @@ -50,7 +50,7 @@ object PresidentialSampleMain extends App { val initialize = stage { (_: Any, options: Options) => Step.fromEffect(console.putStrLn(s"Options: $options")) *> - (loadExecutiveBranchInfo |+| loadLegislators).map { case (executiveData, legislatureData) => + (loadExecutiveBranchInfo |+| loadLegislators).mapValue { case (executiveData, legislatureData) => RawDataSources(executiveData = executiveData, legislatureData = legislatureData) }.mapOutputs((_, out) => (out, out)) } diff --git a/morphir/flowz/src-2.11/morphir/flowz/NeedsInputState.scala b/morphir/flowz/src-2.11/morphir/flowz/NeedsInputState.scala new file mode 100644 index 00000000..d3f8fbce --- /dev/null +++ b/morphir/flowz/src-2.11/morphir/flowz/NeedsInputState.scala @@ -0,0 +1,23 @@ +package morphir.flowz + +import scala.annotation.implicitNotFound + +/** + * A value of type `CanFail[E]` provides implicit evidence that an effect with + * error type `E` can fail, that is, that `E` is not equal to `Nothing`. + */ +@implicitNotFound( + "This operation assumes that your behavior (or effect, ste, or flow) requires an input state. " + + "However, your behavior has Any for the input state type, which means it " + + "has no requirement, so there is no need to provide the input state." +) +sealed abstract class NeedsInputState[+S] + +object NeedsInputState extends NeedsInputState[Nothing] { + + implicit def needsInputState[S]: NeedsInputState[S] = NeedsInputState + + // Provide multiple ambiguous values so an implicit NeedsInputState[Nothing] cannot be found. + implicit val needsInputStateAmbiguous1: NeedsInputState[Any] = NeedsInputState + implicit val needsInputStateAmbiguous2: NeedsInputState[Any] = NeedsInputState +} diff --git a/morphir/flowz/src-2.11/morphir/flowz/NeedsMsg.scala b/morphir/flowz/src-2.11/morphir/flowz/NeedsMsg.scala new file mode 100644 index 00000000..5db35f07 --- /dev/null +++ b/morphir/flowz/src-2.11/morphir/flowz/NeedsMsg.scala @@ -0,0 +1,23 @@ +package morphir.flowz + +import scala.annotation.implicitNotFound + +/** + * A value of type `NeedsMsg[Msg]` provides implicit evidence that an effect with + * error type `Msg` can fail, that is, that `Msg` is not equal to `Nothing`. + */ +@implicitNotFound( + "This operation assumes that your behavior, flow, step, or effect requires a message to be provided as input. " + + "However, your behavior has Any for the message type, which means it " + + "has no requirement, so there is no need to provide the message." +) +sealed abstract class NeedsMsg[+Msg] + +object NeedsMsg extends NeedsMsg[Nothing] { + + implicit def needsMsg[S]: NeedsMsg[S] = NeedsMsg + + // Provide multiple ambiguous values so an implicit NeedsNsg[Nothing] cannot be found. + implicit val needMsgAmbiguous1: NeedsMsg[Any] = NeedsMsg + implicit val needsMsgAmbiguous2: NeedsMsg[Any] = NeedsMsg +} diff --git a/morphir/flowz/src-2.12/morphir/flowz/NeedsInputState.scala b/morphir/flowz/src-2.12/morphir/flowz/NeedsInputState.scala new file mode 100644 index 00000000..8f776661 --- /dev/null +++ b/morphir/flowz/src-2.12/morphir/flowz/NeedsInputState.scala @@ -0,0 +1,24 @@ +package morphir.flowz + +import scala.annotation.implicitAmbiguous + +/** + * A value of type `NeedsInputState[R]` provides implicit evidence that a behavior, flow, step, or effect with + * input state type `S` needs a state value, that is, that `S` is not equal to + * `Any`. + */ +sealed abstract class NeedsInputState[+S] + +object NeedsInputState extends NeedsInputState[Nothing] { + + implicit def needsInputState[S]: NeedsInputState[S] = NeedsInputState + + // Provide multiple ambiguous values so an implicit NeedsInputState[Any] cannot be found. + @implicitAmbiguous( + "This operation assumes that your behavior, flow, step, or effect requires an input state. " + + "However, your behavior has Any for the input state type, which means it " + + "has no requirement, so there is no need to provide the input state." + ) + implicit val needsInputStateAmbiguous1: NeedsInputState[Any] = NeedsInputState + implicit val needsInputStateAmbiguous2: NeedsInputState[Any] = NeedsInputState +} diff --git a/morphir/flowz/src-2.12/morphir/flowz/NeedsMsg.scala b/morphir/flowz/src-2.12/morphir/flowz/NeedsMsg.scala new file mode 100644 index 00000000..add75c6f --- /dev/null +++ b/morphir/flowz/src-2.12/morphir/flowz/NeedsMsg.scala @@ -0,0 +1,22 @@ +package morphir.flowz +import scala.annotation.implicitAmbiguous + +/** + * A value of type `NeedsMsg[Msg]` provides implicit evidence that a behavior with + * message type `Msg` needs a message, that is, that `Msg` is not equal to `Any`. + */ +sealed abstract class NeedsMsg[+Msg] + +object NeedsMsg extends NeedsMsg[Nothing] { + + implicit def needsMsg[Msg]: NeedsMsg[Msg] = NeedsMsg + + // Provide multiple ambiguous values so an implicit NeedsMsg[Nothing] cannot be found. + @implicitAmbiguous( + "This operation assumes that your behavior, flow, step, or effect requires a message to be provided as input. " + + "However, your behavior has Any for the message type, which means it " + + "has no requirement, so there is no need to provide the message." + ) + implicit val needsMsgAmbiguous1: NeedsMsg[Any] = NeedsMsg + implicit val needsMsgAmbiguous2: NeedsMsg[Any] = NeedsMsg +} diff --git a/morphir/flowz/src-2.13/morphir/flowz/NeedsInputState.scala b/morphir/flowz/src-2.13/morphir/flowz/NeedsInputState.scala new file mode 100644 index 00000000..8f776661 --- /dev/null +++ b/morphir/flowz/src-2.13/morphir/flowz/NeedsInputState.scala @@ -0,0 +1,24 @@ +package morphir.flowz + +import scala.annotation.implicitAmbiguous + +/** + * A value of type `NeedsInputState[R]` provides implicit evidence that a behavior, flow, step, or effect with + * input state type `S` needs a state value, that is, that `S` is not equal to + * `Any`. + */ +sealed abstract class NeedsInputState[+S] + +object NeedsInputState extends NeedsInputState[Nothing] { + + implicit def needsInputState[S]: NeedsInputState[S] = NeedsInputState + + // Provide multiple ambiguous values so an implicit NeedsInputState[Any] cannot be found. + @implicitAmbiguous( + "This operation assumes that your behavior, flow, step, or effect requires an input state. " + + "However, your behavior has Any for the input state type, which means it " + + "has no requirement, so there is no need to provide the input state." + ) + implicit val needsInputStateAmbiguous1: NeedsInputState[Any] = NeedsInputState + implicit val needsInputStateAmbiguous2: NeedsInputState[Any] = NeedsInputState +} diff --git a/morphir/flowz/src-2.13/morphir/flowz/NeedsMsg.scala b/morphir/flowz/src-2.13/morphir/flowz/NeedsMsg.scala new file mode 100644 index 00000000..add75c6f --- /dev/null +++ b/morphir/flowz/src-2.13/morphir/flowz/NeedsMsg.scala @@ -0,0 +1,22 @@ +package morphir.flowz +import scala.annotation.implicitAmbiguous + +/** + * A value of type `NeedsMsg[Msg]` provides implicit evidence that a behavior with + * message type `Msg` needs a message, that is, that `Msg` is not equal to `Any`. + */ +sealed abstract class NeedsMsg[+Msg] + +object NeedsMsg extends NeedsMsg[Nothing] { + + implicit def needsMsg[Msg]: NeedsMsg[Msg] = NeedsMsg + + // Provide multiple ambiguous values so an implicit NeedsMsg[Nothing] cannot be found. + @implicitAmbiguous( + "This operation assumes that your behavior, flow, step, or effect requires a message to be provided as input. " + + "However, your behavior has Any for the message type, which means it " + + "has no requirement, so there is no need to provide the message." + ) + implicit val needsMsgAmbiguous1: NeedsMsg[Any] = NeedsMsg + implicit val needsMsgAmbiguous2: NeedsMsg[Any] = NeedsMsg +} diff --git a/morphir/flowz/src/morphir/flowz/AbstractFlow.scala b/morphir/flowz/src/morphir/flowz/AbstractFlow.scala deleted file mode 100644 index fa4be327..00000000 --- a/morphir/flowz/src/morphir/flowz/AbstractFlow.scala +++ /dev/null @@ -1,3 +0,0 @@ -package morphir.flowz - -trait AbstractFlow diff --git a/morphir/flowz/src/morphir/flowz/AbstractStatelessStep.scala b/morphir/flowz/src/morphir/flowz/AbstractStatelessStep.scala new file mode 100644 index 00000000..72083e70 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/AbstractStatelessStep.scala @@ -0,0 +1,6 @@ +package morphir.flowz + +/** + * Represents a behavior that does not use state. + */ +abstract class AbstractStatelessStep[-InputMsg, -R, +E, +A] extends Step[Any, Any, InputMsg, R, E, A] {} diff --git a/morphir/flowz/src/morphir/flowz/Activity.scala b/morphir/flowz/src/morphir/flowz/Activity.scala deleted file mode 100644 index e463d25d..00000000 --- a/morphir/flowz/src/morphir/flowz/Activity.scala +++ /dev/null @@ -1,11 +0,0 @@ -package morphir.flowz - -import zio._ - -object Activity extends StepCompanion[Any] { - - def apply[Params, Value](f: Params => Value): Activity[Any, Params, Throwable, Value] = - Step(ZIO.environment[StepContext.having.Parameters[Params]].mapEffect { ctx => - StepOutputs.assignBoth(f(ctx.inputs.params)) - }) -} diff --git a/morphir/flowz/src/morphir/flowz/ActivityExports.scala b/morphir/flowz/src/morphir/flowz/ActivityExports.scala deleted file mode 100644 index 6da58bf2..00000000 --- a/morphir/flowz/src/morphir/flowz/ActivityExports.scala +++ /dev/null @@ -1,32 +0,0 @@ -package morphir.flowz - -import zio._ - -trait ActivityExports { stepExports: StepExports => - - /** Defines a step that does not rely on state. */ - def act[Params, Out](f: Params => Out): Activity[Any, Params, Throwable, Out] = Activity.act(f) - - /** Defines a step that does not rely on state. */ - def act[Params, Out](name: String)(f: Params => Out): Activity[Any, Params, Throwable, Out] = - Activity.act(name)(f) - - /** Defines a step that does not rely on state. */ - def act[Params, Out](name: String, description: String)(f: Params => Out): Activity[Any, Params, Throwable, Out] = - Activity.act(name = name, description = description)(f) - - /** Defines a step that performs some effect which does not rely on state. */ - def activity[Env, Params, Err, Value](func: Params => ZIO[Env, Err, Value]): Activity[Env, Params, Err, Value] = - Activity.activity(func) - - /** Defines a step that performs some effect which does not rely on state. */ - def activity[Env, Params, Err, Value](name: String)( - func: Params => ZIO[Env, Err, Value] - ): Activity[Env, Params, Err, Value] = Activity.activity(name)(func) - - /** Defines a step that performs some effect which does not rely on state. */ - def activity[Env, Params, Err, Value](name: String, description: String)( - func: Params => ZIO[Env, Err, Value] - ): Activity[Env, Params, Err, Value] = Activity.activity(name = name, description = description)(func) - -} diff --git a/morphir/flowz/src/morphir/flowz/Api.scala b/morphir/flowz/src/morphir/flowz/Api.scala deleted file mode 100644 index 5fd07c31..00000000 --- a/morphir/flowz/src/morphir/flowz/Api.scala +++ /dev/null @@ -1,3 +0,0 @@ -package morphir.flowz - -trait Api extends StepExports with ActivityExports with FiberSyntax with FlowDsl diff --git a/morphir/flowz/src/morphir/flowz/Context.scala b/morphir/flowz/src/morphir/flowz/Context.scala deleted file mode 100644 index 7be388ed..00000000 --- a/morphir/flowz/src/morphir/flowz/Context.scala +++ /dev/null @@ -1,13 +0,0 @@ -package morphir.flowz - -sealed trait Context { - def eventBus: EventBus -} - -object Context { - final case class FlowStartupContext(eventBus: EventBus) extends Context - final case class StepInputContext[+Params](parameters: Params, eventBus: EventBus) extends Context - final case class StepOutputContext(eventBus: EventBus) extends Context -} - -sealed trait EventBus diff --git a/morphir/flowz/src/morphir/flowz/ContextSetup.scala b/morphir/flowz/src/morphir/flowz/ContextSetup.scala deleted file mode 100644 index 5f8b2dc6..00000000 --- a/morphir/flowz/src/morphir/flowz/ContextSetup.scala +++ /dev/null @@ -1,199 +0,0 @@ -package morphir.flowz - -import com.github.ghik.silencer.silent -import zio._ - -/** - * ContextSetup provides a recipe for building the context which a flow needs to execute. - */ -trait ContextSetup[-StartupEnv, -Input, +Env, +State, +Params] { self => - - def &&[StartupEnv1 <: StartupEnv, Input1 <: Input, Env0 >: Env, Env1, State1, Params1]( - other: ContextSetup[StartupEnv1, Input1, Env1, State1, Params1] - )(implicit - ev: Has.Union[Env0, Env1] - ): ContextSetup[StartupEnv1, Input1, Env0 with Env1, (State, State1), (Params, Params1)] = - ContextSetup[StartupEnv1, Input1, Env0 with Env1, (State, State1), (Params, Params1)] { input: Input1 => - self.recipe.provideSome[StartupEnv1]((_, input)).zipWith(other.recipe.provideSome[StartupEnv1]((_, input))) { - case ( - StepContext(leftEnv, StepInputs(leftState, leftParams)), - StepContext(rightEnv, StepInputs(rightState, rightParams)) - ) => - StepContext( - environment = ev.union(leftEnv, rightEnv), - state = (leftState, rightState), - params = (leftParams, rightParams) - ) - } - } - - def andUses[StartupEnv1]: ContextSetup[StartupEnv with StartupEnv1, Input, Env, State, Params] = ContextSetup { - input => - ZIO.accessM[StartupEnv with StartupEnv1](env => self.recipe.provide((env, input))) - } - - def extractParamsWith[StartupEnv1 <: StartupEnv, Input1 <: Input, Params1]( - func: Input1 => RIO[StartupEnv1, Params1] - ): ContextSetup[StartupEnv1, Input1, Env, State, Params1] = ContextSetup[StartupEnv1, Input1, Env, State, Params1] { - input => - ZIO.accessM[StartupEnv1](env => - self.recipe.flatMap(ctx => func(input).map(ctx.updateParams).provide(env)).provide((env, input)) - ) - } - - def provideSomeInput[In](adapter: In => Input): ContextSetup[StartupEnv, In, Env, State, Params] = - ContextSetup[StartupEnv, In, Env, State, Params](input => - self.recipe.provideSome[StartupEnv](env => (env, adapter(input))) - ) - - def makeContext(input: Input): RIO[StartupEnv, StepContext[Env, State, Params]] = - recipe.provideSome[StartupEnv](env => (env, input)) - - def derivesParamsWith[Input1 <: Input, Params1]( - func: Input1 => Params1 - ): ContextSetup[StartupEnv, Input1, Env, State, Params1] = - ContextSetup.CreateFromRecipe[StartupEnv, Input1, Env, State, Params1]( - ZIO.accessM[(StartupEnv, Input1)] { case (_, input) => - self.recipe.flatMap(ctx => ZIO.effect(ctx.updateParams(func(input)))) - } - ) - - /** - * The effect that can be used to build the `StepContext` - */ - def recipe: ZIO[(StartupEnv, Input), Throwable, StepContext[Env, State, Params]] - - /** - * Apply further configuration. - */ - def configure[StartupEnv1, Input1, Env1, State1, Params1]( - func: ContextSetup[StartupEnv, Input, Env, State, Params] => ContextSetup[ - StartupEnv1, - Input1, - Env1, - State1, - Params1 - ] - ): ContextSetup[StartupEnv1, Input1, Env1, State1, Params1] = func(self) - - /** - * Parse the parameters of the input to this flow from a command line - */ - def parseCommandLineWith[CmdLine <: Input, Params1](parse: CmdLine => Params1)(implicit - @silent ev: CmdLine <:< List[String] - ): ContextSetup[StartupEnv, CmdLine, Env, State, Params1] = - ContextSetup[StartupEnv, CmdLine, Env, State, Params1](cmdLine => - self.recipe - .flatMap(context => ZIO.effect(context.updateParams(parse(cmdLine)))) - .provideSome[StartupEnv](env => (env, cmdLine)) - ) -} - -object ContextSetup { - - def apply[StartupEnv, Input, Env, State, Params]( - f: Input => RIO[StartupEnv, StepContext[Env, State, Params]] - ): ContextSetup[StartupEnv, Input, Env, State, Params] = Create[StartupEnv, Input, Env, State, Params](f) - - /** - * Create a `ContextSetup` from a setup function (which is potentially side-effecting). - */ - def create[Input, Env, State, Params]( - setupFunc: Input => StepContext[Env, State, Params] - ): ContextSetup[Any, Input, Env, State, Params] = - ContextSetup { input: Input => ZIO.effect(setupFunc(input)) } - - /** - * Create an instance of the default StepContextConfig. - * This configuration only requires ZIO's ZEnv, and accepts command line args (as a list of strings). - */ - val default: ContextSetup[ZEnv, List[String], ZEnv, Any, Any] = ContextSetup { _: List[String] => - ZIO.access[ZEnv](StepContext.fromEnvironment) - } - - def deriveParams[Input, Params]( - f: Input => Params - ): ContextSetup[Any, Input, Any, Any, Params] = - ContextSetup { input => - ZIO - .accessM[(Any, Input)] { case (env, input) => - ZIO.effect(StepContext(environment = env, state = (), params = f(input))) - } - .provide(((), input)) - } - - val empty: ContextSetup[Any, Any, Any, Any, Any] = ContextSetup { _: Any => - ZIO.succeed(StepContext.any) - } - - /** - * Create a context setup from an effectual function that parses a command line. - */ - def forCommandLineApp[StartupEnv <: Has[_], Params]( - func: List[String] => RIO[StartupEnv, Params] - ): ContextSetup[StartupEnv, List[String], StartupEnv, Any, Params] = - ContextSetup[StartupEnv, List[String], StartupEnv, Any, Params]((cmdLineArgs: List[String]) => - ZIO.accessM[StartupEnv] { env => - func(cmdLineArgs).map(params => StepContext(environment = env, state = (), params = params)) - } - ) - - /** - * Creates a context setup that uses its initial startup requirements as the environment of the created context. - */ - def givenEnvironmentAtStartup[R]: ContextSetup[R, Any, R, Any, Any] = ContextSetup { _: Any => - ZIO.access[R](env => StepContext(environment = env, state = (), params = ())) - } - - def requiresEnvironmentOfType[R]: ContextSetup[R, Any, R, Any, Any] = - new ContextSetup[R, Any, R, Any, Any] { - - /** - * The effect that can be used to build the `StepContext` - */ - def recipe: ZIO[(R, Any), Throwable, StepContext[R, Any, Any]] = ZIO.access[(R, Any)] { case (env, _) => - StepContext.fromEnvironment(env) - } - } - - /** - * Creates a new context setup that requires input of the provided type - */ - def requiresInputOfType[Input]: ContextSetup[Any, Input, Any, Any, Any] = ContextSetup { _: Input => - ZIO.succeed(StepContext(environment = (), state = (), params = ())) - } - - def uses[StartupEnv]: ContextSetup[StartupEnv, Any, StartupEnv, Any, Any] = ContextSetup { _: Any => - ZIO.access[StartupEnv](StepContext.fromEnvironment) - } - - //def make[StartupEnv, Input, Env, State, Params](effect:ZIO[Input, Throwable, StepContext[Env, State, Params]]) - //def make[R, StartupEnv, Input, Env, State, Params](effect:ZIO[R, Throwable, StepContext[Env, State, Params]]) = ??? - - val withNoRequirements: ContextSetup[Any, Any, Any, Any, Any] = ContextSetup { _: Any => - ZIO.succeed(StepContext(environment = (), state = (), params = ())) - } - - final case class CreateFromRecipe[StartupEnv, Input, Env, State, Params]( - recipe0: RIO[(StartupEnv, Input), StepContext[Env, State, Params]] - ) extends ContextSetup[StartupEnv, Input, Env, State, Params] { - - /** - * The effect that can be used to build the `StepContext` - */ - def recipe: ZIO[(StartupEnv, Input), Throwable, StepContext[Env, State, Params]] = recipe0 - } - - final case class Create[StartupEnv, Input, Env, State, Params]( - f: Input => RIO[StartupEnv, StepContext[Env, State, Params]] - ) extends ContextSetup[StartupEnv, Input, Env, State, Params] { - - /** - * The effect that can be used to build the `StepContext` - */ - def recipe: ZIO[(StartupEnv, Input), Throwable, StepContext[Env, State, Params]] = - ZIO.accessM[(StartupEnv, Input)] { case (env, input) => - f(input).provide(env) - } - } -} diff --git a/morphir/flowz/src/morphir/flowz/FiberSyntax.scala b/morphir/flowz/src/morphir/flowz/FiberSyntax.scala index 9ca94314..ff2c3abb 100644 --- a/morphir/flowz/src/morphir/flowz/FiberSyntax.scala +++ b/morphir/flowz/src/morphir/flowz/FiberSyntax.scala @@ -1,18 +1,18 @@ -package morphir.flowz - -import zio.Fiber - -trait FiberSyntax { - import FiberSyntax._ - implicit def toFiberOutputChannelOps[State, Err, Output]( - fiber: Fiber[Err, StepOutputs[State, Err]] - ): FiberOutputChannelOps[State, Err, Output] = - new FiberOutputChannelOps[State, Err, Output](fiber) - -} - -object FiberSyntax { - class FiberOutputChannelOps[+State, +Err, +Output](val self: Fiber[Err, StepOutputs[State, Err]]) extends { - def joinFlow: Step[Any, State, Any, Any, Err, Err] = Step.join(self) - } -} +//package morphir.flowz +// +//import zio.Fiber +// +//trait FiberSyntax { +// import FiberSyntax._ +// implicit def toFiberOutputChannelOps[State, Err, Output]( +// fiber: Fiber[Err, StepOutputs[State, Err]] +// ): FiberOutputChannelOps[State, Err, Output] = +// new FiberOutputChannelOps[State, Err, Output](fiber) +// +//} +// +//object FiberSyntax { +// class FiberOutputChannelOps[+State, +Err, +Output](val self: Fiber[Err, StepOutputs[State, Err]]) extends { +// def joinFlow: Act[Any, State, Any, Any, Err, Err] = Act.join(self) +// } +//} diff --git a/morphir/flowz/src/morphir/flowz/FlowArguments.scala b/morphir/flowz/src/morphir/flowz/FlowArguments.scala new file mode 100644 index 00000000..2e5fd564 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/FlowArguments.scala @@ -0,0 +1,3 @@ +package morphir.flowz + +final case class FlowArguments(commandLine: List[String]) diff --git a/morphir/flowz/src/morphir/flowz/FlowDsl.scala b/morphir/flowz/src/morphir/flowz/FlowDsl.scala deleted file mode 100644 index 610dec77..00000000 --- a/morphir/flowz/src/morphir/flowz/FlowDsl.scala +++ /dev/null @@ -1,187 +0,0 @@ -package morphir.flowz - -import zio._ -import com.github.ghik.silencer.silent -trait FlowDsl { - def flow[StartupEnv, Input, Env, StateIn, Params]( - name: String, - setup: => ContextSetup[StartupEnv, Input, Env, StateIn, Params] - ): Flow.Builder[StartupEnv, Input, Env, StateIn, Params, Any, Flow.BuilderPhase.Setup] = - Flow.builder(name)(setup) - - abstract class Flow[-StartupEnv, -Input, +Output] { - type Env - type InitialState - type Params - - def name: String - def context(input: Input): RIO[StartupEnv, StepContext[Env, InitialState, Params]] - def step: Step[InitialState, _, Env, Params, Throwable, Output] - def run(input: Input): RIO[StartupEnv, Output] = - for { - stepContext <- context(input) - result <- step.run(stepContext) - } yield result.value - } - - object Flow { - type Aux[-StartupEnv, -Input, Env0, State0, Params0, +Output] = Flow[StartupEnv, Input, Output] { - type Env = Env0 - type InitialState = State0 - type Params = Params0 - } - - def builder[StartupEnv, Input, Env, StateIn, Params](name: String)( - setup: => ContextSetup[StartupEnv, Input, Env, StateIn, Params] - ): Builder[StartupEnv, Input, Env, StateIn, Params, Any, BuilderPhase.Setup] = apply(name)(setup) - - object BuilderPhase { - type Setup - type DefineStages - type Report - } - - def apply[StartupEnv, Input, Env, StateIn, Params](name: String)( - setup: => ContextSetup[StartupEnv, Input, Env, StateIn, Params] - ): Builder[StartupEnv, Input, Env, StateIn, Params, Any, BuilderPhase.Setup] = - Builder(name)(setup) - - sealed abstract class Builder[-StartupEnv, -Input, Env, State, Params, Output, Phase] { self => - - protected def name: String - protected def step: Option[Step[State, _, Env, Params, Throwable, Output]] - protected def contextSetup: ContextSetup[StartupEnv, Input, Env, State, Params] - protected def report: Option[Output => ZIO[Env, Throwable, Any]] - - final def setup[StartupEnv1 <: StartupEnv, Input1, Env1, State1, Params1]( - contextSetup: => ContextSetup[StartupEnv1, Input1, Env1, State1, Params1] - ): Builder[StartupEnv1, Input1, Env1, State1, Params1, Output, Phase with BuilderPhase.Setup] = - Builder[StartupEnv1, Input1, Env1, State1, Params1, Output, Phase with BuilderPhase.Setup]( - name = self.name, - contextSetup = contextSetup, - step = None, - report = None - ) - - final def setup[StartupEnv0 <: StartupEnv, StartupEnv1, Input1, Env1, State1, Params1]( - configure: ContextSetup[StartupEnv0, Input, Env, State, Params] => ContextSetup[ - StartupEnv1, - Input1, - Env1, - State1, - Params1 - ] - ): Builder[StartupEnv1, Input1, Env1, State1, Params1, Output, Phase with BuilderPhase.Setup] = - Builder[StartupEnv1, Input1, Env1, State1, Params1, Output, Phase with BuilderPhase.Setup]( - name = self.name, - contextSetup = contextSetup.configure(configure), - step = None, - report = None - ) - - final def stages[Output1]( - step: Step[State, _, Env, Params, Throwable, Output1] - ): Builder[StartupEnv, Input, Env, State, Params, Output1, Phase with BuilderPhase.DefineStages] = - Builder[StartupEnv, Input, Env, State, Params, Output1, Phase with BuilderPhase.DefineStages]( - name = self.name, - contextSetup = self.contextSetup, - step = Some(step), - report = None - ) - - def build(implicit - ev: Phase <:< BuilderPhase.Setup with BuilderPhase.DefineStages - ): Flow[StartupEnv, Input, Output] - - final def report( - f: Output => ZIO[Env, Throwable, Any] - )(implicit - @silent("never used") ev: Phase <:< BuilderPhase.DefineStages - ): Builder[StartupEnv, Input, Env, State, Params, Output, Phase with BuilderPhase.Report] = - Builder[StartupEnv, Input, Env, State, Params, Output, Phase with BuilderPhase.Report]( - name = self.name, - contextSetup = self.contextSetup, - step = self.step, - report = self.report.map(g => (o: Output) => f(o) *> g(o)) orElse Some(f) - ) - } - - object Builder { - def apply[StartupEnv, Input, Env, StateIn, Params]( - name: String - )( - setup: => ContextSetup[StartupEnv, Input, Env, StateIn, Params] - ): Builder[StartupEnv, Input, Env, StateIn, Params, Any, BuilderPhase.Setup] = - FlowBuilder( - name = name, - step = None, - contextSetup = setup, - report = None - ) - - private def apply[StartupEnv, Input, Env, StateIn, Params, Output, Phase]( - name: String, - step: Option[Step[StateIn, _, Env, Params, Throwable, Output]], - contextSetup: ContextSetup[StartupEnv, Input, Env, StateIn, Params], - report: Option[Output => ZIO[Env, Throwable, Any]] - ): Builder[StartupEnv, Input, Env, StateIn, Params, Output, Phase] = - FlowBuilder( - name = name, - step = step, - contextSetup = contextSetup, - report = report - ) - - private sealed case class FlowBuilder[StartupEnv, Input, Env, StateIn, Params, Output, Phase]( - name: String, - step: Option[Step[StateIn, _, Env, Params, Throwable, Output]], - contextSetup: ContextSetup[StartupEnv, Input, Env, StateIn, Params], - report: Option[Output => ZIO[Env, Throwable, Any]] - ) extends Builder[StartupEnv, Input, Env, StateIn, Params, Output, Phase] { self => - - def build(implicit - ev: Phase <:< BuilderPhase.Setup with BuilderPhase.DefineStages - ): Flow[StartupEnv, Input, Output] = { - type Env0 = Env - type Params0 = Params - new Flow[StartupEnv, Input, Output] { - type Env = Env0 - type InitialState = StateIn - type Params = Params0 - - def name: String = self.name - - def context(input: Input): RIO[StartupEnv, StepContext[Env, InitialState, Params]] = - self.contextSetup.makeContext(input) - - def step: Step[InitialState, _, Env, Params, Throwable, Output] = self.step.get.tap { case (_, output) => - self.report.fold[ZIO[Env, Throwable, Any]](ZIO.unit)(_(output)) - } - } - } - - } - - } - - } -} - -object demo extends zio.App { - import morphir.flowz.api._ - - override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = - flow( - name = "sum-flow", - setup = ContextSetup.forCommandLineApp(args => - ZIO.accessM[console.Console](_ => ZIO.collectAllSuccesses(args.map(entry => ZIO.effect(entry.toInt)))) - ) - ) - .stages( - stage((_: Any, items: List[Int]) => Step.succeed(items.sum)) - ) - .report(res => console.putStrLn(s"Result: $res")) - .build - .run(List("1", "2", "3")) - .exitCode -} diff --git a/morphir/flowz/src/morphir/flowz/FlowHost.scala b/morphir/flowz/src/morphir/flowz/FlowHost.scala deleted file mode 100644 index 7e057968..00000000 --- a/morphir/flowz/src/morphir/flowz/FlowHost.scala +++ /dev/null @@ -1,65 +0,0 @@ -package morphir.flowz - -import zio._ - -final case class FlowHost[-HostEnv, +Err, +HostParams]( - private val effect: ZIO[FlowHostContext[HostEnv], Err, HostParams] -) { self => - - def map[HostParams2](fn: HostParams => HostParams2): FlowHost[HostEnv, Err, HostParams2] = - FlowHost(self.effect.map(fn)) - - def flatMap[R1 <: HostEnv, E1 >: Err, P](fn: HostParams => FlowHost[R1, E1, P]): FlowHost[R1, E1, P] = - FlowHost(self.effect.flatMap(fn(_).effect)) - - def provideCommandLine(commandLineArgs: CommandLineArgs): FlowHost[HostEnv, Err, HostParams] = - FlowHost(effect.provideSome[(HostEnv, Any, Variables)] { case (env, _, variables) => - (env, commandLineArgs, variables) - }) - - def provideHostEnv(hostEnv: HostEnv): FlowHost[Any, Err, HostParams] = - FlowHost(self.effect.provideSome[(Any, CommandLineArgs, Variables)] { case (_, args, variables) => - (hostEnv, args, variables) - }) - - def provideVariables(variables: Variables): FlowHost[HostEnv, Err, HostParams] = - FlowHost(effect.provideSome[(HostEnv, CommandLineArgs, Any)] { case (env, args, _) => - (env, args, variables) - }) - - def getParams(args: CommandLineArgs, variables: Variables) = - self.effect.provideSome[HostEnv](env => (env, args, variables)) - - def withCommandLine(commandLineArgs: CommandLineArgs): FlowHost[HostEnv, Err, HostParams] = - provideCommandLine(commandLineArgs) - - def withHostEnv(hostEnv: HostEnv): FlowHost[Any, Err, HostParams] = - provideHostEnv(hostEnv) - - def withVariables(variables: Variables): FlowHost[HostEnv, Err, HostParams] = - provideVariables(variables) -} - -object FlowHost { - - def apply( - args: CommandLineArgs - ): UFlowHost[(CommandLineArgs, Variables)] = - FlowHost(ZIO.succeed((args, Variables(sys.env)))) - - def apply( - args: CommandLineArgs, - variables: Variables - ): UFlowHost[(CommandLineArgs, Variables)] = - FlowHost( - ZIO.environment[FlowHostContext[Any]].provide(((), args, variables)).map { case (_, args, variables) => - (args, variables) - } - ) - - def args: UFlowHost[CommandLineArgs] = - FlowHost(ZIO.environment[FlowHostContext[Any]].map(_._2)) - - def variables: UFlowHost[Variables] = - FlowHost(ZIO.environment[FlowHostContext[Any]].map(_._3)) -} diff --git a/morphir/flowz/src/morphir/flowz/FlowInfo.scala b/morphir/flowz/src/morphir/flowz/FlowInfo.scala deleted file mode 100644 index afa77cc2..00000000 --- a/morphir/flowz/src/morphir/flowz/FlowInfo.scala +++ /dev/null @@ -1,3 +0,0 @@ -package morphir.flowz - -final case class FlowInfo(name: Option[String], description: Option[String]) diff --git a/morphir/flowz/src/morphir/flowz/NodePath.scala b/morphir/flowz/src/morphir/flowz/NodePath.scala new file mode 100644 index 00000000..b4313a8f --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/NodePath.scala @@ -0,0 +1,6 @@ +package morphir.flowz + +import zio.Chunk + +final case class NodePath(segments: Chunk[String]) +object NodePath {} diff --git a/morphir/flowz/src/morphir/flowz/Property.scala b/morphir/flowz/src/morphir/flowz/Property.scala new file mode 100644 index 00000000..d1484d0c --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/Property.scala @@ -0,0 +1,60 @@ +package morphir.flowz + +import zio.duration._ +import zio.{ Chunk, Fiber, Tag } + +/** + * Represents a named data holder which represents a property on some object. + */ +final class Property[V] private ( + val identifier: String, + val initial: V, + val combine: (V, V) => V, + private val tag: Tag[V] +) extends Serializable { + + override def equals(that: Any): Boolean = that match { + case that: Property[_] => (identifier, tag) == ((that.identifier, that.tag)) + } + + override lazy val hashCode: Int = + (identifier, tag).hashCode +} + +object Property { + def apply[V](identifier: String, initial: V, combine: (V, V) => V)(implicit tag: Tag[V]): Property[V] = + new Property[V](identifier, initial, combine, tag) + + /** + * An annotation which counts ignored steps. + */ + val ignored: Property[Int] = + Property("ignored", 0, _ + _) + + /** + * An annotation which tags steps with strings. + */ + val tagged: Property[Set[String]] = + Property("tagged", Set.empty, _ union _) + + /** + * An annotation for timing. + */ + val timing: Property[Duration] = + Property("timing", Duration.Zero, _ + _) + + import zio.Ref + + import scala.collection.immutable.SortedSet + + val fibers: Property[Either[Int, Chunk[Ref[SortedSet[Fiber.Runtime[Any, Any]]]]]] = + Property("fibers", Left(0), compose) + + def compose[A](left: Either[Int, Chunk[A]], right: Either[Int, Chunk[A]]): Either[Int, Chunk[A]] = + (left, right) match { + case (Left(n), Left(m)) => Left(n + m) + case (Right(refs1), Right(refs2)) => Right(refs1 ++ refs2) + case (Right(_), Left(n)) => Left(n) + case (Left(_), Right(refs)) => Right(refs) + } +} diff --git a/morphir/flowz/src/morphir/flowz/PropertyMap.scala b/morphir/flowz/src/morphir/flowz/PropertyMap.scala new file mode 100644 index 00000000..744238b5 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/PropertyMap.scala @@ -0,0 +1,44 @@ +package morphir.flowz + +/** + * A property map keeps track of properties of different types. + * It serves as a backing store for properties and can be used to extend/addProperty objects with custom properties. + */ +final class PropertyMap private (private val map: Map[Property[Any], AnyRef]) { self => + + def ++(that: PropertyMap): PropertyMap = + new PropertyMap((self.map.toVector ++ that.map.toVector).foldLeft[Map[Property[Any], AnyRef]](Map()) { + case (acc, (key, value)) => + acc + (key -> acc.get(key).fold(value)(key.combine(_, value).asInstanceOf[AnyRef])) + }) + + /** + * Appends the specified annotation to the annotation map. + */ + def annotate[V](key: Property[V], value: V): PropertyMap = { + val res = update[V](key, key.combine(_, value)) + res + } + + /** + * Retrieves the annotation of the specified type, or its default value if there is none. + */ + def get[V](key: Property[V]): V = + map.get(key.asInstanceOf[Property[Any]]).fold(key.initial)(_.asInstanceOf[V]) + + private def overwrite[V](key: Property[V], value: V): PropertyMap = + new PropertyMap(map + (key.asInstanceOf[Property[Any]] -> value.asInstanceOf[AnyRef])) + + private def update[V](key: Property[V], f: V => V): PropertyMap = + overwrite(key, f(get(key))) + + override def toString: String = map.toString() +} + +object PropertyMap { + + /** + * An empty annotation map. + */ + val empty: PropertyMap = new PropertyMap(Map()) +} diff --git a/morphir/flowz/src/morphir/flowz/Step.scala b/morphir/flowz/src/morphir/flowz/Step.scala index 5272c414..e28d5406 100644 --- a/morphir/flowz/src/morphir/flowz/Step.scala +++ b/morphir/flowz/src/morphir/flowz/Step.scala @@ -1,475 +1,601 @@ package morphir.flowz +import morphir.flowz.instrumentation.InstrumentationEvent import zio._ -import zio.clock.Clock -import scala.util.Try +/** + * A step is a purely functional description of a computation that requires + * an environment `R`, an initial state of `SIn` and an input/update message `Msg`. + * It may fail with either an `E` or succeed with an updated state `SOut` and a result `A`. + */ +abstract class Step[-SIn, +SOut, -Msg, -R, +E, +A] { self => + import Step._ -final case class Step[-StateIn, +StateOut, -Env, -Params, +Err, +Value]( - private[flowz] val rawEffect: ZIO[StepContext[Env, StateIn, Params], Err, StepOutputs[StateOut, Value]], - name: Option[String] = None, - description: Option[String] = None -) { self => + /** + * Returns a step that executes both this step and the specified step, + * in parallel, returning the result of the provided step. If either side fails, + * then the other side will be interrupted, interrupting the result. + */ + def &>[SIn1 <: SIn, In1 <: Msg, R1 <: R, E1 >: E, SOut1, B, S, C]( + that: Step[SIn1, SOut1, In1, R1, E1, B] + ): Step[SIn1, SOut1, In1, R1, E1, B] = + self.zipWithPar(that)((_, b) => b) /** - * Connect this Step to the given Step. By connecting the output state of this Step to the input state of the other Step - * and by connecting the output value of this Step to the input of the other. + * Connect this Step to the given Step by connecting the output state of this Step to the input state + * of the other Step and by connecting the result of this Step to the input of the other. */ - def >>>[SOut2, Env1 <: Env, Err1 >: Err, Output2]( - that: Step[StateOut, SOut2, Env1, Value, Err1, Output2] - ): Step[StateIn, SOut2, Env1, Params, Err1, Output2] = + def >>>[SOut2, R1 <: R, E1 >: E, B]( + that: Step[SOut, SOut2, A, R1, E1, B] + ): Step[SIn, SOut2, Msg, R1, E1, B] = self andThen that - def *>[StateIn1 <: StateIn, Env1 <: Env, Params1 <: Params, Err1 >: Err, StateOut2, Output2]( - that: Step[StateIn1, StateOut2, Env1, Params1, Err1, Output2] - ): Step[StateIn1, StateOut2, Env1, Params1, Err1, Output2] = - Step(self.effect *> that.effect) + /** + * A variant of flatMap that ignores the values (result and state) produced by this step. + */ + def *>[SIn1 <: SIn, Msg1 <: Msg, R1 <: R, E1 >: E, SOut1, B]( + that: Step[SIn1, SOut1, Msg1, R1, E1, B] + ): Step[SIn1, SOut1, Msg1, R1, E1, B] = + Step(self.asEffect *> that.asEffect) - def <*>[StateIn1 <: StateIn, Env1 <: Env, In1 <: Params, Err1 >: Err, StateOut2, Output2]( - that: Step[StateIn1, StateOut2, Env1, In1, Err1, Output2] - ): Step[StateIn1, (StateOut, StateOut2), Env1, In1, Err1, (Value, Output2)] = self zip that + /** + * An operator alias for zip. + */ + def <*>[SIn1 <: SIn, Msg1 <: Msg, R1 <: R, E1 >: E, SOut1, B]( + that: Step[SIn1, SOut1, Msg1, R1, E1, B] + ): Step[SIn1, (SOut, SOut1), Msg1, R1, E1, (A, B)] = self zip that - def |+|[StateIn1 <: StateIn, Env1 <: Env, In1 <: Params, Err1 >: Err, StateOut2, Output2]( - that: Step[StateIn1, StateOut2, Env1, In1, Err1, Output2] - ): Step[StateIn1, (StateOut, StateOut2), Env1, In1, Err1, (Value, Output2)] = - Step((self.effect zipPar that.effect).map { case (left, right) => left zip right }) + /** + * Returns a step that executes both this step and the specified step, + * in parallel, this step's result is returned. If either side fails, + * then the other side will be interrupted, thus interrupting the result. + */ + def <&[SIn1 <: SIn, In1 <: Msg, R1 <: R, E1 >: E, SOut1, B, S, C]( + that: Step[SIn1, SOut1, In1, R1, E1, B] + ): Step[SIn1, SOut, In1, R1, E1, A] = + self.zipWithPar(that)((a, _) => a) - def <&>[StateIn1 <: StateIn, Env1 <: Env, In1 <: Params, Err1 >: Err, StateOut2, Output2]( - that: Step[StateIn1, StateOut2, Env1, In1, Err1, Output2] - ): Step[StateIn1, (StateOut, StateOut2), Env1, In1, Err1, (Value, Output2)] = - Step((self.effect zipPar that.effect).map { case (left, right) => left zip right }) + /** + * An operator alias for zip. + */ + def <&>[SIn1 <: SIn, Msg1 <: Msg, R1 <: R, E1 >: E, SOut1, B]( + that: Step[SIn1, SOut1, Msg1, R1, E1, B] + ): Step[SIn1, (SOut, SOut1), Msg1, R1, E1, (A, B)] = self zipPar that - def <*[StateIn1 <: StateIn, Env1 <: Env, Params1 <: Params, Err1 >: Err, StateOut2, Output2]( - that: Step[StateIn1, StateOut2, Env1, Params1, Err1, Output2] - ): Step[StateIn1, StateOut, Env1, Params1, Err1, Value] = - Step(self.effect <* that.effect) + /** + * An operator alias for zipPar. + */ + def |+|[SIn1 <: SIn, Msg1 <: Msg, R1 <: R, E1 >: E, SOut1, B]( + that: Step[SIn1, SOut1, Msg1, R1, E1, B] + ): Step[SIn1, (SOut, SOut1), Msg1, R1, E1, (A, B)] = self zipPar that /** - * Adapts the input provided to the Step using the provided function. + * Sequences the specified step after this step, but ignores the + * values (result and state) produced by the step. */ - def adaptParameters[Input0](func: Input0 => Params): Step[StateIn, StateOut, Env, Input0, Err, Value] = - new Step[StateIn, StateOut, Env, Input0, Err, Value](self.effect.provideSome { ctx => - ctx.copy(inputs = ctx.inputs.copy(params = func(ctx.inputs.params))) - }) + def <*[SIn1 <: SIn, In1 <: Msg, R1 <: R, E1 >: E, SOut1, B]( + that: Step[SIn1, SOut1, In1, R1, E1, B] + ): Step[SIn1, SOut, In1, R1, E1, A] = Step(self.asEffect <* that.asEffect) - def andThen[SOut2, Env1 <: Env, Err1 >: Err, Output2]( - that: Step[StateOut, SOut2, Env1, Value, Err1, Output2] - ): Step[StateIn, SOut2, Env1, Params, Err1, Output2] = - Step(ZIO.environment[StepContext[Env1, StateIn, Params]].flatMap { ctx => - self.effect.flatMap(out => that.effect.provide(ctx.updateInputs(out))) + /** + * Executes the given step upon the successful execution of this step. + */ + def andThen[SOut2, R1 <: R, E1 >: E, B]( + that: Step[SOut, SOut2, A, R1, E1, B] + ): Step[SIn, SOut2, Msg, R1, E1, B] = + Step(ZIO.accessM[(SIn, Msg, R1)] { case (_, _, r) => + self.asEffect.flatMap(success => that.asEffect.provide((success.state, success.result, r))) }) - def andThenEffect[Err1 >: Err, StateOut2, Output2]( - thatEffect: ZIO[Value, Err1, StepOutputs[StateOut2, Output2]] - ): Step[StateIn, StateOut2, Env, Params, Err1, Output2] = - Step(self.effect.map(out => out.value) andThen thatEffect) - /** - * Maps the success value of this flow to the specified constant value. + * Maps the success value of this step to a constant value. */ - def as[Out2](out: => Out2): Step[StateIn, StateOut, Env, Params, Err, Out2] = self.map(_ => out) + final def as[B](b: => B): Step[SIn, SOut, Msg, R, E, B] = map(_ => b) - def delay(duration: zio.duration.Duration): Step[StateIn, StateOut, Env with Clock, Params, Err, Value] = - Step( - for { - ctx <- ZIO.environment[StepContext[Env with Clock, StateIn, Params]] - result <- self.effect.provide(ctx).delay(duration).provide(ctx.environment) - } yield result - ) + /** + * Get this Step as an effect. + */ + protected final lazy val asEffect: ZBehavior[SIn, SOut, Msg, R, E, A] = toEffect - val effect: ZIO[StepContext[Env, StateIn, Params], Err, StepOutputs[StateOut, Value]] = rawEffect + /** + * Defines the underlying behavior of this `Step`. + */ + protected def behavior(state: SIn, message: Msg): ZIO[R, E, StepSuccess[SOut, A]] - def flatMap[S, Env1 <: Env, P <: Params, Err1 >: Err, B]( - func: Value => Step[StateOut, S, Env1, P, Err1, B] - ): Step[StateIn, S, Env1, P, Err1, B] = - Step(ZIO.environment[StepContext[Env1, StateIn, P]].flatMap { ctx => - self.effect.flatMap(out => func(out.value).effect.provide(ctx.updateState(out.state))) + /** + * Returns a step whose failure and success channels have been mapped by the specified pair of + * functions, f and g. + */ + def bimap[E2, B](f: E => E2, g: A => B)(implicit ev: CanFail[E]): Step[SIn, SOut, Msg, R, E2, B] = + Step(ZIO.accessM[(SIn, Msg, R)] { env => + self.asEffect.provide(env).bimap(f, _.map(g)) }) - def flatten[S, Env1 <: Env, P <: Params, Err1 >: Err, B](implicit - ev: Value <:< Step[StateOut, S, Env1, P, Err1, B] - ): Step[StateIn, S, Env1, P, Err1, B] = - flatMap(ev) - - def flipOutputs: Step[StateIn, Value, Env, Params, Err, StateOut] = - self.mapOutputs { case (state, value) => (value, state) } + /** + * Returns a step whose failure and success have been lifted into an + * `Either`. The resulting computation cannot fail, because the failure case + * has been exposed as part of the `Either` success case. + */ + def either[S >: SOut <: SIn](implicit ev: CanFail[E]): Step[S, S, Msg, R, Nothing, Either[E, A]] = + foldM( + failure = e => modify(s => (s, Left(e))), + success = a => modify(s => (s, Right(a))) + ) - def fork: ForkedStep[StateIn, StateOut, Env, Params, Err, Value] = - Step[StateIn, Unit, Env, Params, Nothing, Fiber.Runtime[Err, StepOutputs[StateOut, Value]]]( - self.effect.fork.map { rt => - StepOutputs(rt) + /** + * Returns a step that models the execution of this step, followed by + * the passing of its value to the specified continuation function `k`, + * followed by the step that it returns. + * + * {{{ + * val parsed = readFile("foo.txt").flatMap(file => parseFile(file)) + * }}} + */ + def flatMap[SOut1, In1 <: Msg, R1 <: R, E1 >: E, B]( + k: A => Step[SOut, SOut1, In1, R1, E1, B] + ): Step[SIn, SOut1, In1, R1, E1, B] = + Step[SIn, SOut1, In1, R1, E1, B]( + ZIO.accessM[(SIn, In1, R1)] { case (_, msg, r) => + asEffect.flatMap { res: StepSuccess[SOut, A] => + k(res.result).asEffect.provide((res.state, msg, r)) + } } ) - def map[Out2](fn: Value => Out2): Step[StateIn, StateOut, Env, Params, Err, Out2] = Step( - self.effect.map(success => success.map(fn)) - ) + /** + * Folds over the failed or successful results of this step to yield + * a step that does not fail, but succeeds with the value of the left + * or right function passed to `fold`. + */ + def fold[S >: SOut <: SIn, B](failure: E => B, success: A => B)(implicit + ev: CanFail[E] + ): Step[S, S, Msg, R, Nothing, B] = + self.foldM(e => modify(s => (s, failure(e): B)), a => modify(s => (s, success(a)))) - def mapEffect[Out2](fn: Value => Out2)(implicit - ev: Err <:< Throwable - ): Step[StateIn, StateOut, Env, Params, Throwable, Out2] = - Step(self.effect.mapEffect(success => success.map(fn))) + /** + * A more powerful version of `foldM` that allows recovering from any kind of failure except interruptions. + */ + def foldCauseM[SIn0 <: SIn, SOut1, Msg1 <: Msg, R1 <: R, E1, B]( + failure: Cause[E] => Step[SIn0, SOut1, Msg1, R1, E1, B], + success: A => Step[SOut, SOut1, Msg1, R1, E1, B] + )(implicit ev: CanFail[E]): Step[SIn0, SOut1, Msg1, R1, E1, B] = { + val _ = ev + fromFunctionM { case (initialState, msg, r1) => + self.asEffect + .foldCauseM( + failure = { cause => failure(cause).toEffect }, + success = { a => success(a.result).toEffect.provide((a.state, msg, r1)) } + ) + .provide((initialState, msg, r1)) + } + } - def mapError[Err2](onError: Err => Err2): Step[StateIn, StateOut, Env, Params, Err2, Value] = - Step(self.effect.mapError(onError)) + /** + * Recovers from errors by accepting one step to execute for the case of an + * error, and one step to execute for the case of success. + */ + def foldM[SIn0 <: SIn, SOut1, Msg1 <: Msg, R1 <: R, E1, B]( + failure: E => Step[SIn0, SOut1, Msg1, R1, E1, B], + success: A => Step[SOut, SOut1, Msg1, R1, E1, B] + )(implicit ev: CanFail[E]): Step[SIn0, SOut1, Msg1, R1, E1, B] = + fromFunctionM { case (initialState, msg, r1) => + self.asEffect + .foldM( + failure = { cause => failure(cause).toEffect }, + success = { a => success(a.result).toEffect.provide((a.state, msg, r1)) } + ) + .provide((initialState, msg, r1)) + } - def mapOutputs[StateOut2, Output2]( - func: (StateOut, Value) => (StateOut2, Output2) - ): Step[StateIn, StateOut2, Env, Params, Err, Output2] = - Step(self.effect.map(out => StepOutputs.fromTuple(func(out.state, out.value)))) + /** + * Exposes the output state into the value channel. + */ + def getState: Step[SIn, SOut, Msg, R, E, (SOut, A)] = + flatMap(a => get.map(s => (s, a))) - def mapOutputChannels[StateOut2, Output2]( - func: StepOutputs[StateOut, Value] => StepOutputs[StateOut2, Output2] - ): Step[StateIn, StateOut2, Env, Params, Err, Output2] = - Step(self.effect.map(func)) + def label: Option[String] = None - def mapState[SOut2](fn: StateOut => SOut2): Step[StateIn, SOut2, Env, Params, Err, Value] = Step( - self.effect.map(success => success.mapState(fn)) + /** + * Returns a step whose success is mapped by the specified function f. + */ + final def map[B](f: A => B): Step[SIn, SOut, Msg, R, E, B] = fromEffect(asEffect.map(_.map(f))) + + /** + * Returns a step with its error channel mapped using the specified + * function. This can be used to lift a "smaller" error into a "larger" + * error. + */ + final def mapError[E2](f: E => E2)(implicit ev: CanFail[E]): Step[SIn, SOut, Msg, R, E2, A] = + Step(self.asEffect.mapError(f)) + + /** + * Returns a step with its full cause of failure mapped using the + * specified function. This can be used to transform errors while + * preserving the original structure of `Cause`. + */ + final def mapErrorCause[E2](f: Cause[E] => Cause[E2])(implicit ev: CanFail[E]): Step[SIn, SOut, Msg, R, E2, A] = { + val _ = ev + Step(self.asEffect.mapErrorCause(f)) + } + + final def mapResults[SOut2, B]( + f: StepSuccess[SOut, A] => StepSuccess[SOut2, B] + ): Step[SIn, SOut2, Msg, R, E, B] = + fromEffect(asEffect.map(res => res.transform(f))) + + final def mapState[SOut2](f: SOut => SOut2): Step[SIn, SOut2, Msg, R, E, A] = fromEffect( + asEffect.map(_.mapState(f)) ) /** - * Executes this step and returns its value, if it succeeds, but otherwise executes the specified step. + * Provide the step all its required inputs and its environment */ - def orElse[StateIn1 <: StateIn, Env1 <: Env, Params1 <: Params, Err2, StateOut2 >: StateOut, Value2 >: Value]( - that: => Step[StateIn1, StateOut2, Env1, Params1, Err2, Value2] - )(implicit ev: CanFail[Err]): Step[StateIn1, StateOut2, Env1, Params1, Err2, Value2] = - Step(self.effect orElse that.effect) + final def provide(initialState: SIn, message: Msg, environment: R): IndieStep[SOut, E, A] = + Step(asEffect.provide((initialState, message, environment))) /** - * Returns a step that will produce the value of this step, unless it - * fails, in which case, it will produce the value of the specified step. + * Provide the step all its required inputs and its environment */ - def orElseEither[StateIn1 <: StateIn, Env1 <: Env, Params1 <: Params, Err2, ThatState >: StateOut, ThatValue]( - that: => Step[StateIn1, ThatState, Env1, Params1, Err2, ThatValue] - )(implicit - ev: CanFail[Err] - ): Step[StateIn1, Either[StateOut, ThatState], Env1, Params1, Err2, Either[Value, ThatValue]] = - new Step((self.effect orElseEither that.effect).map { - case Left(outputs) => StepOutputs(state = Left(outputs.state), value = Left(outputs.value)) - case Right(outputs) => StepOutputs(state = Right(outputs.state), value = Right(outputs.value)) - }) + final def provide(message: Msg)(implicit ev1: Any <:< SIn, ev2: Any <:< R): IndieStep[SOut, E, A] = + Step(asEffect.provide(((), message, ()))) /** - * Executes this step and returns its value, if it succeeds, but otherwise fails with the specified error. + * Provide the step all its required inputs and its environment */ - def orElseFail[State >: StateOut, Err1](error: Err1)(implicit - ev: CanFail[Err] - ): Step[StateIn, StateOut, Env, Params, Err1, Value] = - orElse(Step.fail(error)) + final def provide(initialState: SIn, message: Msg)(implicit ev1: Any <:< R): IndieStep[SOut, E, A] = + Step(asEffect.provide((initialState, message, ()))) /** - * Executes this step and returns its value, if it succeeds, but otherwise succeeds with the specified state and value. + * Provide the step with its environment. */ - def orElseSucceed[State >: StateOut, Value1 >: Value](state: => State, value: => Value1)(implicit - ev: CanFail[Err] - ): Step[StateIn, State, Env, Params, Nothing, Value1] = - orElse(Step.succeed(state = state, value = value)) + final def provideEnvironment(environment: R): Step[SIn, SOut, Msg, Any, E, A] = + Step(ZIO.accessM[(SIn, Msg, Any)] { case (initialState, message, _) => + asEffect.provide((initialState, message, environment)) + }) - def named(name: String): Step[StateIn, StateOut, Env, Params, Err, Value] = copy(name = Option(name)) - def describe(description: String): Step[StateIn, StateOut, Env, Params, Err, Value] = - copy(description = Option(description)) + /** + * Provide the step with its initial state and message. + */ + final def provideInputs(initialState: SIn, message: Msg): Step[Any, SOut, Any, R, E, A] = + Step(ZIO.accessM[(Any, Any, R)] { case (_, _, r) => asEffect.provide((initialState, message, r)) }) /** - * Repeats the step the specified number of times. + * Provide the step with its update message. */ - def repeatN(n: Int): Step[StateIn, StateOut, Env, Params, Err, Value] = - Step(effect.repeatN(n)) + final def provideMessage(message: Msg): Step[SIn, SOut, Any, R, E, A] = + Step(ZIO.accessM[(SIn, Any, R)] { case (initialState, _, r) => asEffect.provide((initialState, message, r)) }) - def repeatUntil(f: StepOutputs[StateOut, Value] => Boolean): Step[StateIn, StateOut, Env, Params, Err, Value] = - Step(effect.repeatUntil(f)) + /** + * Provide the step with its initial state + */ + final def provideState(initialState: SIn): Step[Any, SOut, Msg, R, E, A] = + Step(asEffect.provideState(initialState)) - def repeatUntil(statePredicate: StateOut => Boolean)( - valuePredicate: Value => Boolean - ): Step[StateIn, StateOut, Env, Params, Err, Value] = - Step(effect.repeatUntil(outputs => statePredicate(outputs.state) && valuePredicate(outputs.value))) + /** + * Runs the step. + */ + final def run(implicit ev1: Any <:< SIn, ev2: Any <:< Msg): ZIO[R with StepRuntimeEnv, E, StepSuccess[SOut, A]] = + run((), ()) - def repeatWhile(f: StepOutputs[StateOut, Value] => Boolean): Step[StateIn, StateOut, Env, Params, Err, Value] = - Step(effect.repeatWhile(f)) + /** + * Runs the step. + */ + final def run(state: SIn, message: Msg): ZIO[R with StepRuntimeEnv, E, StepSuccess[SOut, A]] = + for { + uid <- StepUid.nextUid + labelResolved <- ZIO.succeed(label getOrElse "N/A") + _ <- iLog.trace( + InstrumentationEvent.runningStep(s"Running Step[Label=$labelResolved; Uid=$uid;]", uid, labelResolved) + ) + result <- behavior(state, message) + } yield result - def repeatWhileState(f: StateOut => Boolean): Step[StateIn, StateOut, Env, Params, Err, Value] = - Step(effect.repeatWhile(outputs => f(outputs.state))) + /** + * Runs the step. + */ + final def run(message: Msg)(implicit ev: Any <:< SIn): ZIO[R with StepRuntimeEnv, E, StepSuccess[SOut, A]] = + run((), message) - def repeatWhileValue(f: Value => Boolean): Step[StateIn, StateOut, Env, Params, Err, Value] = - Step(effect.repeatWhile(outputs => f(outputs.value))) + /** + * Runs the step. + */ + final def runResult(implicit ev1: Any <:< SIn, ev2: Any <:< Msg): ZIO[R with StepRuntimeEnv, E, A] = + run((), ()).map(_.result) - def retryN(n: Int)(implicit ev: CanFail[Err]): Step[StateIn, StateOut, Env, Params, Err, Value] = - Step(effect.retryN(n)) + /** + * Returns a step that effectfully "peeks" at the success of this behavior. + * + * {{{ + * readFile("data.json").tap(putStrLn) + * }}} + */ + def tap[R1 <: R, E1 >: E]( + func: StepSuccess[SOut, A] => ZIO[R1, E1, Any] + ): Step[SIn, SOut, Msg, R1, E1, A] = + Step[SIn, SOut, Msg, R1, E1, A]( + ZIO.accessM[(SIn, Msg, R1)] { case (_, _, r1) => + self.asEffect.tap(success => func(success).provide(r1)) + } + ) - def retryWhile(f: StepOutputs[StateOut, Value] => Boolean): Step[StateIn, StateOut, Env, Params, Err, Value] = - Step(effect.repeatWhile(f)) + def toEffect: ZIO[(SIn, Msg, R), E, StepSuccess[SOut, A]] = ZIO.accessM[(SIn, Msg, R)] { case (stateIn, msg, r) => + behavior(stateIn, msg).provide(r) + } + + def withAnnotation[R1 <: R with Properties]: Step[SIn, SOut, Msg, R1, Annotated[E], Annotated[A]] = + accessStep[R1] { r => + Step.fromEffect( + r.get.withAnnotation(self.asEffect).map { case (success, map) => + success.map(a => (a, map)) + } + ) + } - def run(implicit - evAnyInput: Any <:< Params, - evAnyState: Any <:< StateIn - ): ZIO[Env, Err, StepOutputs[StateOut, Value]] = - self.effect.provideSome[Env](env => StepContext(environment = env, params = (), state = ())) + def withLabel(label: String): Step[SIn, SOut, Msg, R, E, A] = + StepWithMetadata(label = Option(label), underlyingStep = self) + + def zip[SIn1 <: SIn, In1 <: Msg, R1 <: R, E1 >: E, SOut1, B]( + that: Step[SIn1, SOut1, In1, R1, E1, B] + ): Step[SIn1, (SOut, SOut1), In1, R1, E1, (A, B)] = + Step((self.asEffect zip that.asEffect) map { case (left, right) => left zip right }) + + def zipPar[SIn1 <: SIn, In1 <: Msg, R1 <: R, E1 >: E, SOut1, B]( + that: Step[SIn1, SOut1, In1, R1, E1, B] + ): Step[SIn1, (SOut, SOut1), In1, R1, E1, (A, B)] = + Step((self.asEffect zipPar that.asEffect) map { case (left, right) => left zip right }) + + def zipWith[SIn1 <: SIn, In1 <: Msg, R1 <: R, E1 >: E, SOut1, B, S, C]( + that: Step[SIn1, SOut1, In1, R1, E1, B] + )( + f: (StepSuccess[SOut, A], StepSuccess[SOut1, B]) => StepSuccess[S, C] + ): Step[SIn1, S, In1, R1, E1, C] = + Step((self.asEffect zipWith that.asEffect)(f)) + + def zipWithPar[SIn1 <: SIn, In1 <: Msg, R1 <: R, E1 >: E, SOut1, B, S, C]( + that: Step[SIn1, SOut1, In1, R1, E1, B] + )( + f: (StepSuccess[SOut, A], StepSuccess[SOut1, B]) => StepSuccess[S, C] + ): Step[SIn1, S, In1, R1, E1, C] = + Step((self.asEffect zipWithPar that.asEffect)(f)) +} +object Step extends StepArities with ZBehaviorSyntax { + + def accessStep[R]: AccessStepPartiallyApplied[R] = new AccessStepPartiallyApplied[R] + def accessService[R]: AccessServicePartially[R] = new AccessServicePartially[R] + def accessServiceM[R]: AccessServiceMPartially[R] = new AccessServiceMPartially[R] + + def apply[InitialState, StateOut, In, R, E, A]( + effect: ZBehavior[InitialState, StateOut, In, R, E, A] + ): Step[InitialState, StateOut, In, R, E, A] = + fromEffect[InitialState, StateOut, In, R, E, A](effect) + + implicit def behaviorFromFunctionM[SIn, SOut, In, R, Err, A]( + f: (SIn, In) => ZIO[R, Err, StepSuccess[SOut, A]] + ): Step[SIn, SOut, In, R, Err, A] = + fromEffect[SIn, SOut, In, R, Err, A](ZIO.accessM[(SIn, In, R)] { case (state, msg, r) => + f(state, msg).provide(r) + }) - def run(input: Params)(implicit evAnyState: Unit <:< StateIn): ZIO[Env, Err, StepOutputs[StateOut, Value]] = - self.effect.provideSome[Env](env => StepContext(environment = env, params = input, state = ())) + def environment[R]: StatelessStep[Any, R, Nothing, R] = Stateless[Any, R, Nothing, R]( + ZIO.access[(Any, R)] { case (_, r) => r } + ) - def run(input: Params, initialState: StateIn): ZIO[Env, Err, StepOutputs[StateOut, Value]] = - self.effect.provideSome[Env](env => StepContext(environment = env, params = input, state = initialState)) + /** + * Constructs a behavior that always fails with the given error. + */ + def fail[E](error: E): IndieStep[Nothing, E, Nothing] = Fail(error) + +// def fromEffect[SIn, SOut, In, R, E, A]( +// effect: ZIO[(SIn, In, R), E, StepSuccess[SOut, A]] +// )(evState: NeedsInputState[SIn], evMsg: NeedsMsg[In]): Step[SIn, SOut, In, R, E, A] = { +// val _ = (evState, evMsg) //NOTE: Suppresses the warning about these not being used +// new Step[SIn, SOut, In, R, E, A] { +// protected def behavior(state: SIn, message: In): ZIO[R, E, StepSuccess[SOut, A]] = +// effect.provideSome[R](r => (state, message, r)) +// } +// } + + def fromEffect[SIn, SOut, Msg, R, E, A]( + effect: ZIO[(SIn, Msg, R), E, StepSuccess[SOut, A]] + ): Step[SIn, SOut, Msg, R, E, A] = + FromEffect(effect) - def run(context: StepContext[Env, StateIn, Params]): IO[Err, StepOutputs[StateOut, Value]] = - self.effect.provide(context) + /** + * Create a behavior by providing a possibly impure function. + * + * For example: + * + * {{{ + * val intConverter = Step.fromFunction { numberStr:String => + * numberStr.toInt + * } + * }}} + */ + def fromFunction[In, Out](f: In => Out): FuncStep[In, Out] = + MessageHandler(ZIO.accessM[In](input => ZIO.effect(f(input)))) - def shiftStateToOutput: Step[StateIn, Unit, Env, Params, Err, (StateOut, Value)] = - Step(effect.map(success => StepOutputs(state = (), value = (success.state, success.value)))) + /** + * Lifts an effectful function whose effect requires no environment into + * a behavior that requires the input to the function. + */ + def fromFunctionM[SIn, SOut, In, R, Err, A]( + f: (SIn, In, R) => IO[Err, StepSuccess[SOut, A]] + ): Step[SIn, SOut, In, R, Err, A] = + fromEffect[SIn, SOut, In, R, Err, A](ZIO.accessM[(SIn, In, R)] { case (state, msg, r) => + f(state, msg, r) + }) /** - * Maps the output state value of this step to the specified constant value. + * Creates a behavior from a typical `ZIO` effect which does not have input state and message components. */ - def stateAs[StateOut2](stateOut: => StateOut2): Step[StateIn, StateOut2, Env, Params, Err, Value] = - self.mapState(_ => stateOut) + def fromZIO[R, E, A](effect: ZIO[R, E, A]): ZIOStep[R, E, A] = FromZIO[R, E, A](effect) /** - * Takes the output state and makes it also available as the result value of this flow. + * Constructs a Step that gets the initial state unchanged. */ - def stateAsValue: Step[StateIn, StateOut, Env, Params, Err, StateOut] = - self.mapOutputs((state, _) => (state, state)) + def get[S]: Step[S, S, Any, Any, Nothing, S] = + modify(s => (s, s)) - def tap[Env1 <: Env, Err1 >: Err]( - func: (StateOut, Value) => ZIO[Env1, Err1, Any] - ): Step[StateIn, StateOut, Env1, Params, Err1, Value] = - Step( - ZIO - .environment[StepContext[Env1, StateIn, Params]] - .flatMap(ctx => self.effect.tap(out => func(out.state, out.value).provide(ctx.environment))) - ) + def getMessage[In]: Step[Any, Any, In, Any, Nothing, In] = + MessageHandler(ZIO.access[In](identity)) - def tapState[Env1 <: Env, Err1 >: Err]( - func: StateOut => ZIO[Env1, Err1, Any] - ): Step[StateIn, StateOut, Env1, Params, Err1, Value] = - Step( - ZIO - .environment[StepContext[Env1, StateIn, Params]] - .flatMap(ctx => self.effect.tap(out => func(out.state).provide(ctx.environment))) - ) + /** + * Constructs a Step from a modify function. + */ + def modify[S1, S2, A](f: S1 => (S2, A)): Step[S1, S2, Any, Any, Nothing, A] = + Modify(f) - def tapValue[Env1 <: Env, Err1 >: Err]( - func: Value => ZIO[Env1, Err1, Any] - ): Step[StateIn, StateOut, Env1, Params, Err1, Value] = - Step( - ZIO - .environment[StepContext[Env1, StateIn, Params]] - .flatMap(ctx => self.effect.tap(out => func(out.value).provide(ctx.environment))) - ) + def outputting[S, Value](state: S, value: Value): IndieStep[S, Nothing, Value] = + SetOutputs(newState = state, value = value) - def transformEff[StateOut2, Output2]( - func: (StateOut, Value) => (StateOut2, Output2) - )(implicit ev: Err <:< Throwable): Step[StateIn, StateOut2, Env, Params, Throwable, Output2] = - Step(self.effect.mapEffect(out => StepOutputs.fromTuple(func(out.state, out.value)))) + /** + * Accesses the specified service in the environment of the behavior. + */ + def service[R: Tag]: ZIOStep[Has[R], Nothing, R] = + fromZIO(ZIO.service[R]) /** - * Make the state and the output value the same by making the state equal to the output. + * Constructs a Step that sets the state to the specified value.Ã¥ */ - def valueAsState: Step[StateIn, Value, Env, Params, Err, Value] = - self.mapOutputs { case (_, value) => - (value, value) + def set[S](state: S): Step[Any, S, Any, Any, Nothing, Unit] = modify(_ => (state, ())) + + /** + * Constructs a stateless behavior from a function that produces an effect. + */ + def stateless[In, R, E, A](f: In => ZIO[R, E, A]): StatelessStep[In, R, E, A] = + new AbstractStatelessStep[In, R, E, A] { + protected def behavior(state: Any, message: In): ZIO[R, E, StepSuccess[Any, A]] = f(message).map(state -> _) } - def zip[StateIn1 <: StateIn, Env1 <: Env, In1 <: Params, Err1 >: Err, StateOut2, Output2]( - that: Step[StateIn1, StateOut2, Env1, In1, Err1, Output2] - ): Step[StateIn1, (StateOut, StateOut2), Env1, In1, Err1, (Value, Output2)] = - Step((self.effect zip that.effect).map { case (left, right) => left zip right }) - - def zipPar[StateIn1 <: StateIn, Env1 <: Env, In1 <: Params, Err1 >: Err, StateOut2, Output2]( - that: Step[StateIn1, StateOut2, Env1, In1, Err1, Output2] - ): Step[StateIn1, (StateOut, StateOut2), Env1, In1, Err1, (Value, Output2)] = - Step((self.effect zipPar that.effect).map { case (left, right) => left zip right }) - - def zipWith[ - StateIn1 <: StateIn, - Env1 <: Env, - In1 <: Params, - Err1 >: Err, - StateOut2, - Output2, - FinalState, - FinalOutput - ](that: Step[StateIn1, StateOut2, Env1, In1, Err1, Output2])( - f: ( - StepOutputs[StateOut, Value], - StepOutputs[StateOut2, Output2] - ) => StepOutputs[FinalState, FinalOutput] - ): Step[StateIn1, FinalState, Env1, In1, Err1, FinalOutput] = - Step((self.effect zipWith that.effect)(f)) - - def zipWithPar[ - StateIn1 <: StateIn, - Env1 <: Env, - In1 <: Params, - Err1 >: Err, - StateOut2, - Output2, - FinalState, - FinalOutput - ](that: Step[StateIn1, StateOut2, Env1, In1, Err1, Output2])( - f: (StepOutputs[StateOut, Value], StepOutputs[StateOut2, Output2]) => StepOutputs[FinalState, FinalOutput] - ): Step[StateIn1, FinalState, Env1, In1, Err1, FinalOutput] = - Step((self.effect zipWithPar that.effect)(f)) -} + /** + * Constructs a behavior that always succeeds with the given value. + */ + def succeed[A](value: => A): Step[Any, Any, Any, Any, Nothing, A] = Succeed(value) -object Step extends StepCompanion[Any] { + val unit: Step[Any, Any, Any, Any, Nothing, Unit] = + succeed(()) - def apply[StateIn, StateOut, Env, Params, Err, Value]( - func: (StateIn, Params) => ZIO[Env, Err, StepOutputs[StateOut, Value]] - ): Step[StateIn, StateOut, Env, Params, Err, Value] = - Step( - ZIO - .environment[StepContext[Env, StateIn, Params]] - .flatMap(ctx => func(ctx.inputs.state, ctx.inputs.params).provide(ctx.environment)) - ) + /** + * Constructs a behavior from a function that produces a new state and a result given an initial state and a message. + * This function is expected to be pure without side effects and should not throw any exceptions. + */ + def update[S1, S2, In, A](f: (S1, In) => (S2, A)): Step[S1, S2, In, Any, Nothing, A] = + Stateful(ZIO.access[(S1, In, Any)] { case (s1, msg, _) => + val (s2, a) = f(s1, msg) + StepSuccess(s2, a) + }) - def apply[StateIn, StateOut, Env, Params, Err, Value](name: String)( - func: (StateIn, Params) => ZIO[Env, Err, StepOutputs[StateOut, Value]] - ): Step[StateIn, StateOut, Env, Params, Err, Value] = - Step( - ZIO - .environment[StepContext[Env, StateIn, Params]] - .flatMap(ctx => func(ctx.inputs.state, ctx.inputs.params).provide(ctx.environment)), - name = Option(name) - ) + final case class Fail[E](error: E) extends IndieStep[Nothing, E, Nothing] { + protected def behavior(state: Any, message: Any): ZIO[Any, E, StepSuccess[Nothing, Nothing]] = ZIO.fail(error) + } - def apply[StateIn, StateOut, Env, Params, Err, Value](name: String, description: String)( - func: (StateIn, Params) => ZIO[Env, Err, StepOutputs[StateOut, Value]] - ): Step[StateIn, StateOut, Env, Params, Err, Value] = - Step( - ZIO - .environment[StepContext[Env, StateIn, Params]] - .flatMap(ctx => func(ctx.inputs.state, ctx.inputs.params).provide(ctx.environment)), - name = Option(name), - description = Option(description) - ) + final case class FromEffect[SIn, SOut, In, Env, E, A]( + zio: ZIO[(SIn, In, Env), E, StepSuccess[SOut, A]] + ) extends Step[SIn, SOut, In, Env, E, A] { + override lazy val toEffect: ZIO[(SIn, In, Env), E, StepSuccess[SOut, A]] = zio + protected def behavior(state: SIn, message: In): ZIO[Env, E, StepSuccess[SOut, A]] = + zio.provideSome[Env](env => (state, message, env)) + } - def fromEither[Err, Value](value: Either[Err, Value]): Step[Any, Unit, Any, Any, Err, Value] = - Step(for { - _ <- ZIO.environment[StepContext.having.AnyInputs] - value <- ZIO.fromEither(value) - } yield StepOutputs.fromValue(value)) + final case class FromZIO[-R, +E, +A](zio: ZIO[R, E, A]) extends Step[Any, Any, Any, R, E, A] { + protected def behavior(state: Any, message: Any): ZIO[R, E, StepSuccess[Any, A]] = + zio.map(a => StepSuccess(state = state, result = a)) + } - def fromFunction[In, Out](func: In => Out): Step[Any, Out, Any, In, Nothing, Out] = - Step(ZIO.environment[StepContext.having.Parameters[In]].map { ctx => - val value = func(ctx.inputs.params) - StepOutputs(value = value, state = value) - }) + final case class MessageHandler[-In, +Err, +A](private val zio: ZIO[In, Err, A]) + extends AbstractStatelessStep[In, Any, Err, A] { - def fromOption[Value](value: => Option[Value]): Step[Any, Unit, Any, Any, Option[Nothing], Value] = - Step(for { - _ <- ZIO.environment[StepContext.having.AnyInputs] - value <- ZIO.fromOption(value) - } yield StepOutputs.fromValue(value)) - - def fromOutputs[State, Output](channels: StepOutputs[State, Output]): Step[Any, State, Any, Any, Nothing, Output] = - Step(ZIO.succeed(channels)) - - def fromTry[Value](value: => Try[Value]): Step[Any, Unit, Any, Any, Throwable, Value] = - Step(for { - _ <- ZIO.environment[StepContext.having.AnyInputs] - value <- ZIO.fromTry(value) - } yield StepOutputs.fromValue(value)) - - def inputs[StateIn, Params]: Step[StateIn, (StateIn, Params), Any, Params, Nothing, (StateIn, Params)] = - Step( - ZIO - .environment[StepContext[Any, StateIn, Params]] - .map(ctx => - StepOutputs(state = (ctx.inputs.state, ctx.inputs.params), value = (ctx.inputs.state, ctx.inputs.params)) - ) - ) + protected def behavior(state: Any, message: In): ZIO[Any, Err, StepSuccess[Any, A]] = + zio.map(result => StepSuccess(state = state, result = result)).provide(message) - def join[State, Err, Output](fiber: Fiber[Err, StepOutputs[State, Output]]): Step[Any, State, Any, Any, Err, Output] = - Step(fiber.join) + } - def stage[StateIn, StateOut, Env, Params, Err, Out]( - func: (StateIn, Params) => Step[StateIn, StateOut, Env, Params, Err, Out] - ): Step[StateIn, StateOut, Env, Params, Err, Out] = - Step.context[Env, StateIn, Params].flatMap(ctx => func(ctx.inputs.state, ctx.inputs.params)) + final case class Modify[-S1, +S2, +A](func: S1 => (S2, A)) extends Step[S1, S2, Any, Any, Nothing, A] { - def state[State]: Step[State, State, Any, Any, Nothing, State] = Step( - ZIO.environment[StepContext[Any, State, Any]].map(ctx => StepOutputs.setBoth(ctx.inputs.state)) - ) + protected def behavior(state: S1, message: Any): ZIO[Any, Nothing, StepSuccess[S2, A]] = + ZIO.effectTotal(func(state)) - def stateful[StateIn, Params, StateOut, Out]( - func: (StateIn, Params) => (StateOut, Out) - ): Step[StateIn, StateOut, Any, Params, Nothing, Out] = - Step(ZIO.environment[StepContext.having.AnyEnv[StateIn, Params]].map { ctx => - val (state, value) = func(ctx.inputs.state, ctx.inputs.params) - StepOutputs(state = state, value = value) - }) + } - def statefulEffect[StateIn, Params, StateOut, Out]( - func: (StateIn, Params) => (StateOut, Out) - ): Step[StateIn, StateOut, Any, Params, Throwable, Out] = - Step(ZIO.environment[StepContext.having.AnyEnv[StateIn, Params]].mapEffect { ctx => - val (state, value) = func(ctx.inputs.state, ctx.inputs.params) - StepOutputs(state = state, value = value) - }) + final case class SetOutputs[S, A](newState: S, value: A) extends IndieStep[S, Nothing, A] { + protected def behavior(state: Any, message: Any): ZIO[Any, Nothing, StepSuccess[S, A]] = + ZIO.succeed((newState, value)) + } - /** - * Create a `Step` by providing a function that takes in some state and parameters and returns a tuple of - * the output state and the result. - */ - def step[StateIn, StateOut, Params, Out]( - func: (StateIn, Params) => (StateOut, Out) - ): Step[StateIn, StateOut, Any, Params, Throwable, Out] = - Step(ZIO.environment[StepContext[Any, StateIn, Params]].mapEffect { ctx => - val (state, value) = func(ctx.inputs.state, ctx.inputs.params) - StepOutputs(state = state, value = value) - }) + final case class Succeed[+A](value: A) extends Step[Any, Any, Any, Any, Nothing, A] { + protected def behavior(state: Any, message: Any): ZIO[Any, Nothing, StepSuccess[Any, A]] = + ZIO.succeed(StepSuccess(state, value)) + } /** - * Returns a step with the empty value. + * Represents a stateful behavior that is constructed from an effect. */ - val none: Step[Any, Option[Nothing], Any, Any, Nothing, Option[Nothing]] = - Step(ZIO.environment[StepContext.having.AnyInputs].as(StepOutputs.none)) + final case class Stateful[S, StateOut, In, R, E, A]( + private val effect: ZBehavior[S, StateOut, In, R, E, A] + ) extends Step[S, StateOut, In, R, E, A] { + protected def behavior(state: S, message: In): ZIO[R, E, StepSuccess[StateOut, A]] = + effect.provideSome[R](r => (state, message, r)) + } /** - * A step that succeeds with a unit value. + * Represents a stateless behavior that is constructed from an effect. */ - val unit: Step[Any, Unit, Any, Any, Nothing, Unit] = - Step(ZIO.environment[StepContext.having.AnyInputs].as(StepOutputs.unit)) - - def withStateAs[State](state: => State): Step[Any, State, Any, Any, Nothing, Unit] = - Step(ZIO.succeed(StepOutputs.fromState(state))) + final case class Stateless[In, R, E, A](private val effect: ZIO[(In, R), E, A]) + extends AbstractStatelessStep[In, R, E, A] { + protected def behavior(state: Any, message: In): ZIO[R, E, StepSuccess[Any, A]] = + effect.map(value => StepSuccess(state, value)).provideSome[R](r => (message, r)) + } + + final class AccessServicePartially[R](private val dummy: Boolean = true) extends AnyVal { + def apply[A](f: R => A)(implicit tag: Tag[R]): ZIOStep[Has[R], Nothing, A] = + Step.fromZIO(ZIO.service[R].flatMap(r => ZIO.effectTotal(f(r)))) + } + + final class AccessServiceMPartially[R](private val dummy: Boolean = true) extends AnyVal { + def apply[E, A](f: R => IO[E, A])(implicit tag: Tag[R]): ZIOStep[Has[R], E, A] = + Step.fromZIO(ZIO.service[R].flatMap(r => f(r))) + } + + final class AccessStepPartiallyApplied[R](private val dummy: Boolean = true) extends AnyVal { + def apply[SIn, SOut, Msg, E, A](f: R => Step[SIn, SOut, Msg, R, E, A]): Step[SIn, SOut, Msg, R, E, A] = + Step(ZIO.accessM[(SIn, Msg, R)] { case (_, _, r) => + f(r).toEffect + }) + } + + final class FromEffectFn[-SIn, +SOut, -Msg, -R, +E, +A] extends ((SIn, Msg) => ZIO[R, E, (SOut, A)]) { + def apply(initialState: SIn, message: Msg): ZIO[R, E, (SOut, A)] = ??? + } + + final case class StepWithMetadata[-SIn, +SOut, -Msg, -R, +E, +A]( + override val label: Option[String], + private val underlyingStep: Step[SIn, SOut, Msg, R, E, A] + ) extends Step[SIn, SOut, Msg, R, E, A] { + + /** + * Defines the underlying behavior of this `Step`. + */ + protected def behavior(state: SIn, message: Msg): ZIO[R, E, StepSuccess[SOut, A]] = + underlyingStep.behavior(state, message) + } +} - def withValue[Value](value: => Value): Step[Any, Unit, Any, Any, Nothing, Value] = - Step(ZIO.succeed(StepOutputs.fromValue(value))) +object stepExample extends App { + import zio.console.Console + import zio.logging.LogLevel + import morphir.flowz.instrumentation.InstrumentationLogging - def withStateAndValue[A](valueAndSate: => A): Step[Any, A, Any, Any, Nothing, A] = - Step(ZIO.succeed(valueAndSate).map(StepOutputs.setBoth(_))) + def defineStep[SIn, SOut, Msg, R, E, A](label: String)( + theStep: Step[SIn, SOut, Msg, R, E, A] + ): Step[SIn, SOut, Msg, R with StepRuntimeEnv, E, A] = + theStep.withLabel(label) + //TODO: This is where you could do something like add an Aspect to the step - def withEnvironment[Env, Err, State, Value]( - func: Env => ZIO[Env, Err, (State, Value)] - ): Step[Any, State, Env, Any, Err, Value] = - Step( - ZIO - .environment[StepContext[Env, Any, Any]] - .flatMap(ctx => - func(ctx.environment).map { case (state, value) => - StepOutputs(state = state, value = value) - }.provide(ctx.environment) - ) - ) + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { + val step1 = defineStep("Say Hi")(Step.accessServiceM[Console.Service](_.putStrLn("Hi"))) - def withEnvironment[Env, Err, State, Value]( - effect: ZIO[Env, Err, (State, Value)] - ): Step[Any, State, Env, Any, Err, Value] = - Step( - ZIO - .environment[StepContext[Env, Any, Any]] - .flatMap(ctx => - effect.map { case (state, value) => - StepOutputs(state = state, value = value) - }.provide(ctx.environment) - ) + step1.run.exitCode.provideCustomLayer( + StepUidGenerator.live ++ InstrumentationLogging.console(logLevel = LogLevel.Trace) ) - - def withOutputs[A](valueAndSate: A): Step[Any, A, Any, Any, Nothing, A] = - succeed(state = valueAndSate, value = valueAndSate) - - def withParams[Env, Params, Err, Out](func: Params => ZIO[Env, Err, Out]): Step[Any, Unit, Env, Params, Err, Out] = - Step.parameters[Params].flatMap { params => - Step.fromEffect(func(params)) - } + } } diff --git a/morphir/flowz/src/morphir/flowz/StepArities.scala b/morphir/flowz/src/morphir/flowz/StepArities.scala new file mode 100644 index 00000000..7d1d58f7 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/StepArities.scala @@ -0,0 +1,443 @@ +package morphir.flowz + +import zio.logging.LogLevel + +trait StepArities { + + final def mapN[SIn, SA, SB, Msg, R, E, A, B, SOut, Result, MA, MB]( + behaviorA: Step[SIn, SA, Msg, R, E, A], + behaviorB: Step[SIn, SB, Msg, R, E, B] + )( + f: (StepSuccess[SA, A], StepSuccess[SB, B]) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, E, Result] = + behaviorA.zipWith(behaviorB)(f) + + final def mapParN[SIn, SA, SB, Msg, R, E, A, B, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, E, A], + behaviorB: Step[SIn, SB, Msg, R, E, B] + )( + f: (StepSuccess[SA, A], StepSuccess[SB, B]) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, E, Result] = + behaviorA.zipWithPar(behaviorB)(f) + + final def mapN[SIn, SA, SB, SC, Msg, R, E, A, B, C, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, E, A], + behaviorB: Step[SIn, SB, Msg, R, E, B], + behaviorC: Step[SIn, SC, Msg, R, E, C] + )( + f: (StepSuccess[SA, A], StepSuccess[SB, B], StepSuccess[SC, C]) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, E, Result] = + (behaviorA <*> behaviorB <*> behaviorC).mapResults { case StepSuccess(((sa, sb), sc), ((a, b), c)) => + f(StepSuccess(sa, a), StepSuccess(sb, b), StepSuccess(sc, c)) + } + + final def mapParN[SIn, SA, SB, SC, Msg, R, E, A, B, C, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, E, A], + behaviorB: Step[SIn, SB, Msg, R, E, B], + behaviorC: Step[SIn, SC, Msg, R, E, C] + )( + f: (StepSuccess[SA, A], StepSuccess[SB, B], StepSuccess[SC, C]) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, E, Result] = + (behaviorA <&> behaviorB <&> behaviorC).mapResults { case StepSuccess(((sa, sb), sc), ((a, b), c)) => + f(StepSuccess(sa, a), StepSuccess(sb, b), StepSuccess(sc, c)) + } + + final def mapN[SIn, SA, SB, SC, SD, Msg, R, E, A, B, C, D, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, E, A], + behaviorB: Step[SIn, SB, Msg, R, E, B], + behaviorC: Step[SIn, SC, Msg, R, E, C], + behaviorD: Step[SIn, SD, Msg, R, E, D] + )( + f: ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D] + ) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, E, Result] = + (behaviorA <*> behaviorB <*> behaviorC <*> behaviorD).mapResults { + case StepSuccess((((sa, sb), sc), sd), (((a, b), c), d)) => + f(StepSuccess(sa, a), StepSuccess(sb, b), StepSuccess(sc, c), StepSuccess(sd, d)) + } + + final def mapParN[SIn, SA, SB, SC, SD, Msg, R, E, A, B, C, D, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, E, A], + behaviorB: Step[SIn, SB, Msg, R, E, B], + behaviorC: Step[SIn, SC, Msg, R, E, C], + behaviorD: Step[SIn, SD, Msg, R, E, D] + )( + f: ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D] + ) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, E, Result] = + (behaviorA <&> behaviorB <&> behaviorC <&> behaviorD).mapResults { + case StepSuccess((((sa, sb), sc), sd), (((a, b), c), d)) => + f(StepSuccess(sa, a), StepSuccess(sb, b), StepSuccess(sc, c), StepSuccess(sd, d)) + } + + final def mapN[SIn, SA, SB, SC, SD, SE, Msg, R, Err, A, B, C, D, E, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, Err, A], + behaviorB: Step[SIn, SB, Msg, R, Err, B], + behaviorC: Step[SIn, SC, Msg, R, Err, C], + behaviorD: Step[SIn, SD, Msg, R, Err, D], + behaviorE: Step[SIn, SE, Msg, R, Err, E] + )( + f: ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E] + ) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, Err, Result] = + (behaviorA <*> behaviorB <*> behaviorC <*> behaviorD <*> behaviorE).mapResults { + case StepSuccess(((((sa, sb), sc), sd), se), ((((a, b), c), d), e)) => + f( + StepSuccess(sa, a), + StepSuccess(sb, b), + StepSuccess(sc, c), + StepSuccess(sd, d), + StepSuccess(se, e) + ) + } + + final def mapParN[SIn, SA, SB, SC, SD, SE, Msg, R, Err, A, B, C, D, E, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, Err, A], + behaviorB: Step[SIn, SB, Msg, R, Err, B], + behaviorC: Step[SIn, SC, Msg, R, Err, C], + behaviorD: Step[SIn, SD, Msg, R, Err, D], + behaviorE: Step[SIn, SE, Msg, R, Err, E] + )( + f: ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E] + ) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, Err, Result] = + (behaviorA <&> behaviorB <&> behaviorC <&> behaviorD <&> behaviorE).mapResults { + case StepSuccess(((((sa, sb), sc), sd), se), ((((a, b), c), d), e)) => + f( + StepSuccess(sa, a), + StepSuccess(sb, b), + StepSuccess(sc, c), + StepSuccess(sd, d), + StepSuccess(se, e) + ) + } + + final def mapN[SIn, SA, SB, SC, SD, SE, SF, Msg, R, Err, A, B, C, D, E, F, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, Err, A], + behaviorB: Step[SIn, SB, Msg, R, Err, B], + behaviorC: Step[SIn, SC, Msg, R, Err, C], + behaviorD: Step[SIn, SD, Msg, R, Err, D], + behaviorE: Step[SIn, SE, Msg, R, Err, E], + behaviorF: Step[SIn, SF, Msg, R, Err, F] + )( + fn: ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E], + StepSuccess[SF, F] + ) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, Err, Result] = + (behaviorA <*> behaviorB <*> behaviorC <*> behaviorD <*> behaviorE <*> behaviorF).mapResults { + case StepSuccess((((((sa, sb), sc), sd), se), sf), (((((a, b), c), d), e), f)) => + fn( + StepSuccess(sa, a), + StepSuccess(sb, b), + StepSuccess(sc, c), + StepSuccess(sd, d), + StepSuccess(se, e), + StepSuccess(sf, f) + ) + } + + final def mapParN[SIn, SA, SB, SC, SD, SE, SF, Msg, R, Err, A, B, C, D, E, F, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, Err, A], + behaviorB: Step[SIn, SB, Msg, R, Err, B], + behaviorC: Step[SIn, SC, Msg, R, Err, C], + behaviorD: Step[SIn, SD, Msg, R, Err, D], + behaviorE: Step[SIn, SE, Msg, R, Err, E], + behaviorF: Step[SIn, SF, Msg, R, Err, F] + )( + fn: ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E], + StepSuccess[SF, F] + ) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, Err, Result] = + (behaviorA <&> behaviorB <&> behaviorC <&> behaviorD <&> behaviorE <&> behaviorF).mapResults { + case StepSuccess((((((sa, sb), sc), sd), se), sf), (((((a, b), c), d), e), f)) => + fn( + StepSuccess(sa, a), + StepSuccess(sb, b), + StepSuccess(sc, c), + StepSuccess(sd, d), + StepSuccess(se, e), + StepSuccess(sf, f) + ) + } + + final def mapN[SIn, SA, SB, SC, SD, SE, SF, SG, Msg, R, Err, A, B, C, D, E, F, G, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, Err, A], + behaviorB: Step[SIn, SB, Msg, R, Err, B], + behaviorC: Step[SIn, SC, Msg, R, Err, C], + behaviorD: Step[SIn, SD, Msg, R, Err, D], + behaviorE: Step[SIn, SE, Msg, R, Err, E], + behaviorF: Step[SIn, SF, Msg, R, Err, F], + behaviorG: Step[SIn, SG, Msg, R, Err, G] + )( + fn: ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E], + StepSuccess[SF, F], + StepSuccess[SG, G] + ) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, Err, Result] = + (behaviorA <*> behaviorB <*> behaviorC <*> behaviorD <*> behaviorE <*> behaviorF <*> behaviorG).mapResults { + case StepSuccess(((((((sa, sb), sc), sd), se), sf), sg), ((((((a, b), c), d), e), f), g)) => + fn( + StepSuccess(sa, a), + StepSuccess(sb, b), + StepSuccess(sc, c), + StepSuccess(sd, d), + StepSuccess(se, e), + StepSuccess(sf, f), + StepSuccess(sg, g) + ) + } + + final def mapParN[SIn, SA, SB, SC, SD, SE, SF, SG, Msg, R, Err, A, B, C, D, E, F, G, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, Err, A], + behaviorB: Step[SIn, SB, Msg, R, Err, B], + behaviorC: Step[SIn, SC, Msg, R, Err, C], + behaviorD: Step[SIn, SD, Msg, R, Err, D], + behaviorE: Step[SIn, SE, Msg, R, Err, E], + behaviorF: Step[SIn, SF, Msg, R, Err, F], + behaviorG: Step[SIn, SG, Msg, R, Err, G] + )( + fn: ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E], + StepSuccess[SF, F], + StepSuccess[SG, G] + ) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, Err, Result] = + (behaviorA <&> behaviorB <&> behaviorC <&> behaviorD <&> behaviorE <&> behaviorF <&> behaviorG).mapResults { + case StepSuccess(((((((sa, sb), sc), sd), se), sf), sg), ((((((a, b), c), d), e), f), g)) => + fn( + StepSuccess(sa, a), + StepSuccess(sb, b), + StepSuccess(sc, c), + StepSuccess(sd, d), + StepSuccess(se, e), + StepSuccess(sf, f), + StepSuccess(sg, g) + ) + } + + final def mapN[SIn, SA, SB, SC, SD, SE, SF, SG, SH, Msg, R, Err, A, B, C, D, E, F, G, H, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, Err, A], + behaviorB: Step[SIn, SB, Msg, R, Err, B], + behaviorC: Step[SIn, SC, Msg, R, Err, C], + behaviorD: Step[SIn, SD, Msg, R, Err, D], + behaviorE: Step[SIn, SE, Msg, R, Err, E], + behaviorF: Step[SIn, SF, Msg, R, Err, F], + behaviorG: Step[SIn, SG, Msg, R, Err, G], + behaviorH: Step[SIn, SH, Msg, R, Err, H] + )( + fn: ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E], + StepSuccess[SF, F], + StepSuccess[SG, G], + StepSuccess[SH, H] + ) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, Err, Result] = + (behaviorA <*> behaviorB <*> behaviorC <*> behaviorD <*> behaviorE <*> behaviorF <*> behaviorG <*> behaviorH).mapResults { + case StepSuccess((((((((sa, sb), sc), sd), se), sf), sg), sh), (((((((a, b), c), d), e), f), g), h)) => + fn( + StepSuccess(sa, a), + StepSuccess(sb, b), + StepSuccess(sc, c), + StepSuccess(sd, d), + StepSuccess(se, e), + StepSuccess(sf, f), + StepSuccess(sg, g), + StepSuccess(sh, h) + ) + } + + final def mapParN[SIn, SA, SB, SC, SD, SE, SF, SG, SH, Msg, R, Err, A, B, C, D, E, F, G, H, SOut, Result]( + behaviorA: Step[SIn, SA, Msg, R, Err, A], + behaviorB: Step[SIn, SB, Msg, R, Err, B], + behaviorC: Step[SIn, SC, Msg, R, Err, C], + behaviorD: Step[SIn, SD, Msg, R, Err, D], + behaviorE: Step[SIn, SE, Msg, R, Err, E], + behaviorF: Step[SIn, SF, Msg, R, Err, F], + behaviorG: Step[SIn, SG, Msg, R, Err, G], + behaviorH: Step[SIn, SH, Msg, R, Err, H] + )( + fn: ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E], + StepSuccess[SF, F], + StepSuccess[SG, G], + StepSuccess[SH, H] + ) => StepSuccess[SOut, Result] + ): Step[SIn, SOut, Msg, R, Err, Result] = + (behaviorA <&> behaviorB <&> behaviorC <&> behaviorD <&> behaviorE <&> behaviorF <&> behaviorG <&> behaviorH).mapResults { + case StepSuccess((((((((sa, sb), sc), sd), se), sf), sg), sh), (((((((a, b), c), d), e), f), g), h)) => + fn( + StepSuccess(sa, a), + StepSuccess(sb, b), + StepSuccess(sc, c), + StepSuccess(sd, d), + StepSuccess(se, e), + StepSuccess(sf, f), + StepSuccess(sg, g), + StepSuccess(sh, h) + ) + } + + final implicit def toSuccessMergeFunc[SA, A, SB, B, SOut, Result]( + fn: ((SA, A), (SB, B)) => (SOut, Result) + ): (StepSuccess[SA, A], StepSuccess[SB, B]) => StepSuccess[SOut, Result] = { case (successA, successB) => + fn(successA.toTuple, successB.toTuple) + } + + final implicit def toSuccessMergeFunc[SA, A, SB, B, SC, C, SOut, Result]( + fn: ((SA, A), (SB, B), (SC, C)) => (SOut, Result) + ): (StepSuccess[SA, A], StepSuccess[SB, B], StepSuccess[SC, C]) => StepSuccess[SOut, Result] = { + case (successA, successB, successC) => + fn(successA.toTuple, successB.toTuple, successC.toTuple) + } + + final implicit def toSuccessMergeFunc[SA, A, SB, B, SC, C, SD, D, SOut, Result]( + fn: ((SA, A), (SB, B), (SC, C), (SD, D)) => (SOut, Result) + ): ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D] + ) => StepSuccess[SOut, Result] = { case (successA, successB, successC, successD) => + fn(successA.toTuple, successB.toTuple, successC.toTuple, successD.toTuple) + } + + final implicit def toSuccessMergeFunc[SA, A, SB, B, SC, C, SD, D, SE, E, SOut, Result]( + fn: ((SA, A), (SB, B), (SC, C), (SD, D), (SE, E)) => (SOut, Result) + ): ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E] + ) => StepSuccess[SOut, Result] = { case (successA, successB, successC, successD, successE) => + fn(successA.toTuple, successB.toTuple, successC.toTuple, successD.toTuple, successE.toTuple) + } + + final implicit def toSuccessMergeFunc[SA, A, SB, B, SC, C, SD, D, SE, E, SF, F, SOut, Result]( + fn: ((SA, A), (SB, B), (SC, C), (SD, D), (SE, E), (SF, F)) => (SOut, Result) + ): ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E], + StepSuccess[SF, F] + ) => StepSuccess[SOut, Result] = { case (successA, successB, successC, successD, successE, successF) => + fn(successA.toTuple, successB.toTuple, successC.toTuple, successD.toTuple, successE.toTuple, successF.toTuple) + } + + final implicit def toSuccessMergeFunc[SA, A, SB, B, SC, C, SD, D, SE, E, SF, F, SG, G, SOut, Result]( + fn: ((SA, A), (SB, B), (SC, C), (SD, D), (SE, E), (SF, F), (SG, G)) => (SOut, Result) + ): ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E], + StepSuccess[SF, F], + StepSuccess[SG, G] + ) => StepSuccess[SOut, Result] = { case (successA, successB, successC, successD, successE, successF, successG) => + fn( + successA.toTuple, + successB.toTuple, + successC.toTuple, + successD.toTuple, + successE.toTuple, + successF.toTuple, + successG.toTuple + ) + } + + final implicit def toSuccessMergeFunc[SA, A, SB, B, SC, C, SD, D, SE, E, SF, F, SG, G, SH, H, SOut, Result]( + fn: ((SA, A), (SB, B), (SC, C), (SD, D), (SE, E), (SF, F), (SG, G), (SH, H)) => (SOut, Result) + ): ( + StepSuccess[SA, A], + StepSuccess[SB, B], + StepSuccess[SC, C], + StepSuccess[SD, D], + StepSuccess[SE, E], + StepSuccess[SF, F], + StepSuccess[SG, G], + StepSuccess[SH, H] + ) => StepSuccess[SOut, Result] = { + case (successA, successB, successC, successD, successE, successF, successG, successH) => + fn( + successA.toTuple, + successB.toTuple, + successC.toTuple, + successD.toTuple, + successE.toTuple, + successF.toTuple, + successG.toTuple, + successH.toTuple + ) + } + +} + +object mapNExamples extends zio.App { + import zio._ + import morphir.flowz.instrumentation.InstrumentationLogging + + def run(args: List[String]): URIO[ZEnv, ExitCode] = { + val stepA = Step.set("SA").as('A') + val stepB = Step.set(List("SA")).as("B") + val finalStep = Step.mapN(stepA, stepB) { case (a, b) => + StepSuccess(state = (a.state, b.state), result = (a.result, b.result)) + } + + val finalStepAlt = Step.mapN(stepA, stepB) { case (a, b) => + ((a.state, b.state), (a.result, b.result)) + } + + ( + (finalStep.run.tap(res => console.putStrLn(s"Result Orig: $res")) *> + finalStepAlt.run.tap(res => console.putStrLn(s"Result Alt: $res"))).exitCode + ).provideCustomLayer(StepUidGenerator.live ++ InstrumentationLogging.console(logLevel = LogLevel.Trace)) + } +} diff --git a/morphir/flowz/src/morphir/flowz/StepCompanion.scala b/morphir/flowz/src/morphir/flowz/StepCompanion.scala deleted file mode 100644 index 7c44fe40..00000000 --- a/morphir/flowz/src/morphir/flowz/StepCompanion.scala +++ /dev/null @@ -1,292 +0,0 @@ -package morphir.flowz - -import zio._ - -abstract class StepCompanion[-BaseEnv] { - - /** Defines a step that does not rely on state. */ - def act[Params, Out](f: Params => Out): Activity[Any, Params, Throwable, Out] = - Step( - ZIO - .environment[StepContext.having.Parameters[Params]] - .mapEffect(ctx => StepOutputs.assignBoth(f(ctx.inputs.params))) - ) - - /** Defines a step that does not rely on state. */ - def act[Params, Out](name: String)(f: Params => Out): Activity[Any, Params, Throwable, Out] = - Step( - ZIO - .environment[StepContext.having.Parameters[Params]] - .mapEffect(ctx => StepOutputs.assignBoth(f(ctx.inputs.params))), - name = Option(name) - ) - - /** Defines a step that does not rely on state. */ - def act[Params, Out](name: String, description: String)(f: Params => Out): Activity[Any, Params, Throwable, Out] = - Step( - ZIO - .environment[StepContext.having.Parameters[Params]] - .mapEffect(ctx => StepOutputs.assignBoth(f(ctx.inputs.params))), - name = Option(name), - description = Option(description) - ) - - /** Defines a step that performs some effect which does not rely on state. */ - def activity[Env, Params, Err, Value](func: Params => ZIO[Env, Err, Value]): Activity[Env, Params, Err, Value] = - Step(ZIO.accessM[StepContext[Env, Any, Params]] { ctx => - func(ctx.inputs.params).map(value => StepOutputs(state = value, value = value)).provide(ctx.environment) - }) - - /** Defines a step that performs some effect which does not rely on state. */ - def activity[Env, Params, Err, Value](name: String)( - func: Params => ZIO[Env, Err, Value] - ): Activity[Env, Params, Err, Value] = - Step( - ZIO.accessM[StepContext[Env, Any, Params]] { ctx => - func(ctx.inputs.params).map(value => StepOutputs(state = value, value = value)).provide(ctx.environment) - }, - name = Option(name) - ) - - def activity[Env, Params, Err, Value](name: String, description: String)( - func: Params => ZIO[Env, Err, Value] - ): Activity[Env, Params, Err, Value] = - Step( - ZIO.accessM[StepContext[Env, Any, Params]] { ctx => - func(ctx.inputs.params).map(value => StepOutputs(state = value, value = value)).provide(ctx.environment) - }, - name = Option(name), - description = Option(description) - ) - - def context[Env, StateIn, Params]: Step[StateIn, StateIn, Env, Params, Nothing, StepContext[Env, StateIn, Params]] = - Step( - ZIO - .environment[StepContext[Env, StateIn, Params]] - .map(ctx => StepOutputs(value = ctx, state = ctx.inputs.state)) - ) - - def describe[StateIn, StateOut, Env, Params, Err, Value](description: String)( - step: Step[StateIn, StateOut, Env, Params, Err, Value] - ): Step[StateIn, StateOut, Env, Params, Err, Value] = - step.describe(description) - - def effect[Value](value: => Value): TaskStep[Any, Value] = - Step(ZIO.environment[StepContext.having.AnyInputs].mapEffect(_ => StepOutputs.fromValue(value))) - - def environment[Env <: BaseEnv]: Step[Any, Env, Env, Any, Nothing, Env] = - Step(ZIO.environment[StepContext.having.Environment[Env]].map(ctx => StepOutputs.setBoth(ctx.environment))) - - def fail[Err](error: Err): Step[Any, Nothing, BaseEnv, Any, Err, Nothing] = - Step(ZIO.environment[StepContext.having.AnyInputs] *> ZIO.fail(error)) - - def fromEffect[R, E, A](effect: ZIO[R, E, A]): Step[Any, Unit, R, Any, E, A] = - Step( - ZIO - .environment[StepContext[R, Any, Any]] - .flatMap(ctx => effect.map(StepOutputs.fromValue(_)).provide(ctx.environment)) - ) - - def fromEffect[P, R, E, A](func: P => ZIO[R, E, A]): Step[Any, Unit, R, P, E, A] = - Step( - ZIO - .environment[StepContext[R, Any, P]] - .flatMap(ctx => func(ctx.inputs.params).map(StepOutputs.fromValue(_)).provide(ctx.environment)) - ) - - /** - * Get the state. - */ - def get[State]: Step[State, State, Any, Any, Nothing, State] = - modify[State, State, State](s => (s, s)) - - def mapN[S0, R, P, Err, SA, A, SB, B, SC, C](flowA: Step[S0, SA, R, P, Err, A], flowB: Step[S0, SB, R, P, Err, B])( - f: (StepOutputs[SA, A], StepOutputs[SB, B]) => StepOutputs[SC, C] - ): Step[S0, SC, R, P, Err, C] = - flowA.zipWith(flowB)(f) - - def mapParN[S0, R, P, Err, SA, A, SB, B, SC, C]( - flowA: Step[S0, SA, R, P, Err, A], - flowB: Step[S0, SB, R, P, Err, B] - )( - f: (StepOutputs[SA, A], StepOutputs[SB, B]) => StepOutputs[SC, C] - ): Step[S0, SC, R, P, Err, C] = - flowA.zipWithPar(flowB)(f) - - def mapParN[S0, R, P, Err, SA, A, SB, B, SC, C, SD, D]( - flowA: Step[S0, SA, R, P, Err, A], - flowB: Step[S0, SB, R, P, Err, B], - flowC: Step[S0, SC, R, P, Err, C] - )( - f: (StepOutputs[SA, A], StepOutputs[SB, B], StepOutputs[SC, C]) => StepOutputs[SD, D] - ): Step[S0, SD, R, P, Err, D] = - (flowA <&> flowB <&> flowC).mapOutputChannels { case StepOutputs(((sa, sb), sc), ((a, b), c)) => - val outsA = StepOutputs(state = sa, value = a) - val outsB = StepOutputs(state = sb, value = b) - val outsC = StepOutputs(state = sc, value = c) - f(outsA, outsB, outsC) - } - - def mapParN[S0, R, P, Err, SA, A, SB, B, SC, C, SD, D, SF, F]( - flowA: Step[S0, SA, R, P, Err, A], - flowB: Step[S0, SB, R, P, Err, B], - flowC: Step[S0, SC, R, P, Err, C], - flowD: Step[S0, SD, R, P, Err, D] - )( - f: (StepOutputs[SA, A], StepOutputs[SB, B], StepOutputs[SC, C], StepOutputs[SD, D]) => StepOutputs[SF, F] - ): Step[S0, SF, R, P, Err, F] = - (flowA <&> flowB <&> flowC <&> flowD).mapOutputChannels { - case StepOutputs((((sa, sb), sc), sd), (((a, b), c), d)) => - val outsA = StepOutputs(state = sa, value = a) - val outsB = StepOutputs(state = sb, value = b) - val outsC = StepOutputs(state = sc, value = c) - val outsD = StepOutputs(state = sd, value = d) - f(outsA, outsB, outsC, outsD) - } - - def mapParN[S0, R, P, Err, S1, A1, S2, A2, S3, A3, S4, A4, S5, A5, S6, A6]( - flow1: Step[S0, S1, R, P, Err, A1], - flow2: Step[S0, S2, R, P, Err, A2], - flow3: Step[S0, S3, R, P, Err, A3], - flow4: Step[S0, S4, R, P, Err, A4], - flow5: Step[S0, S5, R, P, Err, A5] - )( - f: ( - StepOutputs[S1, A1], - StepOutputs[S2, A2], - StepOutputs[S3, A3], - StepOutputs[S4, A4], - StepOutputs[S5, A5] - ) => StepOutputs[S6, A6] - ): Step[S0, S6, R, P, Err, A6] = - (flow1 <&> flow2 <&> flow3 <&> flow4 <&> flow5).mapOutputChannels { - case StepOutputs(((((sa, sb), sc), sd), se), ((((a, b), c), d), e)) => - val outsA = StepOutputs(state = sa, value = a) - val outsB = StepOutputs(state = sb, value = b) - val outsC = StepOutputs(state = sc, value = c) - val outsD = StepOutputs(state = sd, value = d) - val outsE = StepOutputs(state = se, value = e) - f(outsA, outsB, outsC, outsD, outsE) - } - - def mapParN[S0, R, P, Err, S1, A1, S2, A2, S3, A3, S4, A4, S5, A5, S6, A6, S7, A7]( - flow1: Step[S0, S1, R, P, Err, A1], - flow2: Step[S0, S2, R, P, Err, A2], - flow3: Step[S0, S3, R, P, Err, A3], - flow4: Step[S0, S4, R, P, Err, A4], - flow5: Step[S0, S5, R, P, Err, A5], - flow6: Step[S0, S6, R, P, Err, A6] - )( - func: ( - StepOutputs[S1, A1], - StepOutputs[S2, A2], - StepOutputs[S3, A3], - StepOutputs[S4, A4], - StepOutputs[S5, A5], - StepOutputs[S6, A6] - ) => StepOutputs[S7, A7] - ): Step[S0, S7, R, P, Err, A7] = - (flow1 <&> flow2 <&> flow3 <&> flow4 <&> flow5 <&> flow6).mapOutputChannels { - case StepOutputs((((((sa, sb), sc), sd), se), sf), (((((a, b), c), d), e), f)) => - val outsA = StepOutputs(state = sa, value = a) - val outsB = StepOutputs(state = sb, value = b) - val outsC = StepOutputs(state = sc, value = c) - val outsD = StepOutputs(state = sd, value = d) - val outsE = StepOutputs(state = se, value = e) - val outsF = StepOutputs(state = sf, value = f) - func(outsA, outsB, outsC, outsD, outsE, outsF) - } - - def mapParN[S0, R, P, Err, S1, A1, S2, A2, S3, A3, S4, A4, S5, A5, S6, A6, S7, A7, S8, A8]( - flow1: Step[S0, S1, R, P, Err, A1], - flow2: Step[S0, S2, R, P, Err, A2], - flow3: Step[S0, S3, R, P, Err, A3], - flow4: Step[S0, S4, R, P, Err, A4], - flow5: Step[S0, S5, R, P, Err, A5], - flow6: Step[S0, S6, R, P, Err, A6], - flow7: Step[S0, S7, R, P, Err, A7] - )( - func: ( - StepOutputs[S1, A1], - StepOutputs[S2, A2], - StepOutputs[S3, A3], - StepOutputs[S4, A4], - StepOutputs[S5, A5], - StepOutputs[S6, A6], - StepOutputs[S7, A7] - ) => StepOutputs[S8, A8] - ): Step[S0, S8, R, P, Err, A8] = - (flow1 <&> flow2 <&> flow3 <&> flow4 <&> flow5 <&> flow6 <&> flow7).mapOutputChannels { - case StepOutputs(((((((sa, sb), sc), sd), se), sf), sg), ((((((a, b), c), d), e), f), g)) => - val outsA = StepOutputs(state = sa, value = a) - val outsB = StepOutputs(state = sb, value = b) - val outsC = StepOutputs(state = sc, value = c) - val outsD = StepOutputs(state = sd, value = d) - val outsE = StepOutputs(state = se, value = e) - val outsF = StepOutputs(state = sf, value = f) - val outsG = StepOutputs(state = sg, value = g) - func(outsA, outsB, outsC, outsD, outsE, outsF, outsG) - } - - def mapParN[S0, R, P, Err, S1, A1, S2, A2, S3, A3, S4, A4, S5, A5, S6, A6, S7, A7, S8, A8, S9, A9]( - flow1: Step[S0, S1, R, P, Err, A1], - flow2: Step[S0, S2, R, P, Err, A2], - flow3: Step[S0, S3, R, P, Err, A3], - flow4: Step[S0, S4, R, P, Err, A4], - flow5: Step[S0, S5, R, P, Err, A5], - flow6: Step[S0, S6, R, P, Err, A6], - flow7: Step[S0, S7, R, P, Err, A7], - flow8: Step[S0, S8, R, P, Err, A8] - )( - func: ( - StepOutputs[S1, A1], - StepOutputs[S2, A2], - StepOutputs[S3, A3], - StepOutputs[S4, A4], - StepOutputs[S5, A5], - StepOutputs[S6, A6], - StepOutputs[S7, A7], - StepOutputs[S8, A8] - ) => StepOutputs[S9, A9] - ): Step[S0, S9, R, P, Err, A9] = - (flow1 <&> flow2 <&> flow3 <&> flow4 <&> flow5 <&> flow6 <&> flow7 <&> flow8).mapOutputChannels { - case StepOutputs((((((((sa, sb), sc), sd), se), sf), sg), sh), (((((((a, b), c), d), e), f), g), h)) => - val outsA = StepOutputs(state = sa, value = a) - val outsB = StepOutputs(state = sb, value = b) - val outsC = StepOutputs(state = sc, value = c) - val outsD = StepOutputs(state = sd, value = d) - val outsE = StepOutputs(state = se, value = e) - val outsF = StepOutputs(state = sf, value = f) - val outsG = StepOutputs(state = sg, value = g) - val outsH = StepOutputs(state = sh, value = h) - func(outsA, outsB, outsC, outsD, outsE, outsF, outsG, outsH) - } - - def modify[StateIn, StateOut, Output]( - func: StateIn => (StateOut, Output) - ): Step[StateIn, StateOut, Any, Any, Nothing, Output] = - context[Any, StateIn, Any].flatMap { ctx => - val (stateOut, output) = func(ctx.inputs.state) - Step.succeed(value = output, state = stateOut) - } - - def name[StateIn, StateOut, Env, Params, Err, Value](name: String)( - step: Step[StateIn, StateOut, Env, Params, Err, Value] - ): Step[StateIn, StateOut, Env, Params, Err, Value] = - step.named(name) - - /** - * A step that returns the given parameters. - */ - def parameters[P]: Step[Any, P, Any, P, Nothing, P] = - Step.context[Any, Any, P].flatMap { ctx => - Step.succeed(ctx.inputs.params, ctx.inputs.params) - } - - def succeed[Value](value: => Value): Step[Any, Unit, Any, Any, Nothing, Value] = - Step(ZIO.succeed(StepOutputs.fromValue(value))) - - def succeed[State, Value](state: => State, value: => Value): Step[Any, State, Any, Any, Nothing, Value] = - Step(ZIO.succeed(StepOutputs(state = state, value = value))) - -} diff --git a/morphir/flowz/src/morphir/flowz/StepContext.scala b/morphir/flowz/src/morphir/flowz/StepContext.scala deleted file mode 100644 index 03911897..00000000 --- a/morphir/flowz/src/morphir/flowz/StepContext.scala +++ /dev/null @@ -1,70 +0,0 @@ -package morphir.flowz - -final case class StepContext[+Env, +State, +Params](environment: Env, inputs: StepInputs[State, Params]) { self => - - def updateInputs[S, P](state: S, params: P): StepContext[Env, S, P] = - self.copy(inputs = StepInputs(state = state, params = params)) - - def updateInputs[S, A](outputs: StepOutputs[S, A]): StepContext[Env, S, A] = - self.copy(inputs = outputs.toInputs) - - def updateParams[P](params: P): StepContext[Env, State, P] = - self.copy(inputs = self.inputs.copy(params = params)) - - def updateState[S](state: S): StepContext[Env, S, Params] = - self.copy(inputs = self.inputs.copy(state = state)) -} - -object StepContext { - - val unit: StepContext[Any, Any, Any] = new StepContext(environment = (), inputs = StepInputs(state = (), params = ())) - @inline val any: StepContext[Any, Any, Any] = StepContext.unit - - def apply[Env, State, Params](environment: Env, state: State, params: Params): StepContext[Env, State, Params] = - StepContext(environment = environment, inputs = StepInputs(params = params, state = state)) - - def fromEnvironment[Env](environment: Env): StepContext[Env, Any, Any] = - StepContext(environment, inputs = StepInputs.AnyInputs) - - def fromParams[Params](params: Params): StepContext[Any, Any, Params] = - StepContext(environment = (): Any, inputs = StepInputs.fromParams(params)) - - def fromState[State](state: State): StepContext[Any, State, Any] = - StepContext(environment = (): Any, inputs = StepInputs.fromState(state)) - - def provideEnvironment[Env](env: => Env): StepContext[Env, Unit, Unit] = - setEnvironment(env) - - def setEnvironment[Env](env: => Env): StepContext[Env, Unit, Unit] = - StepContext(environment = env, inputs = StepInputs.unit) - - object having { - - /** - * A `StepContext` which accepts any inputs. - */ - type AnyInputs = StepContext[Any, Any, Any] - - /** - * A `StepContext` which accepts any environment. - */ - type AnyEnv[+State, +Params] = StepContext[Any, State, Params] - - /** - * A `StepContext` which accepts any state. - */ - type AnyState[+Env, +Params] = StepContext[Env, Any, Params] - - /** - * A `StepContext` which accepts any parameters - */ - type AnyParams[+Env, +State] = StepContext[Env, State, Any] - - /** - * A `StepContext` whose environment must be compatible with type `R`. - */ - type Environment[+R] = StepContext[R, Any, Any] - type InputState[+S] = StepContext[Any, S, Any] - type Parameters[+P] = StepContext[Any, Any, P] - } -} diff --git a/morphir/flowz/src/morphir/flowz/StepExports.scala b/morphir/flowz/src/morphir/flowz/StepExports.scala deleted file mode 100644 index 15a1a1f8..00000000 --- a/morphir/flowz/src/morphir/flowz/StepExports.scala +++ /dev/null @@ -1,36 +0,0 @@ -package morphir.flowz - -import zio.Fiber - -trait StepExports { - def stage[StateIn, StateOut, Env, Params, Err, Out]( - func: (StateIn, Params) => Step[StateIn, StateOut, Env, Params, Err, Out] - ): Step[StateIn, StateOut, Env, Params, Err, Out] = Step.stage(func) - - def step[StateIn, StateOut, Params, Out]( - func: (StateIn, Params) => (StateOut, Out) - ): Step[StateIn, StateOut, Any, Params, Throwable, Out] = Step.step(func) - - final type Activity[-Env, -Params, +Err, +Value] = Step[Any, Value, Env, Params, Err, Value] - val Activity: morphir.flowz.Activity.type = morphir.flowz.Activity - - type ForkedStep[-StateIn, +StateOut, -Env, -Params, +Err, +Output] = - Step[StateIn, Unit, Env, Params, Nothing, Fiber.Runtime[Err, StepOutputs[StateOut, Output]]] - - final type Step[-StateIn, +StateOut, -Env, -Params, +Err, +Value] = - morphir.flowz.Step[StateIn, StateOut, Env, Params, Err, Value] - val Step: morphir.flowz.Step.type = morphir.flowz.Step - - final type IOStep[-Params, +Err, +Value] = Step[Any, Unit, Any, Params, Err, Value] - final type TaskStep[-Params, +Value] = Step[Any, Unit, Any, Params, Throwable, Value] - final type UStep[-Params, +Value] = Step[Any, Unit, Any, Params, Nothing, Value] - - final type StepOutputs[+State, +Value] = morphir.flowz.StepOutputs[State, Value] - val StepOutputs: morphir.flowz.StepOutputs.type = morphir.flowz.StepOutputs - - final type StepInputs[+State, +Params] = morphir.flowz.StepInputs[State, Params] - val StepInputs: morphir.flowz.StepInputs.type = morphir.flowz.StepInputs - - final type StepContext[+Env, +State, +Params] = morphir.flowz.StepContext[Env, State, Params] - val StepContext: morphir.flowz.StepContext.type = morphir.flowz.StepContext -} diff --git a/morphir/flowz/src/morphir/flowz/StepFailure.scala b/morphir/flowz/src/morphir/flowz/StepFailure.scala new file mode 100644 index 00000000..446b5812 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/StepFailure.scala @@ -0,0 +1,8 @@ +package morphir.flowz + +import zio.Cause + +sealed abstract class StepFailure[+E] +object StepFailure { + final case class Runtime[+E](cause: Cause[E]) extends StepFailure[E] +} diff --git a/morphir/flowz/src/morphir/flowz/StepInfo.scala b/morphir/flowz/src/morphir/flowz/StepInfo.scala deleted file mode 100644 index dac6ac70..00000000 --- a/morphir/flowz/src/morphir/flowz/StepInfo.scala +++ /dev/null @@ -1,3 +0,0 @@ -package morphir.flowz - -final case class StepInfo(name: Option[String], description: Option[String], visibility: StepVisibility) diff --git a/morphir/flowz/src/morphir/flowz/StepInputs.scala b/morphir/flowz/src/morphir/flowz/StepInputs.scala deleted file mode 100644 index 71ee92de..00000000 --- a/morphir/flowz/src/morphir/flowz/StepInputs.scala +++ /dev/null @@ -1,45 +0,0 @@ -package morphir.flowz - -final case class StepInputs[+State, +Params](state: State, params: Params) { self => - def acceptingAny: StepInputs[Any, Any] = this - - def acceptingAnyState: StepInputs[Any, Params] = this - - def acceptingAnyParams: StepInputs[State, Any] = this - - /** - * Converts the `StepInputs` to `StepOutputs` - */ - def toOutputs: StepOutputs[State, Params] = - StepOutputs(state = state, value = params) - - def map[P](func: Params => P): StepInputs[State, P] = - copy(params = func(params)) - - def mapState[S](func: State => S): StepInputs[S, Params] = - copy(state = func(state)) - - def tupled: (State, Params) = (state, params) -} - -object StepInputs { - - type AnyInputs = StepInputs[Any, Any] - val AnyInputs: StepInputs.AnyInputs = StepInputs(state = (), params = ()) - - def fromState[State](state: State): StepInputs[State, Any] = StepInputs(state = state, params = ()) - def fromParams[Params](params: Params): StepInputs[Any, Params] = StepInputs(state = (), params = params) - - def provideParameters[P](params: P): StepInputs[Unit, P] = setParameters(params) - - def provideState[S](state: S): StepInputs[S, Unit] = setState(state) - - def setParameters[P](params: P): StepInputs[Unit, P] = - StepInputs(params = params, state = ()) - - def setState[S](state: S): StepInputs[S, Unit] = - StepInputs(state = state, params = ()) - - val unit: StepInputs[Unit, Unit] = StepInputs(state = (), params = ()) - val none: StepInputs[Option[Nothing], Option[Nothing]] = StepInputs(state = None, params = None) -} diff --git a/morphir/flowz/src/morphir/flowz/StepOutputs.scala b/morphir/flowz/src/morphir/flowz/StepOutputs.scala deleted file mode 100644 index 7f6ac583..00000000 --- a/morphir/flowz/src/morphir/flowz/StepOutputs.scala +++ /dev/null @@ -1,49 +0,0 @@ -package morphir.flowz - -final case class StepOutputs[+State, +Value](state: State, value: Value) { self => - def map[Value2](func: Value => Value2): StepOutputs[State, Value2] = - StepOutputs(value = func(value), state = state) - - def mapState[State2](func: State => State2): StepOutputs[State2, Value] = - StepOutputs(value = value, state = func(state)) - - def transform[State2, Value2](func: (State, Value) => (State2, Value2)): StepOutputs[State2, Value2] = { - val (newState, newValue) = func(self.state, self.value) - StepOutputs(state = newState, value = newValue) - } - - def toInputs: StepInputs[State, Value] = - StepInputs(state = self.state, params = self.value) - - def toTuple: (State, Value) = (self.state, self.value) - - def zip[State2, Output2](that: StepOutputs[State2, Output2]): StepOutputs[(State, State2), (Value, Output2)] = - StepOutputs(value = (self.value, that.value), state = (self.state, that.state)) - -} - -object StepOutputs { - def assignBoth[A](value: A): StepOutputs[A, A] = unified(value) - - def fromState[State](state: => State): StepOutputs[State, Unit] = - StepOutputs(state = state, value = ()) - - def fromValue[Value](value: => Value): StepOutputs[Unit, Value] = - StepOutputs(state = (), value = value) - - def apply[Value](value: => Value): StepOutputs[Unit, Value] = - StepOutputs(value = value, state = ()) - - val empty: StepOutputs[Option[Nothing], Option[Nothing]] = StepOutputs(None, None) - - val none: StepOutputs[Option[Nothing], Option[Nothing]] = StepOutputs(value = None, state = None) - - val unit: StepOutputs[Unit, Unit] = StepOutputs((), ()) - - def fromTuple[Value, State](tuple: (State, Value)): StepOutputs[State, Value] = - new StepOutputs(value = tuple._2, state = tuple._1) - - def setBoth[A](value: A): StepOutputs[A, A] = unified(value) - - def unified[Value](value: Value): StepOutputs[Value, Value] = StepOutputs(value, value) -} diff --git a/morphir/flowz/src/morphir/flowz/StepSuccess.scala b/morphir/flowz/src/morphir/flowz/StepSuccess.scala new file mode 100644 index 00000000..b690161f --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/StepSuccess.scala @@ -0,0 +1,33 @@ +package morphir.flowz + +final case class StepSuccess[+S, +A](state: S, result: A) { self => + def flatMap[State2, Value2](f: A => StepSuccess[State2, Value2]): StepSuccess[State2, Value2] = + f(result) + + def map[B](f: A => B): StepSuccess[S, B] = + copy(result = f(self.result)) + + def mapState[S1](f: S => S1): StepSuccess[S1, A] = + copy(state = f(self.state)) + + def toTuple: (S, A) = (state, result) + + def transform[S1, B]( + f: StepSuccess[S, A] => StepSuccess[S1, B] + ): StepSuccess[S1, B] = + f(self) + + def zip[S1, B]( + that: StepSuccess[S1, B] + ): StepSuccess[(S, S1), (A, B)] = + StepSuccess(state = (state, that.state), result = (result, that.result)) +} + +object StepSuccess { + + implicit def behaviorSuccessFromPair[State, Value](pair: (State, Value)): StepSuccess[State, Value] = + StepSuccess(state = pair._1, result = pair._2) + + def fromPair[State, Value](pair: (State, Value)): StepSuccess[State, Value] = + StepSuccess(state = pair._1, result = pair._2) +} diff --git a/morphir/flowz/src/morphir/flowz/StepUid.scala b/morphir/flowz/src/morphir/flowz/StepUid.scala new file mode 100644 index 00000000..2611a656 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/StepUid.scala @@ -0,0 +1,8 @@ +package morphir.flowz + +import zio.URIO + +object StepUid { + def nextUid: URIO[StepUidGenerator, StepUid] = + uidGenerator.nextUid[Step.type] +} diff --git a/morphir/flowz/src/morphir/flowz/StepUidGenerator.scala b/morphir/flowz/src/morphir/flowz/StepUidGenerator.scala new file mode 100644 index 00000000..42d09228 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/StepUidGenerator.scala @@ -0,0 +1,9 @@ +package morphir.flowz + +import morphir.flowz.uidGenerator.UidGenerator +import zio.ZLayer +import zio.clock.Clock + +object StepUidGenerator { + val live: ZLayer[Clock, Nothing, StepUidGenerator] = UidGenerator.live +} diff --git a/morphir/flowz/src/morphir/flowz/StepVisibility.scala b/morphir/flowz/src/morphir/flowz/StepVisibility.scala deleted file mode 100644 index a7babee0..00000000 --- a/morphir/flowz/src/morphir/flowz/StepVisibility.scala +++ /dev/null @@ -1,13 +0,0 @@ -package morphir.flowz - -object StepVisibility { - case object Visible extends StepVisibility - case object Hidden extends StepVisibility - - val value: Set[StepVisibility] = Set(Visible, Hidden) -} - -/** - * The visibility of a Step. - */ -sealed abstract class StepVisibility extends Product with Serializable diff --git a/morphir/flowz/src/morphir/flowz/TODO.md b/morphir/flowz/src/morphir/flowz/TODO.md deleted file mode 100644 index 35c307a2..00000000 --- a/morphir/flowz/src/morphir/flowz/TODO.md +++ /dev/null @@ -1,55 +0,0 @@ -## Create a DSL for Flow Creation - -Create a DSL to make it easy to create an executable flow which is similar to the DSL from Jenkins pipeline - -Target State: - -```scala - -flow { - setup { in => - // Code to setup a context - } - - // We can either create stages - stages { - stage("stage1") { - step1 >>> step2 - } - } - - // or directly create steps - steps { - step() - } -} - -flow ( - context { in => - // Code to setup a context - }, - - // We can either create stages - stage("stage1") { - step1 >>> step2 - } -) - -flow("") - .setup() - .stages() - .run() - -``` - -## Work on Bootstrapping a flow - -Consider how we can build up a flow from its services and state. - -```scala -import morphir.flowz.{StepContext, StepInputs} -def context[In,Env,State,Params] - (makeInputs: In => StepInputs[State,Params]) - (setup: StepInputs[State,Params] => StepContext[Env,State,Params]) -``` - diff --git a/morphir/flowz/src/morphir/flowz/ZBehavior.scala b/morphir/flowz/src/morphir/flowz/ZBehavior.scala new file mode 100644 index 00000000..2b67d00d --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/ZBehavior.scala @@ -0,0 +1,42 @@ +package morphir.flowz + +import zio.{ CanFail, NeedsEnv, ZIO } + +object ZBehavior { + + def apply[InitialState, StateOut, Msg, Env, E, A]( + func: (InitialState, Msg) => ZIO[Env, E, StepSuccess[StateOut, A]] + )(implicit + evStateIn: NeedsInputState[InitialState], + evMsg: NeedsMsg[Msg], + evEnv: NeedsEnv[Env], + evCanFail: CanFail[E] + ): ZBehavior[InitialState, StateOut, Msg, Env, E, A] = { + val _ = (evStateIn, evMsg, evCanFail) + ZIO.accessM[(InitialState, Msg, Env)] { case (stateIn, msg, env) => func(stateIn, msg).provide(env) } + } + + implicit def effectFromFunc[InitialState, StateOut, In, Env, E, A]( + func: (InitialState, In) => ZIO[Env, E, StepSuccess[StateOut, A]] + )(implicit + evStateIn: NeedsInputState[InitialState], + evMsg: NeedsMsg[In], + evEnv: NeedsEnv[Env], + evCanFail: CanFail[E] + ): ZBehavior[InitialState, StateOut, In, Env, E, A] = apply(func) + + implicit def effectFromFunc2[InitialState, StateOut, In, Env, E, A]( + func: (InitialState, In) => ZIO[Env, E, (StateOut, A)] + )(implicit + evStateIn: NeedsInputState[InitialState], + evMsg: NeedsMsg[In], + evEnv: NeedsEnv[Env], + evCanFail: CanFail[E] + ): ZBehavior[InitialState, StateOut, In, Env, E, A] = { + val _ = (evStateIn, evMsg, evCanFail) + ZIO.accessM[(InitialState, In, Env)] { case (stateIn, msg, env) => + func(stateIn, msg).map(StepSuccess.fromPair).provide(env) + } + } + +} diff --git a/morphir/flowz/src/morphir/flowz/ZBehaviorSyntax.scala b/morphir/flowz/src/morphir/flowz/ZBehaviorSyntax.scala new file mode 100644 index 00000000..e304066b --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/ZBehaviorSyntax.scala @@ -0,0 +1,20 @@ +package morphir.flowz +import zio._ + +trait ZBehaviorSyntax { + import ZBehaviorSyntax._ + implicit def convertZIOToBehaviorOps[SIn, SOut, InputMsg, R, E, A]( + zio: ZIO[(SIn, InputMsg, R), E, StepSuccess[SOut, A]] + ): ZBehaviorOps[SIn, SOut, InputMsg, R, E, A] = + new ZBehaviorOps(zio) + +} + +object ZBehaviorSyntax extends ZBehaviorSyntax { + final class ZBehaviorOps[SIn, SOut, InputMsg, R, E, A]( + private val zio: ZBehavior[SIn, SOut, InputMsg, R, E, A] + ) extends AnyVal { self => + def provideState(initialState: SIn): ZBehavior[Any, SOut, InputMsg, R, E, A] = + ZIO.accessM[(Any, InputMsg, R)] { case (_, msg, r) => zio.provide((initialState, msg, r)) } + } +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/AbstractRunnableFlow.scala b/morphir/flowz/src/morphir/flowz/experimental/AbstractRunnableFlow.scala new file mode 100644 index 00000000..2bfe9e52 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/AbstractRunnableFlow.scala @@ -0,0 +1,18 @@ +package morphir.flowz.experimental + +import zio._ + +abstract class AbstractRunnableFlow { + type InputState + type OutputState + type Message + type Environment <: Has[_] + type Failure + type Success + + def flow: Flow[InputState, OutputState, Message, Environment, Failure, Success] + private[flowz] def runFlow(flow: Flow[InputState, OutputState, Message, Environment, Failure, Success]) = + //TODO: Complete definition following the example from zio-test + flow + +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/ExecutedFlow.scala b/morphir/flowz/src/morphir/flowz/experimental/ExecutedFlow.scala new file mode 100644 index 00000000..4a4ca14e --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/ExecutedFlow.scala @@ -0,0 +1,41 @@ +package morphir.flowz.experimental + +final case class ExecutedFlow[+E](caseValue: ExecutedFlow.FlowCase[E, ExecutedFlow[E]]) { self => + import morphir.flowz.experimental.ExecutedFlow._ + + /** + * Folds over all nodes to produce a final result. + */ + def fold[Z](f: FlowCase[E, Z] => Z): Z = + caseValue match { + case ProcessCase(label, specs) => f(ProcessCase(label, specs.map((_.fold(f))))) + case s @ StepCase(_) => f(s) + } + + /** + * Computes the size of the flow, i.e. the number of steps in the flow. + */ + def size: Int = + fold[Int] { + case ProcessCase(_, counts) => counts.sum + case StepCase(_) => 1 + } + +} + +object ExecutedFlow { + sealed trait FlowCase[+E, +Self] { self => + def map[Self2](f: Self => Self2): FlowCase[E, Self2] = + self match { + case ProcessCase(label, specs) => ProcessCase(label, specs.map(f)) + case StepCase(label) => StepCase(label) + } + } + + final case class ProcessCase[+Self](label: String, specs: Vector[Self]) extends FlowCase[Nothing, Self] + final case class StepCase[+E](label: String) extends FlowCase[E, Nothing] + + def process[E](label: String, specs: Vector[ExecutedFlow[E]]): ExecutedFlow[E] = + ExecutedFlow(ProcessCase(label, specs)) + +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/Flow.scala b/morphir/flowz/src/morphir/flowz/experimental/Flow.scala new file mode 100644 index 00000000..8aa07340 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/Flow.scala @@ -0,0 +1,150 @@ +package morphir.flowz.experimental + +import morphir.flowz._ +import zio._ + +final case class Flow[-InitialState, +StateOut, -InputMsg, -Env, +Err, +Result]( + caseValue: Flow.FlowCase[ + InitialState, + StateOut, + InputMsg, + Env, + Err, + Result, + Flow[InitialState, StateOut, InputMsg, Env, Err, Result] + ] +) { self => + import Flow._ + + /** + * Annotates each step in this flow with the specified property. + */ + final def annotate[V](key: Property[V], value: V): Flow[InitialState, StateOut, InputMsg, Env, Err, Result] = + transform[InitialState, StateOut, InputMsg, Env, Err, Result] { + case p @ ProcessCase(_, _) => p // We only add annotations at the step level + case StepCase(label, behavior, annotations) => + Flow.StepCase(label, behavior, annotations.annotate(key, value)) + } + + final def annotated: Flow[InitialState, StateOut, InputMsg, Env with Properties, Annotated[Err], Annotated[Result]] = + transform[InitialState, StateOut, InputMsg, Env with Properties, Annotated[Err], Annotated[Result]] { + case ProcessCase(label, children) => Flow.ProcessCase(label, children.mapError((_, PropertyMap.empty))) + case StepCase(label, behavior, annotations) => + Flow.StepCase(label, behavior.withAnnotation, annotations) + } + + /** + * Run is an alternative to execute that provides information around individual Step success. + */ + def exec[State]( + initialState: State, + message: InputMsg + )(implicit + ev1: State <:< InitialState, + ev2: StateOut <:< InitialState + ): ZManaged[Env, Err, StepSuccess[List[State], List[Result]]] = ??? + //ZManaged.accessManaged[(Any, Any, Any)] { env => } + + def execute( + initialState: InitialState, + message: InputMsg + ): ZManaged[Env, Nothing, Flow[Any, StateOut, Any, Any, Err, Result]] = + ??? + + def execute: ZManaged[(InitialState, InputMsg, Env), Nothing, Flow[Any, StateOut, Any, Any, Err, Result]] = + ZManaged.accessManaged[(InitialState, InputMsg, Env)] { case (initialState, message, env) => + execute(initialState, message).provide(env) + } + + /** + * Transforms the flow one layer at a time. + */ + final def transform[SIn1, SOut1, Msg1, Env1, Err1, Result1]( + f: FlowCase[ + InitialState, + StateOut, + InputMsg, + Env, + Err, + Result, + Flow[SIn1, SOut1, Msg1, Env1, Err1, Result1] + ] => FlowCase[ + SIn1, + SOut1, + Msg1, + Env1, + Err1, + Result1, + Flow[SIn1, SOut1, Msg1, Env1, Err1, Result1] + ] + ): Flow[SIn1, SOut1, Msg1, Env1, Err1, Result1] = caseValue match { + case ProcessCase(label, children) => Flow(f(ProcessCase(label, children.map(_.map(_.transform(f)))))) + case s @ StepCase(_, _, _) => Flow(f(s)) + } + + /** + * Updates a service in the environment of this effect. + */ + final def updateService[M] = + new Flow.UpdateService[InitialState, StateOut, InputMsg, Env, Err, Result, M](self) +} + +object Flow { + + def process[SIn, SOut, In, R, Err, Out]( + label: String, + children: ZManaged[(SIn, In, R), Err, Vector[Flow[SIn, SOut, In, R, Err, Out]]] + ): Flow[SIn, SOut, In, R, Err, Out] = + Flow(ProcessCase(label, children)) + + def step[SIn, SOut, In, R, Err, Out]( + label: String, + behavior: Step[SIn, SOut, In, R, Err, Out], + annotations: PropertyMap + ): Flow[SIn, SOut, In, R, Err, Out] = Flow(StepCase(label, behavior, annotations)) + + sealed abstract class FlowCase[-SIn, +SOut, -In, -R, +Err, +Out, +Self] { self => + final def map[Self1](f: Self => Self1): FlowCase[SIn, SOut, In, R, Err, Out, Self1] = self match { + case ProcessCase(label, children) => ProcessCase(label, children.map(_.map(f))) + case StepCase(label, behavior, annotations) => StepCase(label, behavior, annotations) + } + } + + final case class ProcessCase[-SIn, -InputMsg, -R, +Err, +Self]( + label: String, + children: ZManaged[(SIn, InputMsg, R), Err, Vector[Self]] + ) extends FlowCase[SIn, Nothing, InputMsg, R, Err, Nothing, Self] + + final case class StepCase[-SIn, +SOut, -InputMsg, -R, +Err, +Out]( + label: String, + behavior: Step[SIn, SOut, InputMsg, R, Err, Out], // ~ ZIO[(SIn, InputMsg, Env), E, StepSuccess[SOut, A]] + annotations: PropertyMap + ) extends FlowCase[SIn, SOut, InputMsg, R, Err, Out, Nothing] + + final case class UpdateService[-SIn, +SOut, -InputMsg, -Env, +Err, +Result, M]( + private val self: Flow[SIn, SOut, InputMsg, Env, Err, Result] + ) extends AnyVal { + def apply[Env1 <: Env with Has[M]]( + f: M => M + )(implicit ev: Has.IsHas[Env1], tag: Tag[M]): Flow[SIn, SOut, InputMsg, Env1, Err, Result] = ??? + } + + //ZIO[(SIn, InputMsg, Env), E, StepSuccess[SOut, A]] +} + +object example { + + object behaviors { + val behavior1 = Step.unit + val stateful = Step.get[List[String]] + } + + val flow = process("init")( + process("load data")( + process("inner")( + step("Get accounts")(behaviors.behavior1), + step("Get trade file")(Step.unit) + ) + ) + ) +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/FlowBaseEnv.scala b/morphir/flowz/src/morphir/flowz/experimental/FlowBaseEnv.scala new file mode 100644 index 00000000..3daeaee1 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/FlowBaseEnv.scala @@ -0,0 +1,11 @@ +package morphir.flowz.experimental + +import instrumentor.Instrumentor +import zio.Layer +import zio.clock.Clock +import zio.console.Console + +object FlowBaseEnv { + val default: Layer[Nothing, FlowBaseEnv] = + Console.live ++ Clock.live >+> Instrumentor.console() +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/FlowExecutor.scala b/morphir/flowz/src/morphir/flowz/experimental/FlowExecutor.scala new file mode 100644 index 00000000..f08b4993 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/FlowExecutor.scala @@ -0,0 +1,23 @@ +package morphir.flowz.experimental + +import morphir.flowz.{ ExecutableFlow, Properties } +import zio.{ ExecutionStrategy, Has, Layer, RIO, UIO } + +abstract class FlowExecutor[+InitialState, +Msg, +R <: Has[_], E] { + def run(flow: ExecutableFlow[InitialState, Msg, R, E], executionStrategy: ExecutionStrategy): UIO[ExecutedFlow[E]] + def initialize: RIO[FlowBaseEnv, (InitialState, Msg)] + def environment: Layer[Nothing, R] +} + +object FlowExecutor { + def default[SIn, Msg, R <: Properties, E](init: RIO[FlowBaseEnv, (SIn, Msg)])( + env: Layer[Nothing, R] + ): FlowExecutor[SIn, Msg, R, E] = new FlowExecutor[SIn, Msg, R, E] { + def run(flow: ExecutableFlow[SIn, Msg, R, E], executionStrategy: ExecutionStrategy): UIO[ExecutedFlow[E]] = + ??? + + val initialize: RIO[FlowBaseEnv, (SIn, Msg)] = init + + val environment: Layer[Nothing, R] = env + } +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/FlowFailure.scala b/morphir/flowz/src/morphir/flowz/experimental/FlowFailure.scala new file mode 100644 index 00000000..767c6799 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/FlowFailure.scala @@ -0,0 +1,20 @@ +package morphir.flowz.experimental + +import zio.Cause + +sealed abstract class FlowFailure[+E] +object FlowFailure { + //TODO: Add in Precondition, PostCondition, and Invariant as possible other failure types + // these can be built directly ontop of Assertion or using zio-prelude's Validation + + final case class Runtime[+E](cause: Cause[E]) extends FlowFailure[E] + + def die(t: Throwable): FlowFailure[Nothing] = + halt(Cause.die(t)) + + def fail[E](e: E): FlowFailure[E] = + halt(Cause.fail(e)) + + def halt[E](cause: Cause[E]): FlowFailure[E] = + Runtime(cause) +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/FlowRunner.scala b/morphir/flowz/src/morphir/flowz/experimental/FlowRunner.scala new file mode 100644 index 00000000..ca3390b6 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/FlowRunner.scala @@ -0,0 +1,51 @@ +package morphir.flowz.experimental + +import morphir.flowz.{ ExecutableFlow, FlowReporter } +import zio.internal.Platform +import zio.{ ExecutionStrategy, Exit, FiberFailure, Has, Layer, Managed, Runtime, URIO } + +final case class FlowRunner[+InitialState, Trg, R <: Has[_], E]( + executor: FlowExecutor[InitialState, Trg, R, E], + platform: Platform = Platform.makeDefault().withReportFailure(_ => ()), + reporter: FlowReporter[E] = FlowReporter.silent, //TODO: Make this a default one that actually does something + bootstrap: Layer[Nothing, FlowBaseEnv] = FlowBaseEnv.default +) { self => + lazy val runtime: Runtime[Unit] = Runtime((), platform) + + /** + * Runs the flow, producing the execution results. + */ + def run(flow: ExecutableFlow[InitialState, Trg, R, E]): URIO[FlowBaseEnv, ExecutedFlow[E]] = + executor.run(flow, ExecutionStrategy.ParallelN(4)).timed.flatMap { case (duration, results) => + reporter(duration, results).as(results) + } + + /** + * An unsafe, synchronous run of the specified flow. + */ + def unsafeRun(flow: ExecutableFlow[InitialState, Trg, R, E]): ExecutedFlow[E] = + self.runtime.unsafeRun(run(flow).provideLayer(bootstrap)) + + /** + * An unsafe, asynchronous run of the specified flow. + */ + def unsafeRunAsync(flow: ExecutableFlow[InitialState, Trg, R, E])(k: ExecutedFlow[E] => Unit): Unit = + runtime.unsafeRunAsync(run(flow).provideLayer(bootstrap)) { + case Exit.Success(v) => k(v) + case Exit.Failure(c) => throw FiberFailure(c) + } + + /** + * An unsafe, synchronous run of the specified flow. + */ + def unsafeRunSync( + flow: ExecutableFlow[InitialState, Trg, R, E] + ): Exit[Nothing, ExecutedFlow[E]] = + self.runtime.unsafeRunSync(run(flow).provideLayer(bootstrap)) + + def withPlatform(f: Platform => Platform): FlowRunner[InitialState, Trg, R, E] = + copy(platform = f(platform)) + + private[flowz] def buildRuntime: Managed[Nothing, Runtime[FlowBaseEnv]] = + bootstrap.toRuntime(platform) +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/FlowSuccess.scala b/morphir/flowz/src/morphir/flowz/experimental/FlowSuccess.scala new file mode 100644 index 00000000..81d84b1a --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/FlowSuccess.scala @@ -0,0 +1,9 @@ +package morphir.flowz.experimental + +import morphir.flowz.StepSuccess + +final case class FlowSuccess[+S, +A](state: S, result: A) +object FlowSuccess { + def fromResult[S, A](result: StepSuccess[S, A]): FlowSuccess[S, A] = + FlowSuccess(state = result.state, result = result.result) +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/Message.scala b/morphir/flowz/src/morphir/flowz/experimental/Message.scala new file mode 100644 index 00000000..b3085c64 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/Message.scala @@ -0,0 +1,5 @@ +package morphir.flowz.experimental + +import java.time.Instant + +final case class Message[+Payload](payload: Payload, timestamp: Instant) {} diff --git a/morphir/flowz/src/morphir/flowz/experimental/instrumentor.scala b/morphir/flowz/src/morphir/flowz/experimental/instrumentor.scala new file mode 100644 index 00000000..56c99be1 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/instrumentor.scala @@ -0,0 +1,35 @@ +package morphir.flowz.experimental + +import morphir.flowz.instrumentation.{ InstrumentationEvent, InstrumentationLogging } +import zio.clock.Clock +import zio.console.Console +import zio.logging.{ LogFormat, LogLevel, Logger } +import zio.{ Has, UIO, URIO, ZIO, ZLayer } + +object instrumentor { + type Instrumentor = Has[Instrumentor.Service] + + def logLine(line: String): URIO[Instrumentor, Unit] = + ZIO.accessM(_.get.logLine(line)) + + object Instrumentor { + trait Service { + def logLine(line: String): UIO[Unit] + + } + + final case class LoggingInstrumentor(logger: Logger[InstrumentationEvent]) extends Service { + def logLine(line: String): UIO[Unit] = logger.log(InstrumentationEvent.logLine(line)) + } + + val fromLogger: ZLayer[InstrumentationLogging, Nothing, Instrumentor] = ZLayer.fromService { + logger: Logger[InstrumentationEvent] => LoggingInstrumentor(logger) + } + + def console( + logLevel: LogLevel = LogLevel.Info, + format: LogFormat[InstrumentationEvent] = InstrumentationEvent.logFormats.coloredConsoleLogFormat + ): ZLayer[Console with Clock, Nothing, Instrumentor] = + InstrumentationLogging.console(logLevel, format).map(l => Has(LoggingInstrumentor(l.get))) + } +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/output.scala b/morphir/flowz/src/morphir/flowz/experimental/output.scala new file mode 100644 index 00000000..4df78402 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/output.scala @@ -0,0 +1,17 @@ +package morphir.flowz.experimental + +import zio._ + +object output { + type Output[A] = Has[OutputRef[A]] + + /** Get the value contained in the Output. */ + def value[A: Tag]: URIO[Output[A], A] = + ZIO.accessM(_.get.value) + + final case class OutputRef[A](private val ref: Ref[A], tag: Tag[A]) { self => + implicit def getTag: Tag[A] = tag + def value: UIO[A] = ref.get + + } +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/package.scala b/morphir/flowz/src/morphir/flowz/experimental/package.scala new file mode 100644 index 00000000..a4060aaf --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/package.scala @@ -0,0 +1,8 @@ +package morphir.flowz + +import morphir.flowz.experimental.instrumentor.Instrumentor +import zio.clock.Clock + +package object experimental { + type FlowBaseEnv = Instrumentor with Clock +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/params.scala b/morphir/flowz/src/morphir/flowz/experimental/params.scala new file mode 100644 index 00000000..865404ec --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/params.scala @@ -0,0 +1,16 @@ +package morphir.flowz.experimental + +import zio.{ Has, Ref, Tag, UIO, URIO, ZIO } + +object params { + type Params[A] = Has[ParamsRef[A]] + + /** Get the value contained in the Params. */ + def get[A: Tag]: URIO[Params[A], A] = + ZIO.accessM(_.get.get) + + final case class ParamsRef[A](private val ref: Ref[A], tag: Tag[A]) { self => + implicit def getTag: Tag[A] = tag + def get: UIO[A] = ref.get + } +} diff --git a/morphir/flowz/src/morphir/flowz/experimental/state.scala b/morphir/flowz/src/morphir/flowz/experimental/state.scala new file mode 100644 index 00000000..929c99f3 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/experimental/state.scala @@ -0,0 +1,32 @@ +package morphir.flowz.experimental + +import zio.{ Has, Ref, Tag, UIO, ULayer, URIO, ZIO } + +object state { + type State[A] = Has[StateRef[A]] + + /** Get the value contained in the State. */ + def get[A: Tag]: URIO[State[A], A] = + ZIO.accessM(_.get.get) + + /** Make a layer from the given value */ + def makeLayer[A: Tag](initialValue: A): ULayer[State[A]] = + StateRef.make(initialValue).toLayer + + /** Make an effect that creates a state */ + def make[A: Tag](initialValue: A): UIO[State[A]] = + StateRef.make(initialValue).map(ref => Has(ref)) + + final case class StateRef[A](private val ref: Ref[A], tag: Tag[A]) { self => + implicit def getTag: Tag[A] = tag + @inline def get: UIO[A] = ref.get + + def toState: State[A] = Has(self) + } + + object StateRef { + def make[A: Tag](initialValue: A): UIO[StateRef[A]] = + Ref.make(initialValue).map(ref => StateRef(ref, Tag[A])) + } + +} diff --git a/morphir/flowz/src/morphir/flowz/iLog.scala b/morphir/flowz/src/morphir/flowz/iLog.scala new file mode 100644 index 00000000..dd70aad3 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/iLog.scala @@ -0,0 +1,50 @@ +package morphir.flowz +import morphir.flowz.instrumentation.{ InstrumentationEvent, InstrumentationLogger, InstrumentationLogging } +import zio.logging.{ LogContext, LogLevel } +import zio.{ Cause, URIO, ZIO } + +/** + * Provides accessors for using the InstrumentationLogger. + */ +object iLog { + def apply(level: LogLevel)(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + InstrumentationLogging.log(level)(event) + + val context: URIO[InstrumentationLogging, LogContext] = + InstrumentationLogging.context + + def debug(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + InstrumentationLogging.debug(event) + + def derive(f: LogContext => LogContext): ZIO[InstrumentationLogging, Nothing, InstrumentationLogger] = + InstrumentationLogging.derive(f) + + def error(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + InstrumentationLogging.error(event) + + def error(event: => InstrumentationEvent, cause: Cause[Any]): ZIO[InstrumentationLogging, Nothing, Unit] = + InstrumentationLogging.error(event, cause) + + def info(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + InstrumentationLogging.info(event) + + def locally[R <: InstrumentationLogging, E, A1](fn: LogContext => LogContext)( + zio: ZIO[R, E, A1] + ): ZIO[InstrumentationLogging with R, E, A1] = + InstrumentationLogging.locally(fn)(zio) + + def locallyM[R <: InstrumentationLogging, E, A1]( + fn: LogContext => URIO[R, LogContext] + )(zio: ZIO[R, E, A1]): ZIO[InstrumentationLogging with R, E, A1] = + InstrumentationLogging.locallyM(fn)(zio) + + def throwable(event: => InstrumentationEvent, t: Throwable): ZIO[InstrumentationLogging, Nothing, Unit] = + InstrumentationLogging.throwable(event, t) + + def trace(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + InstrumentationLogging.trace(event) + + def warn(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + InstrumentationLogging.warn(event) + +} diff --git a/morphir/flowz/src/morphir/flowz/instrumentation/InstrumentationEvent.scala b/morphir/flowz/src/morphir/flowz/instrumentation/InstrumentationEvent.scala new file mode 100644 index 00000000..96b86579 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/instrumentation/InstrumentationEvent.scala @@ -0,0 +1,84 @@ +package morphir.flowz.instrumentation + +import morphir.flowz.{ NodePath, StepUid } +import zio.logging.LogFormat.LineFormatter +import zio.logging.{ LogContext, LogFormat } + +/** + * An event used to instrument (provide information) about the execution of a flow, process, step, behavior, etc. + */ +sealed abstract class InstrumentationEvent +object InstrumentationEvent { + + def logLine(line: String): LogLine = LogLine(line) + def runningStep(message: String, uid: StepUid, label: String, path: Option[NodePath] = None): RunningStep = + RunningStep(message, uid, label, path) + + /** + * Models an informational message. The informational message includes the message text, contextData, + * a source and the possible path to the node in the flow graph which + * was being executed (i.e. the step where this failure occurred). + */ + final case class Information[+Data](message: String, contextData: Data, source: String, path: Option[NodePath] = None) + extends InstrumentationEvent + + /** + * Models a warning. The warning includes a message, contextData, a source and the possible path to the node + * in the flow graph which was being executed (i.e. the step where this failure occurred). + */ + final case class Warning[+Data](message: String, contextData: Data, source: String, path: Option[NodePath] = None) + extends InstrumentationEvent + + /** + * Models a trace message, which is often used for developer and diagnostic purposes. + * The trace message includes the message text, contextData, + * a source and the possible path to the node in the flow graph which + * was being executed (i.e. the step where this failure occurred). + */ + final case class Trace[+Data](message: String, contextData: Data, source: String, path: Option[NodePath] = None) + extends InstrumentationEvent + + final case class RunningStep(message: String, uid: StepUid, label: String, path: Option[NodePath] = None) + extends InstrumentationEvent + + /** + * A raw context free instrumentation event for logging a line of text. + */ + final case class LogLine(line: String) extends InstrumentationEvent + + /** + * Contains various log formats for `InstrumentationEvent`s. + */ + object logFormats { + + /** + * A log format for logging instrumentation events to the console with colored messages. + */ + val coloredConsoleLogFormat: LogFormat[InstrumentationEvent] = new LogFormat[InstrumentationEvent] { + val lineFormatter: LineFormatter = (_, s) => s + private val underlyingLogFormat = LogFormat.ColoredLogFormat(lineFormatter) + + def format(context: LogContext, line: InstrumentationEvent): String = + underlyingLogFormat.format(context, eventToLogLine(line)) + + def eventToLogLine(event: InstrumentationEvent): String = + //TODO: Do some appropriate formatting here + event.toString + } + + /** + * A log format for logging instrumentation events to the console. + */ + val simpleConsoleLogFormat: LogFormat[InstrumentationEvent] = new LogFormat[InstrumentationEvent] { + val lineFormatter: LineFormatter = (_, s) => s + private val underlyingLogFormat = LogFormat.SimpleConsoleLogFormat(lineFormatter) + + def format(context: LogContext, line: InstrumentationEvent): String = + underlyingLogFormat.format(context, eventToLogLine(line)) + + def eventToLogLine(event: InstrumentationEvent): String = + //TODO: Do some appropriate formatting here + event.toString + } + } +} diff --git a/morphir/flowz/src/morphir/flowz/instrumentation/InstrumentationLogging.scala b/morphir/flowz/src/morphir/flowz/instrumentation/InstrumentationLogging.scala new file mode 100644 index 00000000..83b124bd --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/instrumentation/InstrumentationLogging.scala @@ -0,0 +1,138 @@ +package morphir.flowz.instrumentation + +import zio.clock.Clock +import zio.console.Console +import zio.logging.Logger.LoggerWithFormat +import zio.logging.Logging.addTimestamp +import zio.logging.{ LogAnnotation, LogAppender, LogContext, LogFormat, LogLevel } +import zio.{ Cause, FiberRef, Has, Layer, URIO, ZIO, ZLayer } + +import java.nio.charset.{ Charset, StandardCharsets } +import java.nio.file.Path + +object InstrumentationLogging { + def console( + logLevel: LogLevel = LogLevel.Info, + format: LogFormat[InstrumentationEvent] = InstrumentationEvent.logFormats.coloredConsoleLogFormat + ): ZLayer[Console with Clock, Nothing, InstrumentationLogging] = + ZLayer.requires[Clock] ++ + LogAppender.console[InstrumentationEvent]( + logLevel, + format + ) >+> InstrumentationLogging.make >>> InstrumentationLogging.modifyLoggerM(addTimestamp[InstrumentationEvent]) + + def consoleErr( + logLevel: LogLevel = LogLevel.Info, + format: LogFormat[InstrumentationEvent] = InstrumentationEvent.logFormats.simpleConsoleLogFormat + ): ZLayer[Console with Clock, Nothing, InstrumentationLogging] = + ZLayer.requires[Clock] ++ + LogAppender.consoleErr[InstrumentationEvent]( + logLevel, + format + ) >+> make >>> modifyLoggerM(addTimestamp[InstrumentationEvent]) + + val context: URIO[InstrumentationLogging, LogContext] = + ZIO.accessM[InstrumentationLogging](_.get.logContext) + + def debug(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + ZIO.accessM[InstrumentationLogging](_.get.debug(event)) + + def derive(f: LogContext => LogContext): ZIO[InstrumentationLogging, Nothing, InstrumentationLogger] = + ZIO.access[InstrumentationLogging](_.get.derive(f)) + + def error(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + ZIO.accessM[InstrumentationLogging](_.get.error(event)) + + def error(event: => InstrumentationEvent, cause: Cause[Any]): ZIO[InstrumentationLogging, Nothing, Unit] = + ZIO.accessM[InstrumentationLogging](_.get.error(event, cause)) + + def file( + destination: Path, + charset: Charset = StandardCharsets.UTF_8, + autoFlushBatchSize: Int = 1, + bufferedIOSize: Option[Int] = None, + logLevel: LogLevel = LogLevel.Info, + format: LogFormat[InstrumentationEvent] = InstrumentationEvent.logFormats.simpleConsoleLogFormat + ): ZLayer[Console with Clock, Throwable, InstrumentationLogging] = + (ZLayer.requires[Clock] ++ + LogAppender + .file[InstrumentationEvent](destination, charset, autoFlushBatchSize, bufferedIOSize, format) + .map(appender => Has(appender.get.filter((ctx, _) => ctx.get(LogAnnotation.Level) >= logLevel))) + >+> make >>> modifyLoggerM(addTimestamp[InstrumentationEvent])) + + def fileAsync( + destination: Path, + charset: Charset = StandardCharsets.UTF_8, + autoFlushBatchSize: Int = 32, + bufferedIOSize: Option[Int] = Some(8192), + logLevel: LogLevel = LogLevel.Info, + format: LogFormat[InstrumentationEvent] = InstrumentationEvent.logFormats.simpleConsoleLogFormat + ): ZLayer[Console with Clock, Throwable, InstrumentationLogging] = + (ZLayer.requires[Clock] ++ + (LogAppender + .file[InstrumentationEvent](destination, charset, autoFlushBatchSize, bufferedIOSize, format) + .map(appender => Has(appender.get.filter((ctx, _) => ctx.get(LogAnnotation.Level) >= logLevel))) + >>> LogAppender.async(autoFlushBatchSize)) + >+> make >>> modifyLoggerM(addTimestamp[InstrumentationEvent])) + + val ignore: Layer[Nothing, InstrumentationLogging] = + LogAppender.ignore[InstrumentationEvent] >>> make + + def info(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + ZIO.accessM[InstrumentationLogging](_.get.info(event)) + + def log(level: LogLevel)(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + ZIO.accessM[InstrumentationLogging](_.get.log(level)(event)) + + def locally[A, R <: InstrumentationLogging, E, A1](fn: LogContext => LogContext)( + zio: ZIO[R, E, A1] + ): ZIO[InstrumentationLogging with R, E, A1] = + ZIO.accessM(_.get.locally(fn)(zio)) + + def locallyM[A, R <: InstrumentationLogging, E, A1]( + fn: LogContext => URIO[R, LogContext] + )(zio: ZIO[R, E, A1]): ZIO[InstrumentationLogging with R, E, A1] = + ZIO.accessM(_.get.locallyM(fn)(zio)) + + def make: ZLayer[InstrumentationAppender, Nothing, InstrumentationLogging] = + ZLayer.fromFunctionM((appender: InstrumentationAppender) => + FiberRef + .make(LogContext.empty) + .map { ref => + LoggerWithFormat(ref, appender.get) + } + ) + + def modifyLogger( + fn: InstrumentationLogger => InstrumentationLogger + ): ZLayer[InstrumentationLogging, Nothing, InstrumentationLogging] = + ZLayer.fromFunction[InstrumentationLogging, InstrumentationLogger](logging => fn(logging.get)) + + def modifyLoggerM[R, E]( + fn: InstrumentationLogger => ZIO[R, E, InstrumentationLogger] + ): ZLayer[InstrumentationLogging with R, E, InstrumentationLogging] = + ZLayer.fromFunctionM[InstrumentationLogging with R, E, InstrumentationLogger](logging => + fn(logging.get).provide(logging) + ) + + def throwable(event: => InstrumentationEvent, t: Throwable): ZIO[InstrumentationLogging, Nothing, Unit] = + ZIO.accessM[InstrumentationLogging](_.get.throwable(event, t)) + + def trace(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + ZIO.accessM[InstrumentationLogging](_.get.trace(event)) + + def warn(event: => InstrumentationEvent): ZIO[InstrumentationLogging, Nothing, Unit] = + ZIO.accessM[InstrumentationLogging](_.get.warn(event)) + + /** + * Adds root logger name + */ + def withRootLoggerName(name: String): ZLayer[InstrumentationLogging, Nothing, InstrumentationLogging] = + modifyLogger(_.named(name)) + + /** + * modify initial context + */ + def withContext(context: LogContext): ZLayer[InstrumentationLogging, Nothing, InstrumentationLogging] = + modifyLogger(_.derive(_ => context)) +} diff --git a/morphir/flowz/src/morphir/flowz/instrumentation/package.scala b/morphir/flowz/src/morphir/flowz/instrumentation/package.scala new file mode 100644 index 00000000..4c168e07 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/instrumentation/package.scala @@ -0,0 +1,10 @@ +package morphir.flowz + +import zio.Has +import zio.logging.{ Appender, Logger } + +package object instrumentation { + type InstrumentationAppender = Appender[InstrumentationEvent] + type InstrumentationLogger = Logger[InstrumentationEvent] + type InstrumentationLogging = Has[InstrumentationLogger] +} diff --git a/morphir/flowz/src/morphir/flowz/package.scala b/morphir/flowz/src/morphir/flowz/package.scala index e3d650bc..9e49b921 100644 --- a/morphir/flowz/src/morphir/flowz/package.scala +++ b/morphir/flowz/src/morphir/flowz/package.scala @@ -1,15 +1,18 @@ package morphir -import zio.Fiber +import morphir.flowz.experimental.{ ExecutedFlow, Flow } +import morphir.flowz.instrumentation.InstrumentationLogging +import morphir.flowz.experimental.instrumentor.Instrumentor +import zio._ +import zio.clock.Clock +import zio.duration.Duration import zio.prelude._ -package object flowz { - object api extends Api +import scala.collection.immutable.SortedSet - type Activity[-Env, -Params, +Err, +Value] = Step[Any, Value, Env, Params, Err, Value] - type IOStep[-Params, +Err, +Value] = Step[Any, Unit, Any, Params, Err, Value] - type TaskStep[-Params, +Value] = Step[Any, Unit, Any, Params, Throwable, Value] - type UStep[-Params, +Value] = Step[Any, Unit, Any, Params, Nothing, Value] +package object flowz { + type Properties = Has[Properties.Service] + type Annotated[+A] = (A, PropertyMap) object CommandLineArgs extends Subtype[List[String]] type CommandLineArgs = CommandLineArgs.Type @@ -17,10 +20,149 @@ package object flowz { object Variables extends Subtype[Map[String, String]] type Variables = Variables.Type - type FlowHostContext[+R] = (R, CommandLineArgs, Variables) + type ExecutableFlow[-InitialState, -InputMsg, -R, +E] = Flow[InitialState, Any, InputMsg, R, E, ExitCode] + type FlowArgs = Has[FlowArguments] + + type StepUidGenerator = uidGenerator.UidGenerator + type StepRuntimeEnv = InstrumentationLogging with StepUidGenerator with Clock + +// type ForkedStep[-StateIn, +StateOut, -Env, -Params, +Err, +Output] = +// Act[StateIn, Unit, Env, Params, Nothing, Fiber.Runtime[Err, StepOutputs[StateOut, Output]]] + + type ZBehavior[-SIn, +SOut, -InputMsg, -Env, +E, +A] = ZIO[(SIn, InputMsg, Env), E, StepSuccess[SOut, A]] + + type Activity[-SIn, +SOut, -Msg, -Env, +E, +A] = ZIO[SIn with Msg with Env, E, StepSuccess[SOut, A]] + + type StatelessStep[-InputMsg, -R, +E, +A] = Step[Any, Any, InputMsg, R, E, A] + + type ZIOStep[-R, +E, +A] = Step[Any, Any, Any, R, E, A] + + /** + * A type alias for a step that acts like an impure function, taking in an input message + * (also referred to as input/parameters) and produces a single value, possibly failing + * with a `Throwable`. + * + * For example: + * + * {{{ + * val intConverter:FuncStep[String,Int] = + * Step.fromFunction { numberStr:String => numberStr.toInt } + * }}} + */ + type FuncStep[-InputMsg, +A] = Step[Any, Any, InputMsg, Any, Throwable, A] + + /** + * Provides a description of an independent behavior which does not + * rely on any inputs to produce its outputs. + */ + type IndieStep[+S, +E, +A] = Step[Any, S, Any, Any, E, A] + + type StepUid = uidGenerator.Uid + +// def behavior[InputState, OutputState, Msg, R, Err, A]( +// f: (InputState, Msg) => ZIO[R, Err, (OutputState, A)] +// )(implicit ev: CanFail[Err]): Step[InputState, OutputState, Msg, R, Err, A] = +// Step[InputState, OutputState, Msg, R, Err, A](f) + +// def behavior[SIn, OutputState, Msg, R, E, A]( +// effect: ZIO[R with InputState[SIn], E, A] +// ): Step[SIn, OutputState, Msg, R, Nothing, A] = +// Step[SIn, OutputState, Msg, R, E, A](effect) + + def process[SIn, SOut, In, R, Err, Out](label: String)( + children: Flow[SIn, SOut, In, R, Err, Out]* + ): Flow[SIn, SOut, In, R, Err, Out] = + Flow.process(label, children = ZManaged.succeed(children.toVector)) + + def step[SIn, SOut, In, R, Err, Out](label: String)( + behavior: Step[SIn, SOut, In, R, Err, Out] + ): Flow[SIn, SOut, In, R, Err, Out] = Flow.step(label, behavior, PropertyMap.empty) + + /** + * The `Properties` trait provides access to a property map that flows and behaviors + * can add arbitrary properties to. Each property consists of a string + * identifier, an initial value, and a function for combining two values. + * Properties form monoids and you can think of `Properties` as a more + * structured logging service or as a super polymorphic version of the writer + * monad effect. + */ + object Properties { + + trait Service extends Serializable { + def addProperty[V](key: Property[V], value: V): UIO[Unit] + def get[V](key: Property[V]): UIO[V] + def withAnnotation[R, E, A](zio: ZIO[R, E, A]): ZIO[R, Annotated[E], Annotated[A]] + def supervisedFibers: UIO[SortedSet[Fiber.Runtime[Any, Any]]] + } + + /** + * Accesses a `Properties` instance in the environment and appends the + * specified property to the property map. + */ + def addProperty[V](key: Property[V], value: V): URIO[Properties, Unit] = + ZIO.accessM(_.get.addProperty(key, value)) + + /** + * Accesses a `Properties` instance in the environment and retrieves the + * property of the specified type, or its default value if there is none. + */ + def get[V](key: Property[V]): URIO[Properties, V] = + ZIO.accessM(_.get.get(key)) + + /** + * Constructs a new `Properties` service. + */ + val live: Layer[Nothing, Properties] = + ZLayer.fromEffect(FiberRef.make(PropertyMap.empty).map { fiberRef => + new Properties.Service { + def addProperty[V](key: Property[V], value: V): UIO[Unit] = + fiberRef.update(_.annotate(key, value)) + def get[V](key: Property[V]): UIO[V] = + fiberRef.get.map(_.get(key)) + def withAnnotation[R, E, A](zio: ZIO[R, E, A]): ZIO[R, Annotated[E], Annotated[A]] = + fiberRef.locally(PropertyMap.empty) { + zio.foldM(e => fiberRef.get.map((e, _)).flip, a => fiberRef.get.map((a, _))) + } + def supervisedFibers: UIO[SortedSet[Fiber.Runtime[Any, Any]]] = + ZIO.descriptorWith { descriptor => + get(Property.fibers).flatMap { + case Left(_) => + val emptySet = SortedSet.empty[Fiber.Runtime[Any, Any]] + ZIO.succeed(emptySet) + case Right(refs) => + ZIO + .foreach(refs)(_.get) + .map(_.foldLeft(SortedSet.empty[Fiber.Runtime[Any, Any]])(_ ++ _)) + .map(_.filter(_.id != descriptor.id)) + } + } + } + }) + + /** + * Accesses an `Properties` instance in the environment and executes the + * specified effect with an empty annotation map, returning the annotation + * map along with the result of execution. + */ + def withAnnotation[R <: Properties, E, A](zio: ZIO[R, E, A]): ZIO[R, Annotated[E], Annotated[A]] = + ZIO.accessM(_.get.withAnnotation(zio)) + + /** + * Returns a set of all fibers in this test. + */ + def supervisedFibers: ZIO[Properties, Nothing, SortedSet[Fiber.Runtime[Any, Any]]] = + ZIO.accessM(_.get.supervisedFibers) + } - type UFlowHost[+HostParams] = FlowHost[Any, Nothing, HostParams] + /** + * A `FlowReporter[E]` is capable of reporting flow execution results with error type `E`. + */ + type FlowReporter[-E] = (Duration, ExecutedFlow[E]) => URIO[Instrumentor, Unit] + object FlowReporter { - type ForkedStep[-StateIn, +StateOut, -Env, -Params, +Err, +Output] = - Step[StateIn, Unit, Env, Params, Nothing, Fiber.Runtime[Err, StepOutputs[StateOut, Output]]] + /** + * A `FlowReporter` that does nothing. + */ + val silent: FlowReporter[Any] = (_, _) => ZIO.unit + } } diff --git a/morphir/flowz/src/morphir/flowz/uidGenerator.scala b/morphir/flowz/src/morphir/flowz/uidGenerator.scala new file mode 100644 index 00000000..a2233f69 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/uidGenerator.scala @@ -0,0 +1,86 @@ +package morphir.flowz + +import zio._ +import zio.clock.Clock +import zio.stm.{ TMap, TRef, ZSTM } + +import java.time.Instant + +object uidGenerator { + type UidGenerator = Has[UidGenerator.Service] + + final case class Uid(namespace: String, timeStamp: Instant, seqNumber: Int) { + override def toString: String = s"$namespace:${timeStamp.toString}:${"%07d".format(seqNumber)}" + } + + def nextUid(namespace: String): URIO[UidGenerator, Uid] = + ZIO.accessM(_.get.nextUid(namespace)) + + def nextUid[Type](implicit tag: Tag[Type]): URIO[UidGenerator, Uid] = + ZIO.accessM(_.get.nextUid) + + object UidGenerator { + trait Service { + def nextUid(namespace: String): UIO[Uid] + final def nextUid[Type](implicit tag: Tag[Type]): UIO[Uid] = nextUid(tag.tag.shortName) + } + object Service { + private[uidGenerator] def live( + clock: Clock.Service, + instantRef: TRef[Instant], + counterMap: TMap[String, Int] + ): Service = + Live(clock, instantRef, counterMap) + private final case class Live(clock: Clock.Service, instantRef: TRef[Instant], counterMap: TMap[String, Int]) + extends Service { + def nextUid(namespace: String): UIO[Uid] = + for { + currentTs <- clock.instant + uid <- getNextUid(namespace, currentTs).commit + } yield uid + + private def getNextUid(namespace: String, currentTimestamp: Instant) = + for { + lastTs <- instantRef.get + _ <- + if (currentTimestamp.isAfter(lastTs)) instantRef.set(currentTimestamp) *> counterMap.put(namespace, 0) + else ZSTM.unit + seqNo <- counterMap.getOrElse(namespace, 0).map(_ + 1) + _ <- counterMap.put(namespace, seqNo) + } yield Uid(namespace, currentTimestamp, seqNo) + } + } + + val live: ZLayer[Clock, Nothing, UidGenerator] = ZLayer.fromServiceM { clock: Clock.Service => + for { + now <- clock.instant + instantRef <- TRef.makeCommit(now) + counterMap <- TMap.empty[String, Int].commit + } yield Service.live(clock, instantRef, counterMap) + } + } +} + +object UidGenDemo extends App { + import uidGenerator.Uid + final case class Foo(id: Uid, idx: Int = 0) + final case class Bar(id: Uid, idx: Int = 0) + + def run(args: List[String]): URIO[ZEnv, ExitCode] = + (for { + foos <- ZIO.collectAll( + NonEmptyChunk + .fromIterable(0, 1 to 1000) + .map((idx: Int) => uidGenerator.nextUid[Foo].map(uid => Foo(uid, idx))) + ) + foo1 <- uidGenerator.nextUid[Foo].map(Foo(_)) + foo2 <- uidGenerator.nextUid[Foo].map(Foo(_)) + bar1 <- uidGenerator.nextUid[Bar].map(Bar(_)) + bar2 <- uidGenerator.nextUid[Bar].map(Bar(_)) + _ <- console.putStrLn(s"Foo[1]: $foo1") + _ <- console.putStrLn(s"Foo[2]: $foo2") + _ <- console.putStrLn(s"Bar[1]: $bar1") + _ <- console.putStrLn(s"Bar[2]: $bar2") + _ <- ZIO.foreach(foos)(foo => console.putStrLn(s"Foo: $foo")) + } yield ExitCode.success).provideCustomLayer(uidGenerator.UidGenerator.live) +} diff --git a/morphir/flowz/test/src/morphir/flowz/AnnotationsSpec.scala b/morphir/flowz/test/src/morphir/flowz/AnnotationsSpec.scala new file mode 100644 index 00000000..d0d81b04 --- /dev/null +++ b/morphir/flowz/test/src/morphir/flowz/AnnotationsSpec.scala @@ -0,0 +1,26 @@ +package morphir.flowz + +import zio.test.Assertion._ +import zio.test.{ DefaultRunnableSpec, ZSpec, assert } +import zio.ZIO + +object AnnotationsSpec extends DefaultRunnableSpec { + def spec: ZSpec[Environment, Failure] = suite("annotationsSpec")( + testM("withAnnotation executes specified effect with an empty annotation map") { + for { + _ <- Properties.addProperty(count, 1) + a <- Properties.get(count) + map <- Properties.withAnnotation(ZIO.unit <* Properties.addProperty(count, 2)).map(_._2) + b = map.get(count) + } yield assert(a)(equalTo(1)) && assert(b)(equalTo(2)) + }, + testM("withAnnotation returns annotation map with result") { + for { + map <- Properties.withAnnotation(Properties.addProperty(count, 3) *> ZIO.fail("fail")).flip.map(_._2) + c = map.get(count) + } yield assert(c)(equalTo(3)) + } + ).provideCustomLayer(Properties.live) + + val count: Property[Int] = Property[Int]("count", 0, _ + _) +} diff --git a/morphir/flowz/test/src/morphir/flowz/FlowHostSpec.scala b/morphir/flowz/test/src/morphir/flowz/FlowHostSpec.scala deleted file mode 100644 index 60d7e6b4..00000000 --- a/morphir/flowz/test/src/morphir/flowz/FlowHostSpec.scala +++ /dev/null @@ -1,8 +0,0 @@ -package morphir.flowz - -import zio.test._ - -object FlowHostSpec extends DefaultRunnableSpec { - def spec = suite("FlowHost Spec")( - ) -} diff --git a/morphir/flowz/test/src/morphir/flowz/StepContextSpec.scala b/morphir/flowz/test/src/morphir/flowz/StepContextSpec.scala deleted file mode 100644 index a334f694..00000000 --- a/morphir/flowz/test/src/morphir/flowz/StepContextSpec.scala +++ /dev/null @@ -1,15 +0,0 @@ -package morphir.flowz - -import zio.test._ -import zio.test.Assertion._ -object StepContextSpec extends DefaultRunnableSpec { - def spec = suite("StepContext Spec")( - suite("When Constructing a StepContext")( - test("It should be possible to create one given only Params")( - assert(StepContext.fromParams(42))( - equalTo(StepContext(environment = (), inputs = StepInputs(state = (), params = 42))) - ) - ) - ) - ) -} diff --git a/morphir/flowz/test/src/morphir/flowz/StepSpec.scala b/morphir/flowz/test/src/morphir/flowz/StepSpec.scala index 84cdc2ca..320f27a6 100644 --- a/morphir/flowz/test/src/morphir/flowz/StepSpec.scala +++ b/morphir/flowz/test/src/morphir/flowz/StepSpec.scala @@ -1,111 +1,69 @@ package morphir.flowz +import morphir.flowz.instrumentation.InstrumentationLogging import zio.test._ import zio.test.Assertion._ -import zio.test.environment.TestSystem object StepSpec extends DefaultRunnableSpec { def spec = suite("Step Spec")( - suite("Constructing")( - testM("It should be possible to create a flow that always succeeds with the unit value")( + suite("When constructing a Step")( + testM("It should be possible to construct a Step that always succeeds with a given value.")( for { - output <- Step.unit.run - } yield assert(output)(equalTo(StepOutputs.unit)) + result <- Step.succeed(42).runResult + } yield assert(result)(equalTo(42)) ), - testM("It should be possible to create a flow that always succeeds with None")( + testM( + "It should be possible to construct a Step that always succeeds with a given value and honors the passed in state." + )( for { - output <- Step.none.run - } yield assert(output)(equalTo(StepOutputs.none)) + result <- Step.succeed(42).run(21, ()) + } yield assert(result)(equalTo(StepSuccess(21, 42))) ), - testM("It should be possible to create a flow that always succeeds with a value")( + testM("It should be possible to construct a Step that always fails with a given value")( for { - output <- Step.succeed(42).run - } yield assert(output)(equalTo(StepOutputs.fromValue(42))) + result <- Step.fail("Oops!").run.run + } yield assert(result)(fails(equalTo("Oops!"))) ), - testM("It should be possible to create a flow that always succeeds with the given output and state")( + testM("It should be possible to construct a Step that modifies its output given an initial state")( for { - actual <- Step.succeed(value = 42, state = "What is the answer?").run - } yield assert(actual)(equalTo(StepOutputs(state = "What is the answer?", value = 42))) + result <- Step.modify { text: String => s"$text:${text.size}" -> text.size }.run("Hello", ()) + } yield assert(result)(equalTo(StepSuccess("Hello:5", 5))) ), - testM("It should be possible to create a flow that always fails with a value")( + testM("It should be possible to construct a Step from a simple update function")( for { - result <- Step.fail("NO!!!").run.run - } yield assert(result)(fails(equalTo("NO!!!"))) + result <- + Step + .update[List[String], List[String], String, String] { case (initialState: List[String], msg: String) => + (msg :: initialState, msg.reverse) + } + .run(List("John", "Joe"), "Jack") + } yield assert(result)(equalTo(StepSuccess(state = List("Jack", "John", "Joe"), result = "kcaJ"))) ), - testM("It should be possible to create a flow that produces the value of executing a function")( - checkM(Gen.int(1, 5000)) { input => - for { - actual <- Step.fromFunction { n: Int => n * 2 }.run(input) - expected = StepOutputs.assignBoth(input * 2) - } yield assert(actual)(equalTo(expected)) - } - ), - testM("It should be possible to create a flow from an Option when the value is a Some")( + testM("It should be possible to construct a behavior that gets the initial state unchanged.")( for { - actual <- Step.fromOption(Some(Widget("sprocket"))).run.map(_.value) - } yield assert(actual)(equalTo(Widget("sprocket"))) + result <- Step.get[Set[Int]].run(Set(1, 2, 3, 4), Set(5, 6, 7, 8)) + } yield assert(result)(equalTo(StepSuccess(Set(1, 2, 3, 4), Set(1, 2, 3, 4)))) ), - testM("It should be possible to create a flow from an Option when the value is a None")( + testM("It should be possible to construct a behavior that sets the state to a value.")( + checkM(Gen.alphaNumericString, Gen.alphaNumericString) { (input, s1) => + for { + result <- Step.set(input).run(s1, "Something") + } yield assert(result)(equalTo(StepSuccess(state = input, result = ()))) + } + ) + ), + suite("Operations")( + testM("It should be possible to return a different constant value using as")( for { - actual <- Step.fromOption(None).run.map(_.value).run - } yield assert(actual)(fails(isNone)) + result <- Step.unit.as("Foo").run("S1", ()) + } yield assert(result)(equalTo(StepSuccess("S1", "Foo"))) ) ), suite("Combining")( - testM("It should be possible to combine steps using the >>> operator.") { - val start = Step.parameters[List[String]] - val next = Step.stateful((_: Any, args: List[String]) => (args, args.headOption)) - val myStep = start >>> next - assertM(myStep.run(List("Hello", "World")))( - equalTo(StepOutputs(state = List("Hello", "World"), value = Option("Hello"))) - ) - }, - testM("It should be possible to combine flows using zip") { - val flowA = Step.withStateAndValue("A") - val flowB = Step.withStateAndValue(1) - val flow: Step[Any, (String, Int), Any, Any, Nothing, (String, Int)] = flowA zip flowB - assertM(flow.run)(equalTo(StepOutputs(state = ("A", 1), value = ("A", 1)))) - }, - testM("It should be possible to combine flows using the zip operator <*>") { - val flowA = Step.withStateAndValue("A") - val flowB = Step.withStateAndValue(1) - val flow: Step[Any, (String, Int), Any, Any, Nothing, (String, Int)] = flowA <*> flowB - assertM(flow.run)(equalTo(StepOutputs(state = ("A", 1), value = ("A", 1)))) - }, testM("It should be possible to sequence flows using flatMap") { - val flow = Step.succeed("true").flatMap(value => Step.succeed(s"The answer is: $value")) - assertM(flow.run)(equalTo(StepOutputs.fromValue("The answer is: true"))) - }, - testM("It should be possible to sequence flows using a for comprehension") { - for { - _ <- TestSystem.putEnv("PROFILE", "local") - out <- (for { - cfg <- Step.succeed(Map("profile.local.host" -> "127.0.0.1", "profile.default.host" -> "finos.org")) - selectedProfile <- Step.fromEffect(zio.system.envOrElse("PROFILE", "default")) - host <- Step.succeed(cfg.getOrElse(s"profile.$selectedProfile.host", "morhir.org")) - } yield host).run.map(_.value) - } yield assert(out)(equalTo("127.0.0.1")) + val behavior = Step.succeed("true").flatMap(value => Step.succeed(s"The answer is: $value")).run(21, 21) + assertM(behavior)(equalTo(StepSuccess(21, "The answer is: true"))) } - ), - testM("It should be possible to rename a step without affecting its value") { - val theStep = Step.succeed("Good Boy!") - val named: Step[Any, Unit, Any, Any, Nothing, String] = Step.name("Praise")(theStep) - - for { - original <- theStep.run - actual <- named.run - } yield assert(actual)(equalTo(original)) - }, - testM("It should be possible to rename a step without affecting its value") { - val theStep = Step.succeed("Good Boy!") - val named: Step[Any, Unit, Any, Any, Nothing, String] = Step.name("Praise")(theStep) - - for { - original <- theStep.run - actual <- named.run - } yield assert(actual)(equalTo(original)) - } - ) - - final case class Widget(name: String) + ) + ).provideCustomLayer(StepUidGenerator.live ++ InstrumentationLogging.ignore) } diff --git a/morphir/flowz/test/src/morphir/flowz/sample/GreetingFlow.scala b/morphir/flowz/test/src/morphir/flowz/sample/GreetingFlow.scala index 21aa26d3..eb7cbd9d 100644 --- a/morphir/flowz/test/src/morphir/flowz/sample/GreetingFlow.scala +++ b/morphir/flowz/test/src/morphir/flowz/sample/GreetingFlow.scala @@ -1,23 +1,27 @@ package morphir.flowz.sample -import morphir.flowz.api.Step +import morphir.flowz.instrumentation.InstrumentationLogging +import morphir.flowz.{ FuncStep, StatelessStep, Step, StepUidGenerator } import zio._ +import zio.console.Console object GreetingFlow extends App { final case class Target(name: String) override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { - // Let's start with a step that gets the optional target - val getTarget = Step.fromFunction { args: List[String] => args.headOption.map(Target) } + // Let's start with a behavior that gets the optional target + val getTarget: FuncStep[List[String], Option[Target]] = Step.fromFunction { args: List[String] => + args.headOption.map(Target) + } - // Next let's construct a step that expects an optional target and prints a greeting to that target orF the world + // Next let's construct a behavior that expects an optional target and prints a greeting to that target orF the world // if no target is specified - val greeterStep = Step.fromEffect { greeting: Option[Target] => + val greeter: StatelessStep[Option[Target], Console, Nothing, Unit] = Step.stateless { greeting: Option[Target] => console.putStrLn(s"Hello, ${greeting getOrElse "world"}") } - val myFlow = getTarget >>> greeterStep + val myStep = getTarget >>> greeter - myFlow.run(args).exitCode + myStep.run(args).provideCustomLayer(StepUidGenerator.live ++ InstrumentationLogging.console()).exitCode } } diff --git a/morphir/flowz/test/src/morphir/flowz/sample/HelloWorld.scala b/morphir/flowz/test/src/morphir/flowz/sample/HelloWorld.scala index de00b9c0..466759c8 100644 --- a/morphir/flowz/test/src/morphir/flowz/sample/HelloWorld.scala +++ b/morphir/flowz/test/src/morphir/flowz/sample/HelloWorld.scala @@ -1,15 +1,20 @@ package morphir.flowz.sample -import morphir.flowz.api._ +import morphir.flowz.Step +import morphir.flowz.StepUidGenerator +import morphir.flowz.instrumentation.InstrumentationLogging import zio._ object HelloWorld extends App { override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { - val helloStep = Step.fromEffect { greeting: Option[String] => + val helloBehavior = Step.stateless { greeting: Option[String] => console.putStrLn(s"Hello, ${greeting.getOrElse("world")}") } - helloStep.run(args.headOption).exitCode + helloBehavior + .run(args.headOption) + .provideCustomLayer(StepUidGenerator.live ++ InstrumentationLogging.console()) + .exitCode } } diff --git a/morphir/flowz/test/src/morphir/flowz/sample/SummingFlow.scala b/morphir/flowz/test/src/morphir/flowz/sample/SummingFlow.scala index ab72f8d1..f7b3a804 100644 --- a/morphir/flowz/test/src/morphir/flowz/sample/SummingFlow.scala +++ b/morphir/flowz/test/src/morphir/flowz/sample/SummingFlow.scala @@ -1,18 +1,17 @@ -package morphir.flowz.sample - -import morphir.flowz.ContextSetup -import morphir.flowz.api.{ Step, flow } -import zio._ - -object SummingFlow extends App { - def run(args: List[String]): URIO[ZEnv, ExitCode] = - flow( - "sum-flow", - setup = ContextSetup.uses[console.Console].derivesParamsWith((items: List[Int]) => items) - ) - .stages(Step.fromFunction { items: List[Int] => items.sum }) - .report(sum => console.putStrLn(s"Sum: $sum")) - .build - .run(List(1, 2, 3)) - .exitCode -} +//package morphir.flowz.sample +// +//import morphir.flowz.ContextSetup +//import zio._ +// +//object SummingFlow extends App { +// def run(args: List[String]): URIO[ZEnv, ExitCode] = +// flow( +// "sum-flow", +// setup = ContextSetup.uses[console.Console].derivesParamsWith((items: List[Int]) => items) +// ) +// .stages(Step.fromFunction { items: List[Int] => items.sum }) +// .report(sum => console.putStrLn(s"Sum: $sum")) +// .build +// .run(List(1, 2, 3)) +// .exitCode +//} diff --git a/morphir/flowz/test/src/morphir/flowz/sample/SummingFlowWithEffectfulSetup.scala b/morphir/flowz/test/src/morphir/flowz/sample/SummingFlowWithEffectfulSetup.scala index 41209981..f1c66b59 100644 --- a/morphir/flowz/test/src/morphir/flowz/sample/SummingFlowWithEffectfulSetup.scala +++ b/morphir/flowz/test/src/morphir/flowz/sample/SummingFlowWithEffectfulSetup.scala @@ -1,20 +1,20 @@ -package morphir.flowz.sample - -import morphir.flowz.ContextSetup -import morphir.flowz.api._ -import zio._ - -object SummingFlowWithEffectfulSetup extends App { - def run(args: List[String]): URIO[ZEnv, ExitCode] = - flow( - "sum-flow", - setup = ContextSetup.uses[console.Console].extractParamsWith { args: List[String] => - ZIO.collectAllSuccesses(args.map(input => ZIO.effect(input.toInt))) - } - ) - .stages(Step.fromFunction { items: List[Int] => items.sum }) - .build - .run(List("1", "2", "3", "Four", "5")) - .flatMap(sum => console.putStrLn(s"Sum: $sum")) - .exitCode -} +//package morphir.flowz.sample +// +//import morphir.flowz.ContextSetup +//import morphir.flowz.api._ +//import zio._ +// +//object SummingFlowWithEffectfulSetup extends App { +// def run(args: List[String]): URIO[ZEnv, ExitCode] = +// flow( +// "sum-flow", +// setup = ContextSetup.uses[console.Console].extractParamsWith { args: List[String] => +// ZIO.collectAllSuccesses(args.map(input => ZIO.effect(input.toInt))) +// } +// ) +// .stages(Step.fromFunction { items: List[Int] => items.sum }) +// .build +// .run(List("1", "2", "3", "Four", "5")) +// .flatMap(sum => console.putStrLn(s"Sum: $sum")) +// .exitCode +//} diff --git a/morphir/ir/src/morphir/ir/recursions.scala b/morphir/ir/src/morphir/ir/recursions.scala new file mode 100644 index 00000000..80c70b30 --- /dev/null +++ b/morphir/ir/src/morphir/ir/recursions.scala @@ -0,0 +1,71 @@ +package morphir.ir + +/** + * In look at how we can potentially model the Morphir IR, research has shown that recursIt woulkd + */ +object recursions { +// type Type attribution +// = Variable a Name +// | Reference a FQName (List (Type a)) +// | Tuple a (List (Type a)) +// | Record a (List (Field a)) +// | ExtensibleRecord a Name (List (Field a)) +// | Function a (Type a) (Type a) +// | Unit a +// + +// +// type alias Field a = +// { name : Name +// , tpe : Type a +// } +// + + type Name = String + type FQNAme = String + + final case class Field[+Self](name: Name, value: Self) + + sealed trait TypeCase[+Self, +Attrib] { + def map[Self2](fn: Self => Self2): TypeCase[Self2, Attrib] = ??? + } + object TypeCase { + final case class Variable[Attrib](a: Attrib) extends TypeCase[Nothing, Attrib] + final case class Reference[Self, Attrib](attribute: Attrib, name: FQName, types: List[Self]) + extends TypeCase[Self, Attrib] + final case class Tuple[Self, Attrib](attribute: Attrib, types: List[Self]) extends TypeCase[Self, Attrib] + final case class Record[Self, Attrib](attributes: Attrib, fields: List[Field[Self]]) extends TypeCase[Self, Attrib] + final case class ExtensibleRecord[Self, Attrib](attributes: Attrib, name: Name, fields: List[Field[Self]]) + extends TypeCase[Self, Attrib] + final case class Function[Self, Attrib](attribute: Attrib, input: Self, output: Self) extends TypeCase[Self, Attrib] + final case class Unit[+Attrib](attribute: Attrib) extends TypeCase[Nothing, Attrib] + } + + final case class Type[+Attrib](value: TypeCase[Type[Attrib], Attrib]) { self => + def fold[Z](f: TypeCase[Z, Attrib] => Z): Z = + f(value.map(_.fold(f))) + + /** + * Transform the whole tree. + * Top down + */ + def transformDown[Attrib2](f: TypeMapper[Attrib, Attrib2]): Type[Attrib2] = + Type(f(value).map(_.transformDown(f))) + + /** + * Bottom up + */ + def transformUp[Attrib2](f: TypeMapper[Attrib, Attrib2]): Type[Attrib2] = + Type(f(value.map(_.transformUp(f)))) + + } + + object Type { + def unfold[Z, Attrib](initial: Z)(f: Z => TypeCase[Z, Attrib]): Type[Attrib] = + Type(f(initial).map(unfold(_)(f))) + } + + trait TypeMapper[-AttribIn, +AttribOut] { + def apply[Self](value: TypeCase[Self, AttribIn]): TypeCase[Self, AttribOut] + } +} diff --git a/morphir/sdk/core/test/src/morphir/sdk/ExampleSpec.scala b/morphir/sdk/core/test/src/morphir/sdk/ExampleSpec.scala new file mode 100644 index 00000000..3d4dbd60 --- /dev/null +++ b/morphir/sdk/core/test/src/morphir/sdk/ExampleSpec.scala @@ -0,0 +1,33 @@ +package morphir.sdk + +import zio.test._ +import zio.test.Assertion._ +import zio.test.AssertionM.Render.param + +object ExampleSpec extends DefaultRunnableSpec { + + def spec = suite("Example Spec")( + test("matching elements")( + assert(List(1, 2, 3, 4))(hasMatchingElements(List(1, 2, 3, 4))(Assertion.assertionDirect("elementsMatch")() { + case (actual, expected) => Assertion.equalTo(expected).apply(actual) + })) + ) +// testM("matching elements")( +// for { +// actual <- ZIO.succeed(List(2, 4, 6, 8)) +// expected <- ZIO.succeed(List(1, 2, 3, 4)) +// pairs = actual.zip(expected) +// } yield assert(pairs)(forall(Assertion.assertionDirect("elementsMatch")() { case (actual, expected) => +// Assertion.equalTo(expected).apply(actual) +// })) +// ) + ) + + def hasMatchingElements[A](elements: Iterable[A])(assertion: Assertion[(A, A)]): Assertion[Iterable[A]] = + Assertion.assertionRec[Iterable[A], (A, A)]("hasMatchingElements")(param(elements), param(assertion))(assertion)( + (actual) => + if (actual.size != elements.size) None + else actual.zip(elements).find { case (l, r) => assertion.run((l, r)).isFailure }, + _.asSuccess + ) && hasSize(equalTo(elements.size)) +} diff --git a/morphir/sdk/core/test/src/morphir/sdk/RuleSpec.scala b/morphir/sdk/core/test/src/morphir/sdk/RuleSpec.scala index 8c7ab3f6..ac40e21d 100644 --- a/morphir/sdk/core/test/src/morphir/sdk/RuleSpec.scala +++ b/morphir/sdk/core/test/src/morphir/sdk/RuleSpec.scala @@ -17,7 +17,7 @@ limitations under the License. package morphir.sdk import zio.test.Assertion._ -import zio.test.{ suite, _ } +import zio.test._ object RuleSpec extends DefaultRunnableSpec { def spec = suite("RuleSpec")(