From b009404e625dc15cd6ea2614996e4a8ead17cb1f Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 27 Jan 2021 23:35:37 -0500 Subject: [PATCH] SDK and Flowz updates (#83) * Work on the Activity type and improve sharing of Step constructors * Fix unsound operations and allow specifying a reporting phase * Change name of the output phase to be report * Improve numeric support in the SDK * Refine flowz * Try and fix issues * Fix warnings * Fix issues caused by moving the location of the unit val * #65 Add the Set module to the SDK * #65 Fix Set::size issue and add missing functions from morphir.sdk.Int * Remove zio-prelude from being a dependency for morphir-sdk-core * Number type support and inclusion --- build.sc | 6 +- morphir/flowz/src/morphir/flowz/Context.scala | 13 ++ .../src/morphir/flowz/ContextSetup.scala | 199 ++++++++++++++++++ morphir/flowz/src/morphir/flowz/FlowDsl.scala | 191 ++++++++++------- .../flowz/src/morphir/flowz/FlowInfo.scala | 3 + .../flowz/src/morphir/flowz/StepContext.scala | 18 +- .../flowz/src/morphir/flowz/StepInfo.scala | 3 + .../src/morphir/flowz/StepVisibility.scala | 13 ++ morphir/flowz/src/morphir/flowz/TODO.md | 55 +++++ .../morphir/flowz/eventing/EventChannel.scala | 51 +++++ .../flowz/eventing/publishing/eventBus.scala | 3 - .../src/morphir/flowz/StepContextSpec.scala | 15 ++ .../morphir/flowz/sample/GreetingFlow.scala | 2 +- .../morphir/flowz/sample/SummingFlow.scala | 11 +- .../SummingFlowWithEffectfulSetup.scala | 13 +- morphir/sdk/core/src/morphir/sdk/Basics.scala | 53 +++-- morphir/sdk/core/src/morphir/sdk/Bool.scala | 18 ++ .../sdk/core/src/morphir/sdk/Decimal.scala | 130 ++++++++++++ morphir/sdk/core/src/morphir/sdk/Float.scala | 10 + morphir/sdk/core/src/morphir/sdk/Int.scala | 83 +++++++- morphir/sdk/core/src/morphir/sdk/Rule.scala | 9 +- morphir/sdk/core/src/morphir/sdk/Set.scala | 116 ++++++++++ morphir/sdk/core/src/morphir/sdk/String.scala | 20 +- .../test/src/morphir/sdk/BasicsSpec.scala | 14 ++ .../core/test/src/morphir/sdk/IntSpec.scala | 18 +- 25 files changed, 920 insertions(+), 147 deletions(-) create mode 100644 morphir/flowz/src/morphir/flowz/Context.scala create mode 100644 morphir/flowz/src/morphir/flowz/ContextSetup.scala create mode 100644 morphir/flowz/src/morphir/flowz/FlowInfo.scala create mode 100644 morphir/flowz/src/morphir/flowz/StepInfo.scala create mode 100644 morphir/flowz/src/morphir/flowz/StepVisibility.scala create mode 100644 morphir/flowz/src/morphir/flowz/TODO.md create mode 100644 morphir/flowz/src/morphir/flowz/eventing/EventChannel.scala delete mode 100644 morphir/flowz/src/morphir/flowz/eventing/publishing/eventBus.scala create mode 100644 morphir/flowz/test/src/morphir/flowz/StepContextSpec.scala create mode 100644 morphir/sdk/core/src/morphir/sdk/Decimal.scala create mode 100644 morphir/sdk/core/src/morphir/sdk/Float.scala create mode 100644 morphir/sdk/core/src/morphir/sdk/Set.scala diff --git a/build.sc b/build.sc index ad631076..961ec64c 100644 --- a/build.sc +++ b/build.sc @@ -31,9 +31,9 @@ object Deps { val silencer = "1.7.1" val scalaCollectionsCompat = "2.3.1" - val zio = "1.0.3" - val zioConfig = "1.0.0-RC30-1" - val zioLogging = "0.5.4" + val zio = "1.0.4" + val zioConfig = "1.0.0-RC32" + val zioLogging = "0.5.5" val zioNio = "1.0.0-RC10" val zioPrelude = "1.0.0-RC1" val zioProcess = "0.2.0" diff --git a/morphir/flowz/src/morphir/flowz/Context.scala b/morphir/flowz/src/morphir/flowz/Context.scala new file mode 100644 index 00000000..7be388ed --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/Context.scala @@ -0,0 +1,13 @@ +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 new file mode 100644 index 00000000..5f8b2dc6 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/ContextSetup.scala @@ -0,0 +1,199 @@ +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/FlowDsl.scala b/morphir/flowz/src/morphir/flowz/FlowDsl.scala index 0a0fbef7..610dec77 100644 --- a/morphir/flowz/src/morphir/flowz/FlowDsl.scala +++ b/morphir/flowz/src/morphir/flowz/FlowDsl.scala @@ -1,10 +1,13 @@ package morphir.flowz import zio._ - +import com.github.ghik.silencer.silent trait FlowDsl { - def flow(name: String): Flow.Builder[Any, Any, Nothing, Nothing, Nothing, Nothing, Any] = - Flow.builder(name) + 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 @@ -28,97 +31,135 @@ trait FlowDsl { type Params = Params0 } - def builder(name: String): Builder[Any, Any, Nothing, Nothing, Nothing, Nothing, Any] = - apply(name) + 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 Phases { + object BuilderPhase { type Setup type DefineStages + type Report } - def apply(name: String): Builder[Any, Any, Nothing, Nothing, Nothing, Nothing, Any] = - Builder(name) - - sealed trait Builder[-StartupEnv, -Input, +Env, +State, +Params, +Output, Phase] { self => - - def setup[Input1 <: Input, Env1 >: Env, State1 >: State, Params1 >: Params]( - setupFunc: Input1 => StepContext[Env1, State1, Params1] - ): Builder[StartupEnv, Input1, Env1, State1, Params1, Output, Phase with Phases.Setup] - - def setupWithEffect[StartupEnv1 <: StartupEnv, Input1 <: Input, Env1 >: Env, State1 >: State, Params1 >: Params]( - effectualSetupFunc: Input1 => RIO[StartupEnv1, StepContext[Env1, State1, Params1]] - ): Builder[StartupEnv1, Input1, Env1, State1, Params1, Output, Phase with Phases.Setup] - - def stages[Input1 <: Input, Env1 >: Env, State1 >: State, Params1 >: Params, Output1]( - step: Step[State1, _, Env1, Params1, Throwable, Output1] - ): Builder[StartupEnv, Input1, Env1, State1, Params1, Output1, Phase with Phases.DefineStages] + 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 <:< Phases.Setup with Phases.DefineStages + 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(name: String): Builder[Any, Any, Nothing, Nothing, Nothing, Nothing, Any] = - FlowBuilder(name, None, None) - - private sealed case class FlowBuilder[StartupEnv, Input, Env0, State0, Params0, +Output, Phase]( + 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, - setupEffect: Option[RIO[(StartupEnv, Input), StepContext[Env0, State0, Params0]]], - step: Option[Step[State0, _, Env0, Params0, Throwable, Output]] - ) extends Builder[StartupEnv, Input, Env0, State0, Params0, Output, Phase] { self => + 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 <:< Phases.Setup with Phases.DefineStages - ): Flow[StartupEnv, Input, Output] = + 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 = State0 + type InitialState = StateIn type Params = Params0 def name: String = self.name def context(input: Input): RIO[StartupEnv, StepContext[Env, InitialState, Params]] = - self.setupEffect.get.provideSome[StartupEnv](startupEnv => (startupEnv, input)) + self.contextSetup.makeContext(input) - def step: Step[InitialState, _, Env, Params, Throwable, Output] = self.step.get + def step: Step[InitialState, _, Env, Params, Throwable, Output] = self.step.get.tap { case (_, output) => + self.report.fold[ZIO[Env, Throwable, Any]](ZIO.unit)(_(output)) + } } - - def setup[Input1 <: Input, Env1 >: Env0, State1 >: State0, Params1 >: Params0]( - setupFunc: Input1 => StepContext[Env1, State1, Params1] - ): Builder[StartupEnv, Input1, Env1, State1, Params1, Output, Phase with Phases.Setup] = { - val setupEffect = ZIO.environment[(StartupEnv, Input1)].mapEffect { case (_, input) => setupFunc(input) } - FlowBuilder[StartupEnv, Input1, Env1, State1, Params1, Output, Phase with Phases.Setup]( - name = self.name, - setupEffect = Some(setupEffect), - step = self.step.map(_.asInstanceOf[Step[State1, _, Env1, Params1, Throwable, Output]]) - ) } - def setupWithEffect[ - StartupEnv1 <: StartupEnv, - Input1 <: Input, - Env1 >: Env0, - State1 >: State0, - Params1 >: Params0 - ]( - effectualSetupFunc: Input1 => RIO[StartupEnv1, StepContext[Env1, State1, Params1]] - ): Builder[StartupEnv1, Input1, Env1, State1, Params1, Output, Phase with Phases.Setup] = { - val setupEffect = ZIO.environment[(StartupEnv1, Input1)].flatMap { case (startupEnv, input) => - effectualSetupFunc(input).provide(startupEnv) - } - FlowBuilder[StartupEnv1, Input1, Env1, State1, Params1, Output, Phase with Phases.Setup]( - name = self.name, - setupEffect = Some(setupEffect), - step = self.step.map(_.asInstanceOf[Step[State1, _, Env1, Params1, Throwable, Output]]) - ) - } - - def stages[Input1 <: Input, Env1 >: Env0, State1 >: State0, Params1 >: Params0, Output1]( - step: Step[State1, _, Env1, Params1, Throwable, Output1] - ): Builder[StartupEnv, Input1, Env1, State1, Params1, Output1, Phase with Phases.DefineStages] = - copy(step = Some(step)) - } } @@ -130,13 +171,17 @@ object demo extends zio.App { import morphir.flowz.api._ override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = - flow("sum-flow") - .setup((args: List[Int]) => StepContext.fromParams(args)) + 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)) - .flatMap(res => console.putStrLn(s"Result: $res")) + .run(List("1", "2", "3")) .exitCode } diff --git a/morphir/flowz/src/morphir/flowz/FlowInfo.scala b/morphir/flowz/src/morphir/flowz/FlowInfo.scala new file mode 100644 index 00000000..afa77cc2 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/FlowInfo.scala @@ -0,0 +1,3 @@ +package morphir.flowz + +final case class FlowInfo(name: Option[String], description: Option[String]) diff --git a/morphir/flowz/src/morphir/flowz/StepContext.scala b/morphir/flowz/src/morphir/flowz/StepContext.scala index d258fbdf..03911897 100644 --- a/morphir/flowz/src/morphir/flowz/StepContext.scala +++ b/morphir/flowz/src/morphir/flowz/StepContext.scala @@ -8,21 +8,21 @@ final case class StepContext[+Env, +State, +Params](environment: Env, inputs: St 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 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) - def fromEnvironment[Env](environment: Env): StepContext[Env, Any, Any] = StepContext(environment, inputs = StepInputs.AnyInputs) @@ -32,6 +32,12 @@ object StepContext { 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 { /** diff --git a/morphir/flowz/src/morphir/flowz/StepInfo.scala b/morphir/flowz/src/morphir/flowz/StepInfo.scala new file mode 100644 index 00000000..dac6ac70 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/StepInfo.scala @@ -0,0 +1,3 @@ +package morphir.flowz + +final case class StepInfo(name: Option[String], description: Option[String], visibility: StepVisibility) diff --git a/morphir/flowz/src/morphir/flowz/StepVisibility.scala b/morphir/flowz/src/morphir/flowz/StepVisibility.scala new file mode 100644 index 00000000..a7babee0 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/StepVisibility.scala @@ -0,0 +1,13 @@ +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 new file mode 100644 index 00000000..35c307a2 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/TODO.md @@ -0,0 +1,55 @@ +## 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/eventing/EventChannel.scala b/morphir/flowz/src/morphir/flowz/eventing/EventChannel.scala new file mode 100644 index 00000000..7a145e99 --- /dev/null +++ b/morphir/flowz/src/morphir/flowz/eventing/EventChannel.scala @@ -0,0 +1,51 @@ +package morphir.flowz.eventing + +import zio._ +import zio.stm._ +import zio.stream._ + +trait EventChannel[Msg] { + def publish(message: Msg): UIO[Unit] + def subscribe: Stream[Nothing, Msg] +} + +object EventChannel { + def bounded[Msg](n: Int): UIO[EventChannel[Msg]] = + (for { + writeIndex <- TRef.make(0) + subscriberProgress <- TMap.make[Int, Int]() + subscriberIdGen <- TRef.make(0) + buffer <- TArray.make(List.fill[Option[Msg]](n)(None): _*) + } yield new EventChannel[Msg] { + def publish(message: Msg): UIO[Unit] = + (for { + writeIdx <- writeIndex.get + slowestReaderIdx <- subscriberProgress.fold(-1) { case (idx, (_, subscriberIdx)) => + if (idx < 0) subscriberIdx + else + idx.min(subscriberIdx) + } + _ <- STM.check((writeIdx - slowestReaderIdx) < n) + _ <- buffer.update(writeIdx % n, _ => Some(message)) + _ <- writeIndex.update(_ + 1) + } yield ()).commit + + def subscribe: Stream[Nothing, Msg] = + Stream.unwrap( + for { + subscriberId <- subscriberIdGen.updateAndGet(_ + 1).commit // Generate a unique subscriber id + writeIdx <- writeIndex.get.tap(idx => subscriberProgress.put(subscriberId, idx)).commit + readIndex <- TRef.make(writeIdx).commit + } yield Stream + .repeatEffect((for { + readIdx <- readIndex.get + writeIdx <- writeIndex.get + _ <- STM.check(writeIdx - readIdx > 0) // Make sure something is available to read + message <- buffer(readIdx % n).map(_.get) + _ <- readIndex.updateAndGet(_ + 1).flatMap(idx => subscriberProgress.put(subscriberId, idx)) + } yield message).commit) + .ensuring(subscriberProgress.delete(subscriberId).commit) + ) + + }).commit +} diff --git a/morphir/flowz/src/morphir/flowz/eventing/publishing/eventBus.scala b/morphir/flowz/src/morphir/flowz/eventing/publishing/eventBus.scala deleted file mode 100644 index c8cb925e..00000000 --- a/morphir/flowz/src/morphir/flowz/eventing/publishing/eventBus.scala +++ /dev/null @@ -1,3 +0,0 @@ -package morphir.flowz.eventing.publishing - -trait eventBus {} diff --git a/morphir/flowz/test/src/morphir/flowz/StepContextSpec.scala b/morphir/flowz/test/src/morphir/flowz/StepContextSpec.scala new file mode 100644 index 00000000..a334f694 --- /dev/null +++ b/morphir/flowz/test/src/morphir/flowz/StepContextSpec.scala @@ -0,0 +1,15 @@ +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/sample/GreetingFlow.scala b/morphir/flowz/test/src/morphir/flowz/sample/GreetingFlow.scala index be33dbc3..21aa26d3 100644 --- a/morphir/flowz/test/src/morphir/flowz/sample/GreetingFlow.scala +++ b/morphir/flowz/test/src/morphir/flowz/sample/GreetingFlow.scala @@ -10,7 +10,7 @@ object GreetingFlow extends App { // Let's start with a step that gets the optional target val getTarget = 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 or the world + // Next let's construct a step 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] => console.putStrLn(s"Hello, ${greeting getOrElse "world"}") diff --git a/morphir/flowz/test/src/morphir/flowz/sample/SummingFlow.scala b/morphir/flowz/test/src/morphir/flowz/sample/SummingFlow.scala index a3eecb3a..ab72f8d1 100644 --- a/morphir/flowz/test/src/morphir/flowz/sample/SummingFlow.scala +++ b/morphir/flowz/test/src/morphir/flowz/sample/SummingFlow.scala @@ -1,15 +1,18 @@ package morphir.flowz.sample -import morphir.flowz.api.{ Step, StepContext, flow } +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(StepContext.fromParams[List[Int]]) + 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)) - .flatMap(sum => console.putStrLn(s"Sum: $sum")) .exitCode } diff --git a/morphir/flowz/test/src/morphir/flowz/sample/SummingFlowWithEffectfulSetup.scala b/morphir/flowz/test/src/morphir/flowz/sample/SummingFlowWithEffectfulSetup.scala index 6278687d..41209981 100644 --- a/morphir/flowz/test/src/morphir/flowz/sample/SummingFlowWithEffectfulSetup.scala +++ b/morphir/flowz/test/src/morphir/flowz/sample/SummingFlowWithEffectfulSetup.scala @@ -1,16 +1,17 @@ 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").setupWithEffect { args: List[String] => - val parsedItems = args.map(input => ZIO.effect(input.toInt)) - ZIO - .collectAllSuccesses(parsedItems) - .map(StepContext.fromParams[List[Int]]) - } + 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")) diff --git a/morphir/sdk/core/src/morphir/sdk/Basics.scala b/morphir/sdk/core/src/morphir/sdk/Basics.scala index f86190cc..2f156737 100644 --- a/morphir/sdk/core/src/morphir/sdk/Basics.scala +++ b/morphir/sdk/core/src/morphir/sdk/Basics.scala @@ -15,34 +15,44 @@ limitations under the License. */ package morphir.sdk +import morphir.sdk.{ Bool => BoolModule } object Basics { + sealed abstract class Order(val value: Int) extends Product with Serializable + object Order { + case object LT extends Order(-1) + case object EQ extends Order(0) + case object GT extends Order(1) + } + + val LT: Order = Order.LT + val EQ: Order = Order.EQ + val GT: Order = Order.GT + // Bool - type Bool = scala.Boolean - @inline def not(a: Bool): Bool = !a - @inline def and(a: Bool)(b: Bool): Bool = a && b - @inline def or(a: Bool)(b: Bool): Bool = a || b - @inline def xor(a: Bool)(b: Bool): Bool = (a && !b) || (!a && b) + type Bool = BoolModule.Bool + val Bool: BoolModule.Bool.type = BoolModule.Bool + @inline def not(a: Bool): Bool = BoolModule.not(a) + @inline def and(a: Bool)(b: Bool): Bool = BoolModule.and(a)(b) + @inline def or(a: Bool)(b: Bool): Bool = BoolModule.or(a)(b) + @inline def xor(a: Bool)(b: Bool): Bool = BoolModule.xor(a)(b) // Equality - @inline def equal[A](a: A)(b: A): Bool = a == b - @inline def notEqual[A](a: A)(b: A): Bool = a != b + @inline def equal[A](a: A)(b: A): Bool = BoolModule.equal(a)(b) + @inline def notEqual[A](a: A)(b: A): Bool = BoolModule.notEqual(a)(b) // Comparable - def lessThan[A: Ordering](a: A)(b: A): Bool = implicitly[Ordering[A]].lt(a, b) - def lessThanOrEqual[A: Ordering](a: A)(b: A): Bool = - implicitly[Ordering[A]].lteq(a, b) - def greaterThan[A: Ordering](a: A)(b: A): Bool = - implicitly[Ordering[A]].gt(a, b) - def greaterThanOrEqual[A: Ordering](a: A)(b: A): Bool = - implicitly[Ordering[A]].gteq(a, b) - def min[A: Ordering](a: A)(b: A): A = if (lessThan(a)(b)) a else b - def max[A: Ordering](a: A)(b: A): A = if (greaterThan(a)(b)) a else b + @inline def lessThan[A: Ordering](a: A)(b: A): Bool = BoolModule.lessThan(a)(b) + @inline def lessThanOrEqual[A: Ordering](a: A)(b: A): Bool = BoolModule.lessThanOrEqual(a)(b) + @inline def greaterThan[A: Ordering](a: A)(b: A): Bool = BoolModule.greaterThan(a)(b) + @inline def greaterThanOrEqual[A: Ordering](a: A)(b: A): Bool = BoolModule.greaterThanOrEqual(a)(b) + @inline def min[A: Ordering](a: A)(b: A): A = BoolModule.min(a)(b) + @inline def max[A: Ordering](a: A)(b: A): A = BoolModule.max(a)(b) // Int construction - type Int = scala.Long - def Int(v: scala.Long): Int = v + type Int = morphir.sdk.Int.Int + val Int: morphir.sdk.Int.Int.type = morphir.sdk.Int.Int // Int functions @inline def lessThan(a: Int)(b: Int): Bool = a < b @@ -66,7 +76,7 @@ object Basics { else a // Float construction - type Float = scala.Double + type Float = morphir.sdk.Float.Float @inline def Float(number: Number): Float = number.doubleValue() @@ -95,6 +105,9 @@ object Basics { @inline def isNaN(a: Float): Bool = a.isNaN @inline def isInfinite(a: Float): Bool = a.isInfinite + type Decimal = morphir.sdk.Decimal.Decimal + val Decimal: morphir.sdk.Decimal.Decimal.type = morphir.sdk.Decimal.Decimal + // Utilities @inline def identity[A](a: A): A = scala.Predef.identity(a) @inline def always[A, B](a: A): B => A = _ => a @@ -102,6 +115,4 @@ object Basics { @inline def composeRight[A, B, C](f: A => B)(g: B => C): A => C = a => g(f(a)) def never[A](nothing: Nothing): A = nothing - type Decimal = scala.BigDecimal - } diff --git a/morphir/sdk/core/src/morphir/sdk/Bool.scala b/morphir/sdk/core/src/morphir/sdk/Bool.scala index d6683051..ab8c1241 100644 --- a/morphir/sdk/core/src/morphir/sdk/Bool.scala +++ b/morphir/sdk/core/src/morphir/sdk/Bool.scala @@ -20,6 +20,9 @@ import morphir.sdk.String.String object Bool { type Bool = Boolean + object Bool { + @inline def apply(value: Boolean): Bool = value + } val True: Bool = true val False: Bool = false @@ -29,4 +32,19 @@ object Bool { @inline def or(a: Bool)(b: Bool): Bool = a || b @inline def xor(a: Bool)(b: Bool): Bool = a ^ b @inline def toString(value: Bool): String = value.toString + + // Equality + @inline def equal[A](a: A)(b: A): Bool = a == b + @inline def notEqual[A](a: A)(b: A): Bool = a != b + + // Comparable + def lessThan[A: Ordering](a: A)(b: A): Bool = implicitly[Ordering[A]].lt(a, b) + def lessThanOrEqual[A: Ordering](a: A)(b: A): Bool = + implicitly[Ordering[A]].lteq(a, b) + def greaterThan[A: Ordering](a: A)(b: A): Bool = + implicitly[Ordering[A]].gt(a, b) + def greaterThanOrEqual[A: Ordering](a: A)(b: A): Bool = + implicitly[Ordering[A]].gteq(a, b) + def min[A: Ordering](a: A)(b: A): A = if (lessThan(a)(b)) a else b + def max[A: Ordering](a: A)(b: A): A = if (greaterThan(a)(b)) a else b } diff --git a/morphir/sdk/core/src/morphir/sdk/Decimal.scala b/morphir/sdk/core/src/morphir/sdk/Decimal.scala new file mode 100644 index 00000000..2231ce5f --- /dev/null +++ b/morphir/sdk/core/src/morphir/sdk/Decimal.scala @@ -0,0 +1,130 @@ +package morphir.sdk +import morphir.sdk.Maybe.Maybe +import morphir.sdk.Basics.Order + +import java.math.{ BigDecimal => BigDec, RoundingMode } +import scala.util.control.NonFatal + +object Decimal { + + type Decimal = BigDec + + object Decimal { + def apply(value: BigDec): Decimal = value + def apply(value: scala.BigDecimal): Decimal = value.bigDecimal + def apply(value: morphir.sdk.Float.Float): Decimal = BigDecimal.exact(value).bigDecimal + def apply(value: morphir.sdk.Int.Int): Decimal = BigDecimal.exact(value).bigDecimal + + } + + /** + * Absolute value (sets the sign as positive) + */ + def abs(value: Decimal): Decimal = value.abs() + + def add(a: Decimal)(b: Decimal): Decimal = a.add(b) + + def bps(n: morphir.sdk.Int.Int): Decimal = Decimal(n * 0.0001) + + def compare(a: Decimal)(b: Decimal): Order = + a.compareTo(b) match { + case 0 => Order.EQ + case n if n > 0 => Order.GT + case _ => Order.LT + } + + def div(a: Decimal)(b: Decimal): Maybe[Decimal] = + if (b.compareTo(zero) == 0) Maybe.nothing + else + try { + Maybe.just(a.divide(b)) + } catch { + case NonFatal(_) => Maybe.nothing + } + + def divWithDefault(default: Decimal)(a: Decimal)(b: Decimal): Decimal = + Maybe.withDefault(default)(div(a)(b)) + + def eq(a: Decimal)(b: Decimal): morphir.sdk.Bool.Bool = a.compareTo(b) == 0 + + def fromInt(value: morphir.sdk.Basics.Int): Decimal = + Decimal(value) + + /** + * Converts a Float to a Decimal + */ + def fromFloat(value: morphir.sdk.Float.Float): Decimal = + Decimal(value) + + def fromString(value: morphir.sdk.String.String): Maybe[Decimal] = + try { + Maybe.just(new BigDec(value)) + } catch { + case NonFatal(_) => Maybe.nothing + } + + /** + * Converts an `Int` to a `Decimal`` that represents n hundreds. + */ + def hundred(n: morphir.sdk.Int.Int): Decimal = + Decimal(n * 100) + + def hundredth(n: morphir.sdk.Int.Int): Decimal = + Decimal(n * 0.01) + + def gt(a: Decimal)(b: Decimal): morphir.sdk.Bool.Bool = a.compareTo(b) > 0 + def gte(a: Decimal)(b: Decimal): morphir.sdk.Bool.Bool = a.compareTo(b) >= 1 + + def lt(a: Decimal)(b: Decimal): morphir.sdk.Bool.Bool = a.compareTo(b) < 0 + + def million(n: morphir.sdk.Int.Int): Decimal = + Decimal(n * 1000000) + + def millionth(n: morphir.sdk.Int.Int): Decimal = + Decimal(n * 0.000001) + + def mul(a: Decimal)(b: Decimal): Decimal = a.multiply(b) + + @inline def ne(a: Decimal)(b: Decimal): morphir.sdk.Bool.Bool = neq(a)(b) + def neq(a: Decimal)(b: Decimal): morphir.sdk.Bool.Bool = a.compareTo(b) != 0 + + def negate(value: Decimal): Decimal = value.negate() + + def round(decimal: Decimal): Decimal = { + val scale = decimal.scale() + decimal.setScale(scale, RoundingMode.HALF_EVEN) + } + + def shiftDecimalLeft(n: morphir.sdk.Int.Int)(value: Decimal): Decimal = + value.scaleByPowerOfTen(-n.intValue()) //TODO: When we align Int to Int this should settle in correctly + + def shiftDecimalRight(n: morphir.sdk.Int.Int)(value: Decimal): Decimal = + value.scaleByPowerOfTen(n.intValue()) //TODO: When we align Int to Int this should settle in correctly + + def sub(a: Decimal)(b: Decimal): Decimal = a.subtract(b) + + def thousand(n: morphir.sdk.Int.Int): Decimal = + Decimal(n * 1000) + + def toFloat(value: Decimal): morphir.sdk.Float.Float = + morphir.sdk.Float.Float(value.doubleValue()) + + //TODO: Make sure the Elm call and this call return the same value + def toString(value: Decimal): morphir.sdk.String.String = value.toString + + def truncate(decimal: Decimal): Decimal = { + // Since morphir's Int is actually a Long this isn't really safe + val scale = decimal.scale() + decimal.setScale(scale, RoundingMode.DOWN) + } + + /** + * The number -1. + */ + val minusOne: Decimal = BigDecimal.exact(-1).bigDecimal + + val one: Decimal = BigDec.ONE + + val zero: Decimal = BigDec.ZERO + +} diff --git a/morphir/sdk/core/src/morphir/sdk/Float.scala b/morphir/sdk/core/src/morphir/sdk/Float.scala new file mode 100644 index 00000000..10430c5e --- /dev/null +++ b/morphir/sdk/core/src/morphir/sdk/Float.scala @@ -0,0 +1,10 @@ +package morphir.sdk + +object Float { + type Float = scala.Double + object Float { + def apply(value: java.lang.Number): Float = value.doubleValue() + def apply(value: scala.Double): Float = value + def apply(value: scala.Float): Float = value.doubleValue() + } +} diff --git a/morphir/sdk/core/src/morphir/sdk/Int.scala b/morphir/sdk/core/src/morphir/sdk/Int.scala index ca9b742d..368466c7 100644 --- a/morphir/sdk/core/src/morphir/sdk/Int.scala +++ b/morphir/sdk/core/src/morphir/sdk/Int.scala @@ -17,13 +17,41 @@ limitations under the License. package morphir.sdk object Int { - type Int = scala.BigInt - type Int8 = scala.Byte + type Int = scala.Long + val Int: scala.Long.type = scala.Long + + private[Int] type IntCompanion = scala.Long.type + private[Int] val IntCompanion: IntCompanion = scala.Long + + /** + * Represents an 8 bit integer value. + */ + type Int8 = scala.Byte + val Int8: scala.Byte.type = scala.Byte + + /** + * Represents a 16 bit integer value. + */ type Int16 = scala.Short + val Int16: scala.Short.type = scala.Short + + /** + * Represents a 32 bit integer value. + */ type Int32 = scala.Int + val Int32: scala.Int.type = scala.Int + + /** + * Represents a 64 bit integer value. + */ type Int64 = scala.Long + val Int64: scala.Long.type = scala.Long + + def apply(value: scala.Byte): Int = value.longValue() + def apply(value: scala.Short): Int = value.longValue() + def apply(value: scala.Int): Int = value.longValue() + def apply(value: scala.Long): Int = value.longValue() - @inline def divide(dividend: Int)(divisor: Int): Int = dividend / divisor @inline def divide(dividend: Int8)(divisor: Int8): Int8 = (dividend / divisor).toByte @inline def divide(dividend: Int16)(divisor: Int16): Int16 = @@ -33,7 +61,6 @@ object Int { @inline def divide(dividend: Int64)(divisor: Int64): Int64 = dividend / divisor - @inline def modBy(divisor: Int)(dividend: Int): Int = (dividend % divisor).abs @inline def modBy(divisor: Int8)(dividend: Int8): Int8 = (dividend % divisor).toByte.abs @inline def modBy(divisor: Int16)(dividend: Int16): Int16 = @@ -43,7 +70,6 @@ object Int { @inline def modBy(divisor: Int64)(dividend: Int64): Int64 = (dividend % divisor).abs - @inline def remainderBy(divisor: Int)(dividend: Int): Int = dividend % divisor @inline def remainderBy(divisor: Int8)(dividend: Int8): Int8 = (dividend % divisor).toByte @inline def remainderBy(divisor: Int16)(dividend: Int16): Int16 = @@ -53,4 +79,51 @@ object Int { @inline def remainderBy(divisor: Int64)(dividend: Int64): Int64 = dividend % divisor + /** + * Turn an 8 bit integer value into an arbitrary precision integer to use in calculations. + */ + def fromInt8(int: Int8): Basics.Int = Basics.Int(int) + + def toInt8(int: Basics.Int): Maybe.Maybe[Int8] = + if (int < Int8.MinValue && int > Int8.MaxValue) + Maybe.nothing + else + Maybe.just(int.byteValue()) + + def fromInt16(int: Int16): Basics.Int = Basics.Int(int) + + def toInt16(int: Basics.Int): Maybe.Maybe[Int16] = + if (int < Int16.MinValue && int > Int16.MaxValue) + Maybe.nothing + else + Maybe.just(int.shortValue()) + + def fromInt32(int: Int32): Basics.Int = Basics.Int(int) + + def toInt32(int: Basics.Int): Maybe.Maybe[Int32] = + if (int < Int32.MinValue && int > Int32.MaxValue) + Maybe.nothing + else + Maybe.just(int.intValue()) + + /** + * Turn a 64 bit integer value into a arbitrary precision integer to use in calculations. + */ + def fromInt64(int: Int64): Basics.Int = int + + /** + * Turns an arbitrary precision integer into a 64 bit integer if it fits within the precision. + */ + def toInt64(int: Basics.Int): Maybe.Maybe[Basics.Int] = + if (int < Int64.MinValue && int > Int64.MaxValue) + Maybe.nothing + else + Maybe.just(int.longValue()) + + private implicit class RichIntCompanion(private val _self: IntCompanion) extends AnyVal { + def apply(value: scala.Byte): Int = value.longValue() + def apply(value: scala.Short): Int = value.longValue() + def apply(value: scala.Long): Int = value.longValue() + def apply(value: scala.Int): Int = value.longValue() + } } diff --git a/morphir/sdk/core/src/morphir/sdk/Rule.scala b/morphir/sdk/core/src/morphir/sdk/Rule.scala index 6c12c3dc..9cd08912 100644 --- a/morphir/sdk/core/src/morphir/sdk/Rule.scala +++ b/morphir/sdk/core/src/morphir/sdk/Rule.scala @@ -16,7 +16,6 @@ limitations under the License. package morphir.sdk -import morphir.sdk.Bool.Bool import morphir.sdk.List.List import morphir.sdk.Maybe.Maybe @@ -30,16 +29,16 @@ object Rule { .find(rule => rule(input).isDefined) .flatMap(rule => rule(input)) - def any[A]: A => Bool = + def any[A]: A => Bool.Bool = _ => Bool.True - def is[A](ref: A)(input: A): Bool = + def is[A](ref: A)(input: A): Bool.Bool = ref == input - def anyOf[A](ref: List[A])(input: A): Bool = + def anyOf[A](ref: List[A])(input: A): Bool.Bool = ref.contains(input) - def noneOf[A](ref: List[A])(input: A): Bool = + def noneOf[A](ref: List[A])(input: A): Bool.Bool = !anyOf(ref)(input) } diff --git a/morphir/sdk/core/src/morphir/sdk/Set.scala b/morphir/sdk/core/src/morphir/sdk/Set.scala new file mode 100644 index 00000000..3f381f69 --- /dev/null +++ b/morphir/sdk/core/src/morphir/sdk/Set.scala @@ -0,0 +1,116 @@ +/* +Copyright 2021 Morgan Stanley + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + */ +package morphir.sdk + +object Set { + type Set[A] = scala.collection.immutable.Set[A] + private val Set = scala.collection.immutable.Set + + /** + * Create an empty set. + */ + @inline def empty[A]: Set[A] = + Set.empty[A] + + /** + * Create a set with one value. + */ + @inline def singleton[A](value: A): Set[A] = Set(value) + + /** + * Insert a value into a set. + */ + @inline def insert[A](value: A)(set: Set[A]): Set[A] = set + value + + /** + * Remove a value from a set. If the value is not found, no changes are made. + */ + @inline def remove[A](value: A)(set: Set[A]): Set[A] = set - value + + /** + * Determine if a set is empty. + */ + @inline def isEmpty[A](set: Set[A]): Bool.Bool = set.isEmpty + + /** + * Determine if a value is in a set. + */ + @inline def member[A](value: A)(set: Set[A]): Bool.Bool = set.contains(value) + + /** + * Determine the number of elements in a set. + */ + def size[A](set: Set[A]): morphir.sdk.Basics.Int = Int(set.size) + + /** + * Get the union of two sets. Keep all values. + */ + @inline def union[A](a: Set[A])(b: Set[A]): Set[A] = a union b + + /** + * Get the intersection of two sets. Keep all the values. + */ + @inline def intersect[A](a: Set[A])(b: Set[A]): Set[A] = a intersect b + + /** + * Get the difference between the first set and the second. Keeps values that do not appear in the second set. + */ + @inline def diff[A](a: Set[A])(b: Set[A]): Set[A] = a diff b + + /** + * Convert a set into a list, sorted from lowest to highest. + */ + def toList[A: Ordering](set: Set[A]): morphir.sdk.List.List[A] = set.toList.sorted + + /** + * Convert a list into a set, removing any duplicates. + */ + @inline def fromList[A](list: morphir.sdk.List.List[A]): Set[A] = list.toSet + + /** + * Map a function onto a set, creating a new set with no duplicates. + */ + @inline def map[A, B](fn: A => B)(set: Set[A]): Set[B] = set.map(fn) + + /** + * Fold over the values in a set, in order from left to right. + */ + def foldl[A, B](f: A => B => B)(initial: => B)(set: Set[A]): B = { + def fn(b: B, a: A): B = f(a)(b) + set.foldLeft(initial)(fn) + } + + /** + * Fold over the values in a set, in order from right to left. + */ + def foldr[A, B](f: A => B => B)(initial: => B)(set: Set[A]): B = { + def fn(a: A, b: B): B = f(a)(b) + set.foldRight(initial)(fn) + } + + /** + * Only keep elements that pass the given test. + */ + def filter[A](predicate: A => Bool.Bool)(set: Set[A]): Set[A] = + set.filter(predicate) + + /** + * Create tow new sets. The first contains all the elements that passed the given test, + * and the second contains all the elements that did not. + */ + def partition[A](predicate: A => Bool.Bool)(set: Set[A]): (Set[A], Set[A]) = + set.partition(predicate) +} diff --git a/morphir/sdk/core/src/morphir/sdk/String.scala b/morphir/sdk/core/src/morphir/sdk/String.scala index 360d6ef4..dbc303ef 100644 --- a/morphir/sdk/core/src/morphir/sdk/String.scala +++ b/morphir/sdk/core/src/morphir/sdk/String.scala @@ -16,14 +16,13 @@ limitations under the License. package morphir.sdk -import morphir.sdk.Basics.{ Bool, Float } import morphir.sdk.Char.Char import morphir.sdk.Maybe.Maybe object String { type String = scala.Predef.String - @inline def isEmpty(str: String): Bool = str.isEmpty() + @inline def isEmpty(str: String): Basics.Bool = str.isEmpty() @inline def length(str: String): Basics.Int = str.length().toLong @@ -93,12 +92,12 @@ object String { def dropRight(n: Basics.Int)(str: String): String = str.dropRight(n.toInt) - def contains(substring: String)(str: String): Bool = str.contains(substring) + def contains(substring: String)(str: String): Basics.Bool = str.contains(substring) - def startsWith(substring: String)(str: String): Bool = + def startsWith(substring: String)(str: String): Basics.Bool = str.startsWith(substring) - def endsWith(substring: String)(str: String): Bool = str.endsWith(substring) + def endsWith(substring: String)(str: String): Basics.Bool = str.endsWith(substring) def indexes(substring: String)(str: String): List[Basics.Int] = str.r.findAllMatchIn(substring).map(_.start.toLong).toList @@ -106,13 +105,14 @@ object String { def indices(substring: String)(str: String): List[Basics.Int] = indexes(substring)(str) - def toFloat(str: String): Maybe[Float] = + def toFloat(str: String): Maybe[morphir.sdk.Float.Float] = try Maybe.just(str.toDouble) catch { case _: NumberFormatException => Maybe.nothing } - def fromFloat(float: Float): String = float.toString + def fromFloat(float: morphir.sdk.Float.Float): String = float.toString + def fromFloat(float: scala.Float): String = float.toString def fromChar(ch: Char): String = ch.toString @@ -148,7 +148,7 @@ object String { def map(f: Char => Char)(str: String): String = str.toList.map(ch => f(Char.from(ch))).mkString - def filter(f: Char => Bool)(str: String): String = + def filter(f: Char => Basics.Bool)(str: String): String = str.toList.filter(ch => f(Char.from(ch))).mkString def foldl[B](f: Char => B => B)(z: B)(str: String): B = @@ -157,10 +157,10 @@ object String { def foldr[B](f: Char => B => B)(z: B)(str: String): B = str.toList.foldRight(z)((next, soFar) => f(Char.from(next))(soFar)) - def any(f: (Char => Bool))(str: String): Bool = + def any(f: (Char => Basics.Bool))(str: String): Basics.Bool = str.toList.exists(ch => f(Char.from(ch))) - def all(f: (Char => Bool))(str: String): Bool = + def all(f: (Char => Basics.Bool))(str: String): Basics.Bool = str.toList.forall(ch => f(Char.from(ch))) implicit class StringOps(private val self: String) extends AnyVal { diff --git a/morphir/sdk/core/test/src/morphir/sdk/BasicsSpec.scala b/morphir/sdk/core/test/src/morphir/sdk/BasicsSpec.scala index ae225875..89c66058 100644 --- a/morphir/sdk/core/test/src/morphir/sdk/BasicsSpec.scala +++ b/morphir/sdk/core/test/src/morphir/sdk/BasicsSpec.scala @@ -162,6 +162,20 @@ object BasicsSpec extends DefaultRunnableSpec { assert(Basics.lessThanOrEqual(d1)(d1))(equalTo(expected)) } } + ), + suite("BoolSpec")( + test("Bool xor - true xor true")( + assert(Basics.xor(Basics.Bool(true))(Basics.Bool(true)))(isFalse) + ), + test("Bool xor - true xor false")( + assert(Basics.xor(Basics.Bool(true))(Basics.Bool(false)))(isTrue) + ), + test("Bool xor - false xor true")( + assert(Basics.xor(Basics.Bool(false))(Basics.Bool(true)))(isTrue) + ), + test("Bool xor - false xor false")( + assert(Basics.xor(Basics.Bool(false))(Basics.Bool(false)))(isFalse) + ) ) ) } diff --git a/morphir/sdk/core/test/src/morphir/sdk/IntSpec.scala b/morphir/sdk/core/test/src/morphir/sdk/IntSpec.scala index 83654da9..0cd6f36c 100644 --- a/morphir/sdk/core/test/src/morphir/sdk/IntSpec.scala +++ b/morphir/sdk/core/test/src/morphir/sdk/IntSpec.scala @@ -50,10 +50,8 @@ object IntSpec extends DefaultRunnableSpec { }, testM("Dividing an Int value by an Int value") { check(Gen.anyLong, Gen.anyLong.filter(n => n != 0)) { (x: Int64, y: Int64) => - val bigX = BigInt(x) - val bigY = BigInt(y) - val expected: BigInt = bigX / bigY - assert(sdk.Int.divide(bigX)(bigY))(equalTo(expected)) + val expected: Long = x / y + assert(sdk.Int.divide(x)(y))(equalTo(expected)) } } ), @@ -84,9 +82,9 @@ object IntSpec extends DefaultRunnableSpec { }, testM("Performing ModBy on Ints") { check(Gen.anyLong.filter(n => n != 0), Gen.anyLong) { (longDivisor, longDividend) => - val divisor = BigInt(longDivisor) - val dividend = BigInt(longDividend) - val expected: scala.BigInt = (dividend % divisor).abs + val divisor = sdk.Int.fromInt64(longDivisor) + val dividend = sdk.Int.fromInt64(longDividend) + val expected: Int = (dividend % divisor).abs assert(sdk.Int.modBy(divisor)(dividend))(equalTo(expected)) } } @@ -122,9 +120,9 @@ object IntSpec extends DefaultRunnableSpec { }, testM("Performing remainderBy on Ints") { check(Gen.anyLong.filter(n => n != 0), Gen.anyLong) { (longDivisor, longDividend) => - val divisor = BigInt(longDivisor) - val dividend = BigInt(longDividend) - val expected: scala.BigInt = dividend % divisor + val divisor = sdk.Int.fromInt64(longDivisor) + val dividend = sdk.Int.fromInt64(longDividend) + val expected: Int = dividend % divisor assert(sdk.Int.remainderBy(divisor)(dividend))(equalTo(expected)) } }