From d2ebe2268c0e5c7fc076ff19a5e775b3e91bcbbf Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Tue, 28 Apr 2020 14:20:14 -0400 Subject: [PATCH 1/5] Working on loading up a workspace --- .github/workflows/publish.yml | 1 + .gitmodules | 3 + build.sbt | 99 ++++++++++++++----- .../src/main/org}/morphir/Main.scala | 8 +- .../src/main/org}/morphir/cli/Cli.scala | 2 +- .../main/org}/morphir/cli/CommandLine.scala | 2 +- .../src/main/org}/morphir/cli/package.scala | 2 +- .../main/org}/morphir/runtime/ExitCode.scala | 0 .../morphir/core/newtype/NewtypeModule.scala | 62 ++++++++++++ .../org/morphir/core/newtype/package.scala | 3 + .../org/morphir/ir/AccessControlled.scala | 2 + .../org/morphir/sdk/NonEmptyListModule.scala | 22 +++++ .../services/files/FilesModule.scala | 9 ++ .../services/files/FilesModule.scala | 11 +++ morphir/toolbox/modules/morphir-elm | 1 + .../toolbox/binding/FrontendBinding.scala | 19 ++++ .../binding/elm/ElmFrontendBinding.scala | 3 + .../org/morphir/toolbox/core/Errors.scala | 21 ++++ .../morphir/toolbox/core/ManifestFile.scala | 68 +++++++++++++ .../org/morphir/toolbox/core/Project.scala | 16 +++ .../morphir/toolbox/core/ProjectName.scala | 21 ++++ .../morphir/toolbox/core/ProjectPath.scala | 26 +++++ .../org/morphir/toolbox/core/SourceFile.scala | 5 + .../org/morphir/toolbox/core/Workspace.scala | 59 +++++++++++ .../morphir/toolbox/core/WorkspaceDir.scala | 39 ++++++++ .../morphir/toolbox/core/WorkspacePath.scala | 37 +++++++ .../toolbox/core/codecs/TomlCodec.scala | 37 +++++++ .../toolbox/core/codecs/TomlSupport.scala | 29 ++++++ .../org/morphir/toolbox/core/package.scala | 7 ++ .../workspace/config/BindingSettings.scala | 14 +++ .../workspace/config/BindingsSettings.scala | 7 ++ .../workspace/config/ProjectSettings.scala | 9 ++ .../workspace/config/WorkspaceSettings.scala | 14 +++ .../toolbox/workspace/config/package.scala | 5 + .../morphir/toolbox/workspace/package.scala | 26 +++++ .../toolbox/core/ProjectPathSpec.scala | 22 +++++ .../config/WorkspaceSettingsSpec.scala | 36 +++++++ project/metals.sbt | 2 +- project/plugins.sbt | 4 +- 39 files changed, 716 insertions(+), 37 deletions(-) create mode 100644 .gitmodules rename morphir/cli/{src => shared/src/main/org}/morphir/Main.scala (94%) rename morphir/cli/{src => shared/src/main/org}/morphir/cli/Cli.scala (98%) rename morphir/cli/{src => shared/src/main/org}/morphir/cli/CommandLine.scala (98%) rename morphir/cli/{src => shared/src/main/org}/morphir/cli/package.scala (96%) rename morphir/cli/{src => shared/src/main/org}/morphir/runtime/ExitCode.scala (100%) create mode 100644 morphir/core/shared/src/main/scala/org/morphir/core/newtype/NewtypeModule.scala create mode 100644 morphir/core/shared/src/main/scala/org/morphir/core/newtype/package.scala create mode 100644 morphir/sdk/core/shared/src/main/scala/org/morphir/sdk/NonEmptyListModule.scala create mode 100644 morphir/toolbox/js/src/main/scala/org/morphir/toolbox/workspace/services/files/FilesModule.scala create mode 100644 morphir/toolbox/jvm/src/main/scala/org/morphir/toolbox/workspace/services/files/FilesModule.scala create mode 160000 morphir/toolbox/modules/morphir-elm create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/FrontendBinding.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmFrontendBinding.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Errors.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ManifestFile.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Project.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectName.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectPath.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/SourceFile.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspacePath.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlCodec.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlSupport.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/package.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingSettings.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingsSettings.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/ProjectSettings.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/WorkspaceSettings.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/package.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/package.scala create mode 100644 morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/core/ProjectPathSpec.scala create mode 100644 morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/workspace/config/WorkspaceSettingsSpec.scala diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bcdcbcf1..7afc4932 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,6 +18,7 @@ jobs: with: java-version: 1.8 + - name: Write - name: Cache SBT ivy cache uses: actions/cache@v1 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..8e939a99 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "morphir/bindings/elm/morphir-elm"] + path = morphir/toolbox/modules/morphir-elm + url = https://github.com/Morgan-Stanley/morphir-elm.git diff --git a/build.sbt b/build.sbt index 51aeaf35..d29a00da 100644 --- a/build.sbt +++ b/build.sbt @@ -50,25 +50,43 @@ lazy val root = project ) ) .aggregate( + morphirCoreJS, + morphirCoreJVM, morphirSdkCoreJS, - morphirSdkCoreJVM + morphirSdkCoreJVM, //morphirSdkJsonJS, //morphirSdkJsonJVM, // morphirIRCoreJS, // morphirIRCoreJVM, -// morphirCliJS, -// morphirCliJVM + morphirToolboxJS, + morphirToolboxJVM, + morphirCliJVM +// morphirBindingElmJS, +// morphirBindingElmJVM ) +lazy val morphirCore = crossProject(JSPlatform, JVMPlatform) + .in(file("morphir/core")) + .settings(stdSettings("morphir-core")) + .settings(crossProjectSettings) + .settings(buildInfoSettings("morphir.core")) + .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) + +lazy val morphirCoreJS = morphirCore.js + .settings(testJsSettings) + +lazy val morphirCoreJVM = morphirCore.jvm + .settings(dottySettings) + lazy val morphirSdkCore = crossProject(JSPlatform, JVMPlatform) .in(file("morphir/sdk/core")) + .dependsOn(morphirCore) .settings(stdSettings("morphir-sdk-core")) .settings(crossProjectSettings) .settings(buildInfoSettings("morphir.sdk.core")) .settings( libraryDependencies ++= Seq( "dev.zio" %%% "zio" % Versions.zio, - //"io.estatico" %%% "newtype" % "0.4.3", "dev.zio" %%% "zio-test" % Versions.zio % "test", "dev.zio" %%% "zio-test-sbt" % Versions.zio % "test" ) @@ -106,7 +124,7 @@ lazy val morphirSdkCoreJVM = morphirSdkCore.jvm //lazy val morphirIRCore = crossProject(JSPlatform, JVMPlatform) // .in(file("morphir/ir/core")) // .dependsOn(morphirSdkCore) -// .settings(stdSettings("morphirIRCore")) +// .settings(stdSettings("morphir-ir-core")) // .settings(crossProjectSettings) // .settings(buildInfoSettings("morphir.ir.core")) // .settings( @@ -121,31 +139,60 @@ lazy val morphirSdkCoreJVM = morphirSdkCore.jvm // .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) // //lazy val morphirIRCoreJS = morphirIRCore.js +// .settings(testJsSettings) // //lazy val morphirIRCoreJVM = morphirIRCore.jvm // .settings(dottySettings) // .settings(zioNioSettings("1.0.0-RC6")) -// -//lazy val morphirCli = crossProject(JSPlatform, JVMPlatform) -// .in(file("morphir/cli")) -// .dependsOn(morphirIRCore, morphirSdkCore) -// .settings(stdSettings("morphirCli")) -// .settings(crossProjectSettings) -// .settings(buildInfoSettings("morphir.cli")) -// .settings( -// libraryDependencies ++= Seq( -// "dev.zio" %% "zio" % Versions.zio, -// "dev.zio" %% "zio-test" % Versions.zio % "test", -// "dev.zio" %% "zio-test-sbt" % Versions.zio % "test" -// ) -// ) -// .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) -// -//lazy val morphirCliJS = morphirCli.js -// .settings(scalaJSUseMainModuleInitializer := true) -// -//lazy val morphirCliJVM = morphirCli.jvm -// .settings(dottySettings) + +lazy val morphirToolbox = crossProject(JSPlatform, JVMPlatform) + .in(file("morphir/toolbox")) + .dependsOn(morphirCore) + .settings(stdSettings("morphir-toolbox")) + .settings(crossProjectSettings) + .settings(buildInfoSettings("org.morphir.toolbox")) + .settings( + libraryDependencies ++= Seq( + "dev.zio" %%% "zio-streams" % Versions.zio, + "io.circe" %%% "circe-generic" % Versions.circe, + "io.circe" %%% "circe-parser" % Versions.circe, + "dev.zio" %%% "zio-test" % Versions.zio % "test", + "dev.zio" %%% "zio-test-sbt" % Versions.zio % "test", + "tech.sparse" %%% "toml-scala" % "0.2.2" + ) + ) + .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) + +lazy val morphirToolboxJS = morphirToolbox.js + .settings(testJsSettings) + .settings(zioNioSettings("1.0.0-RC6")) + +lazy val morphirToolboxJVM = morphirToolbox.jvm + .settings(dottySettings) + .settings(zioNioSettings("1.0.0-RC6")) + .settings( + libraryDependencies ++= Seq( + "io.github.soc" % "directories" % "11" + ) + ) + +lazy val morphirCli = crossProject(JVMPlatform) + .in(file("morphir/cli")) + .dependsOn(morphirToolbox) + .settings(stdSettings("morphir-cli")) + .settings(crossProjectSettings) + .settings(buildInfoSettings("org.morphir.cli")) + .settings( + libraryDependencies ++= Seq( + "dev.zio" %% "zio" % Versions.zio, + "dev.zio" %% "zio-test" % Versions.zio % "test", + "dev.zio" %% "zio-test-sbt" % Versions.zio % "test" + ) + ) + .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) + +lazy val morphirCliJVM = morphirCli.jvm + .settings(dottySettings) lazy val docs = project .in(file("morphir-jvm-docs")) diff --git a/morphir/cli/src/morphir/Main.scala b/morphir/cli/shared/src/main/org/morphir/Main.scala similarity index 94% rename from morphir/cli/src/morphir/Main.scala rename to morphir/cli/shared/src/main/org/morphir/Main.scala index a14f0a46..4b425da3 100644 --- a/morphir/cli/src/morphir/Main.scala +++ b/morphir/cli/shared/src/main/org/morphir/Main.scala @@ -1,13 +1,13 @@ -package morphir +package org.morphir import zio._ import zio.blocking.Blocking import zio.console._ import caseapp.core.app.CommandApp -import morphir.cli._ +import org.morphir.cli._ import caseapp.core.RemainingArgs -import morphir.runtime._ +import org.morphir.runtime._ import zio.stream.Sink -import morphir.sdk.ModelLoader +import org.morphir.sdk.ModelLoader import java.nio.file.Paths object Main { diff --git a/morphir/cli/src/morphir/cli/Cli.scala b/morphir/cli/shared/src/main/org/morphir/cli/Cli.scala similarity index 98% rename from morphir/cli/src/morphir/cli/Cli.scala rename to morphir/cli/shared/src/main/org/morphir/cli/Cli.scala index b75faa23..f99cebdc 100644 --- a/morphir/cli/src/morphir/cli/Cli.scala +++ b/morphir/cli/shared/src/main/org/morphir/cli/Cli.scala @@ -1,4 +1,4 @@ -package morphir.cli +package org.morphir.cli import java.nio.file.{Path => JPath} import zio._ diff --git a/morphir/cli/src/morphir/cli/CommandLine.scala b/morphir/cli/shared/src/main/org/morphir/cli/CommandLine.scala similarity index 98% rename from morphir/cli/src/morphir/cli/CommandLine.scala rename to morphir/cli/shared/src/main/org/morphir/cli/CommandLine.scala index 0bb6fe1a..ba5fb938 100644 --- a/morphir/cli/src/morphir/cli/CommandLine.scala +++ b/morphir/cli/shared/src/main/org/morphir/cli/CommandLine.scala @@ -3,7 +3,7 @@ package morphir.cli import org.rogach.scallop._ import java.nio.file.{Path, Paths} import upickle.default -import morphir.BuildInfo +import org.morphir.BuildInfo import zio._ import org.rogach.scallop.exceptions.ScallopException diff --git a/morphir/cli/src/morphir/cli/package.scala b/morphir/cli/shared/src/main/org/morphir/cli/package.scala similarity index 96% rename from morphir/cli/src/morphir/cli/package.scala rename to morphir/cli/shared/src/main/org/morphir/cli/package.scala index 8eef48e7..6876d703 100644 --- a/morphir/cli/src/morphir/cli/package.scala +++ b/morphir/cli/shared/src/main/org/morphir/cli/package.scala @@ -1,4 +1,4 @@ -package morphir +package org.morphir import java.nio.file.{Path => JPath} diff --git a/morphir/cli/src/morphir/runtime/ExitCode.scala b/morphir/cli/shared/src/main/org/morphir/runtime/ExitCode.scala similarity index 100% rename from morphir/cli/src/morphir/runtime/ExitCode.scala rename to morphir/cli/shared/src/main/org/morphir/runtime/ExitCode.scala diff --git a/morphir/core/shared/src/main/scala/org/morphir/core/newtype/NewtypeModule.scala b/morphir/core/shared/src/main/scala/org/morphir/core/newtype/NewtypeModule.scala new file mode 100644 index 00000000..3b6388dd --- /dev/null +++ b/morphir/core/shared/src/main/scala/org/morphir/core/newtype/NewtypeModule.scala @@ -0,0 +1,62 @@ +package org.morphir.core.newtype + +sealed trait NewtypeModule { + def newtype[A]: Newtype[A] + def subtype[A]: Subtype[A] + + sealed trait Newtype[A] { + type WrappedType + def apply(a: A): WrappedType + def unwrap(wt: WrappedType): A + def unapply(wt: WrappedType): Option[A] = + Some(unwrap(wt)) + def toF[F[_]](fa: F[A]): F[WrappedType] + def fromF[F[_]](fa: F[WrappedType]): F[A] + } + + sealed trait Subtype[A] extends Newtype[A] { + type WrappedType <: A + } +} + +object NewtypeModule { + val instance: NewtypeModule = new NewtypeModule { + def newtype[A]: Newtype[A] = new Newtype[A] { + type WrappedType = A + def apply(a: A): WrappedType = a + def unwrap(wt: WrappedType): A = wt + override def toF[F[_]](fa: F[A]): F[WrappedType] = fa + override def fromF[F[_]](fa: F[WrappedType]): F[A] = fa + } + + def subtype[A]: Subtype[A] = new Subtype[A] { + type WrappedType = A + def apply(a: A): WrappedType = a + def unwrap(wt: WrappedType): A = wt + override def toF[F[_]](fa: F[A]): F[WrappedType] = fa + override def fromF[F[_]](fa: F[WrappedType]): F[A] = fa + } + } +} + +trait NewtypeModuleExports { + import NewtypeModule._ + + abstract class Newtype[A] extends instance.Newtype[A] { + val newtype: instance.Newtype[A] = instance.newtype[A] + type WrappedType = newtype.WrappedType + def apply(a: A): WrappedType = newtype(a) + def unwrap(wt: WrappedType): A = newtype.unwrap(wt) + override def toF[F[_]](fa: F[A]): F[WrappedType] = newtype.toF(fa) + override def fromF[F[_]](fa: F[WrappedType]): F[A] = newtype.fromF(fa) + } + + abstract class Subtype[A] extends instance.Subtype[A] { + val subtype: instance.Subtype[A] = instance.subtype[A] + type WrappedType = subtype.WrappedType + def apply(a: A): WrappedType = subtype(a) + def unwrap(wt: WrappedType): A = subtype.unwrap(wt) + override def toF[F[_]](fa: F[A]): F[WrappedType] = subtype.toF(fa) + override def fromF[F[_]](fa: F[WrappedType]): F[A] = subtype.fromF(fa) + } +} diff --git a/morphir/core/shared/src/main/scala/org/morphir/core/newtype/package.scala b/morphir/core/shared/src/main/scala/org/morphir/core/newtype/package.scala new file mode 100644 index 00000000..772e0c9c --- /dev/null +++ b/morphir/core/shared/src/main/scala/org/morphir/core/newtype/package.scala @@ -0,0 +1,3 @@ +package org.morphir.core + +package object newtype extends NewtypeModuleExports diff --git a/morphir/ir/core/shared/src/main/scala/org/morphir/ir/AccessControlled.scala b/morphir/ir/core/shared/src/main/scala/org/morphir/ir/AccessControlled.scala index a1b72972..1998bdb6 100644 --- a/morphir/ir/core/shared/src/main/scala/org/morphir/ir/AccessControlled.scala +++ b/morphir/ir/core/shared/src/main/scala/org/morphir/ir/AccessControlled.scala @@ -10,6 +10,8 @@ object AccessControlled { sealed abstract class AccessControlled[A] extends Product with Serializable { + def value: A + def encodeToJson(implicit encoder: Writer[A]): ujson.Value def withPublicAccess: Option[A] = this match { diff --git a/morphir/sdk/core/shared/src/main/scala/org/morphir/sdk/NonEmptyListModule.scala b/morphir/sdk/core/shared/src/main/scala/org/morphir/sdk/NonEmptyListModule.scala new file mode 100644 index 00000000..acb92964 --- /dev/null +++ b/morphir/sdk/core/shared/src/main/scala/org/morphir/sdk/NonEmptyListModule.scala @@ -0,0 +1,22 @@ +package org.morphir.sdk + +import org.morphir.sdk.NonEmptyListModule.NonEmptyList.{Cons, Single} + +import scala.annotation.tailrec + +object NonEmptyListModule { + sealed trait NonEmptyList[+A] { self => + @tailrec + final def foldLeft[B](z: B)(f: (B, A) => B): B = + self match { + case Cons(h, t) => t.foldLeft(f(z, h))(f) + case Single(h) => f(z, h) + } + } + + object NonEmptyList { + final case class Cons[+A](head: A, tail: NonEmptyList[A]) + extends NonEmptyList[A] + final case class Single[+A](head: A) extends NonEmptyList[A] + } +} diff --git a/morphir/toolbox/js/src/main/scala/org/morphir/toolbox/workspace/services/files/FilesModule.scala b/morphir/toolbox/js/src/main/scala/org/morphir/toolbox/workspace/services/files/FilesModule.scala new file mode 100644 index 00000000..65254ce1 --- /dev/null +++ b/morphir/toolbox/js/src/main/scala/org/morphir/toolbox/workspace/services/files/FilesModule.scala @@ -0,0 +1,9 @@ +package org.morphir.toolbox.workspace.services.files + +import java.nio.file.{Path, Paths} + +private[files] object FilesModule { + + def userHomeDir: Path = Paths.get(sys.props.get("user.home").get) + +} diff --git a/morphir/toolbox/jvm/src/main/scala/org/morphir/toolbox/workspace/services/files/FilesModule.scala b/morphir/toolbox/jvm/src/main/scala/org/morphir/toolbox/workspace/services/files/FilesModule.scala new file mode 100644 index 00000000..3583cad9 --- /dev/null +++ b/morphir/toolbox/jvm/src/main/scala/org/morphir/toolbox/workspace/services/files/FilesModule.scala @@ -0,0 +1,11 @@ +package org.morphir.toolbox.workspace.services.files + +import java.nio.file.{Path, Paths} + +import io.github.soc.directories.UserDirectories + +private[files] object FilesModule { + + def userHomeDir: Path = Paths.get(UserDirectories.get().homeDir) + +} diff --git a/morphir/toolbox/modules/morphir-elm b/morphir/toolbox/modules/morphir-elm new file mode 160000 index 00000000..7731cb3d --- /dev/null +++ b/morphir/toolbox/modules/morphir-elm @@ -0,0 +1 @@ +Subproject commit 7731cb3d2b32f27231fff944ce4fbc9fe6331dcc diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/FrontendBinding.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/FrontendBinding.scala new file mode 100644 index 00000000..c1c82171 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/FrontendBinding.scala @@ -0,0 +1,19 @@ +package org.morphir.toolbox.binding +import zio.stream._ +import org.morphir.toolbox.binding.FrontendBinding.{InputPort, OutputPort} + +case class FrontendBinding[S]( + input: InputPort[Any], + output: OutputPort[Any], + initialState: S +) + +object FrontendBinding { + + type InputPort[+A] = Stream[Nothing, FrontendCommand[A]] + + type OutputPort[+A] = Stream[Nothing, FrontendEvent[A]] + + sealed trait FrontendCommand[+A] + sealed trait FrontendEvent[+A] +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmFrontendBinding.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmFrontendBinding.scala new file mode 100644 index 00000000..96db890f --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmFrontendBinding.scala @@ -0,0 +1,3 @@ +package org.morphir.toolbox.binding.elm + +class ElmFrontendBinding {} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Errors.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Errors.scala new file mode 100644 index 00000000..5a70fae6 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Errors.scala @@ -0,0 +1,21 @@ +package org.morphir.toolbox.core + +import java.io.IOException +import java.nio.file.Path + +import toml.Parse + +object Errors { + + final case class PathIsNotADirectoryError( + path: Path, + message: String, + cause: Option[IOException] = None + ) extends IOException(message, cause.orNull) + + final case class ManifestFileParseError( + details: Parse.Error, + message: String = "Failed to parse morphir workspace manifest file." + ) extends Exception(message) + +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ManifestFile.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ManifestFile.scala new file mode 100644 index 00000000..f680666f --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ManifestFile.scala @@ -0,0 +1,68 @@ +package org.morphir.toolbox.core + +import java.io.{File, IOException} +import java.nio.file.Paths + +import org.morphir.core.newtype._ +import org.morphir.toolbox.workspace.config.WorkspaceSettings +import zio._ +import zio.blocking.Blocking +import zio.nio.core.file.Path +import zio.nio.file.Files + +object ManifestFile extends Newtype[File] { + val DefaultName: String = "morphir.toml" + + def fromDirectory( + workspaceDir: WorkspaceDir + ): ZIO[Any, Throwable, ManifestFile] = + workspaceDir + .join(DefaultName) + .flatMap(path => ZIO.effect(ManifestFile(path.toFile))) + + def fromFile(file: File): UIO[ManifestFile] = + ZIO.succeed(ManifestFile(file)) + + def fromPath[R](workspacePath: WorkspacePath): RIO[R, ManifestFile] = + ZIO.effect(workspacePath.toFile).flatMap { file => + if (file.isDirectory) + ZIO.effect( + ManifestFile(Paths.get(file.getPath, DefaultName).toFile) + ) + else ZIO.effect(ManifestFile(file)) + } + + implicit class RichManifestFile(val manifestFile: ManifestFile) + extends AnyVal { + + def toPath: UIO[Path] = ZIO.effectTotal { + val file = ManifestFile.unwrap(manifestFile) + Path.fromJava(file.toPath) + } + + def readAllBytes: ZIO[Blocking, IOException, Chunk[Byte]] = { + for { + path <- toPath + data <- Files.readAllBytes(path) + } yield data + } + + def readAllText: ZIO[Blocking, IOException, String] = + readAllBytes.map(_.mkString) + + def load: ZIO[Blocking, Throwable, WorkspaceSettings] = + for { + contents <- readAllText + path <- toPath + absolutePath <- path.toAbsolutePath + settings <- ZIO + .fromEither(WorkspaceSettings.fromToml(contents)) + .mapError(e => + Errors.ManifestFileParseError( + e, + s"Failed to parse the morphir workspace manifest file at: $absolutePath." + ) + ) + } yield settings + } +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Project.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Project.scala new file mode 100644 index 00000000..0ac3a5f3 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Project.scala @@ -0,0 +1,16 @@ +package org.morphir.toolbox.core + +import java.nio.file.Path + +case class Project( + name: ProjectName, + projectDir: ProjectPath, + targets: List[Target[Any]], + sources: List[SourceFile[Any]] +) + +object Project {} + +case class Target[A](artifacts: List[Artifact[A]]) +case class Artifact[A](path: Path, manifest: ArtifactManifest, data: A) +case class ArtifactManifest() diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectName.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectName.scala new file mode 100644 index 00000000..9e3076f5 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectName.scala @@ -0,0 +1,21 @@ +package org.morphir.toolbox.core + +final case class ProjectName( + original: String, + overridden: Option[String] = None +) { + def resolvedName: String = overridden getOrElse original + + override def toString: String = resolvedName + + override def hashCode(): Int = resolvedName.toLowerCase.hashCode + + override def equals(obj: Any): Boolean = obj match { + case projectName: ProjectName => + projectName.resolvedName.equalsIgnoreCase(resolvedName) + case _ => false + } + + def matches(name: String): Boolean = + name.equalsIgnoreCase(resolvedName) || name.equalsIgnoreCase(original) +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectPath.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectPath.scala new file mode 100644 index 00000000..8969fa25 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectPath.scala @@ -0,0 +1,26 @@ +package org.morphir.toolbox.core + +import java.nio.file.Path + +import toml.{Codec, Value} + +case class ProjectPath(path: Path) extends AnyVal { + def /(segment: String): ProjectPath = + ProjectPath.of(path.toString, segment) + +} + +object ProjectPath { + + @inline def of(path: String, segments: String*): ProjectPath = + ProjectPath(Path.of(path, segments: _*)) + + implicit val projectPathCodec: Codec[ProjectPath] = Codec { + case (Value.Str(value), _, _) => + if (value.isBlank) + Left(List.empty -> s"A non-empty path is expected $value provided") + else Right(ProjectPath.of(value)) + case (value, _, _) => + Left(List.empty -> s"A path is expected, $value provided") + } +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/SourceFile.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/SourceFile.scala new file mode 100644 index 00000000..159b5f45 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/SourceFile.scala @@ -0,0 +1,5 @@ +package org.morphir.toolbox.core + +import java.nio.file.Path + +case class SourceFile[A](path: Path, data: A) diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala new file mode 100644 index 00000000..076b0459 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala @@ -0,0 +1,59 @@ +package org.morphir.toolbox.core + +import java.io.File +import java.nio.file.Path + +import org.morphir.toolbox.workspace.config.{ProjectSettings, WorkspaceSettings} +import org.morphir.toolbox.core.ManifestFile._ +import zio._ +import zio.blocking.Blocking + +case class Workspace( + rootDir: WorkspaceDir, + projects: Map[ProjectName, Project] +) { + def manifestFile: Task[File] = + rootDir + .join(Workspace.WorkspaceFilename) + .flatMap(p => ZIO.effect(p.toFile)) + +} + +object Workspace { + val WorkspaceFilename = "morphir.toml" + + def fromSettings( + settings: WorkspaceSettings + )(implicit workspaceDir: WorkspaceDir): UIO[Workspace] = + for { + dir <- ZIO.succeed(workspaceDir) + projects = settings.projects.map { + case (key, value) => + val project = createProject(value, key, dir) + project.name -> project + } + } yield Workspace(dir, projects) + + def load(path: Path): ZIO[Blocking, Throwable, Workspace] = + for { + workspacePath <- WorkspacePath.from(path) + workspaceDir <- WorkspaceDir.fromPath(workspacePath) + manifestFile <- ManifestFile.fromPath(workspacePath) + settings <- manifestFile.load + workspace <- fromSettings(settings)(workspaceDir) + } yield workspace + + private[core] def createProject( + settigs: ProjectSettings, + name: String, + workspaceDir: WorkspaceDir + ): Project = { + val projectName = ProjectName(name, settigs.name) + val projectDir = + settigs.projectDir getOrElse ProjectPath.of(projectName.resolvedName) + val projectPath = ProjectPath( + workspaceDir.joinPath(projectDir.path.toString) + ) + Project(projectName, projectPath, List.empty, List.empty) + } +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala new file mode 100644 index 00000000..44022adc --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala @@ -0,0 +1,39 @@ +package org.morphir.toolbox.core + +import java.nio.file.Path + +import zio._ + +final case class WorkspaceDir private (path: Path) extends AnyVal { + def /[R](segment: String): RIO[R, WorkspaceDir] = + RIO.effect(Path.of(path.toString, segment).toFile).flatMap { file => + if (file.isDirectory) Task.effect(WorkspaceDir(file.toPath)) + else + RIO.fail( + Errors.PathIsNotADirectoryError( + file.toPath, + s"The workspace directory must point at a directory, but $file is not a directory." + ) + ) + } + + def join(segments: String*): UIO[Path] = + ZIO.succeed(Path.of(path.toString, segments: _*)) + + def joinPath(segments: String*): Path = + Path.of(path.toString, segments: _*) + +} + +object WorkspaceDir { + def fromPath[R](path: Path): RIO[R, WorkspaceDir] = + RIO.ifM(Task.effect(path.toFile.isDirectory))( + RIO.effect(WorkspaceDir(path)), + RIO.fail( + Errors.PathIsNotADirectoryError( + path, + s"The workspace directory must point at a directory, but $path is not a directory." + ) + ) + ) +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspacePath.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspacePath.scala new file mode 100644 index 00000000..51541a28 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspacePath.scala @@ -0,0 +1,37 @@ +package org.morphir.toolbox.core + +import java.nio.file.Path + +import org.morphir.core.newtype._ +import zio._ + +object WorkspacePath extends Subtype[Path] { + def of(path: String): WorkspacePath = WorkspacePath(Path.of(path)) + + def from(path: Option[Path] = None): Task[WorkspacePath] = + Task.effect(path.getOrElse(Path.of("."))).map(WorkspacePath(_)) + + def from(path: Path): UIO[WorkspacePath] = + ZIO.succeed(WorkspacePath(path)) + + def toDirectory(path: WorkspacePath): Task[WorkspaceDir] = + path.ifIsDirectoryM( + WorkspaceDir.fromPath(path), + WorkspaceDir.fromPath(path.getParent) + ) + + implicit class WorkspacePathOps(val self: WorkspacePath) extends AnyVal { + def isDirectory: Task[Boolean] = + Task.effect(self.toFile.isDirectory) + + def ifIsDirectoryM[R, A]( + whenTrue: => RIO[R, A], + whenFalse: => RIO[R, A] + ): RIO[R, A] = + ZIO.ifM(isDirectory)(whenTrue, whenFalse) + + def workspaceManifestFile: Task[ManifestFile] = + ManifestFile.fromPath(self) + + } +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlCodec.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlCodec.scala new file mode 100644 index 00000000..05325441 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlCodec.scala @@ -0,0 +1,37 @@ +package org.morphir.toolbox.core.codecs + +import toml.Codec.Defaults +import toml._ + +class TomlCodec[A](val self: Codec[A]) extends AnyVal { + def map[B](f: A => B): Codec[B] = TomlCodec.instance { + case (value, defaults, index) => + self(value, defaults, index) match { + case Right(value) => Right(f(value)) + case left => left.asInstanceOf[Left[Parse.Error, B]] + } + } + + def mapResult[B](f: A => Either[Parse.Error, B]): Codec[B] = + TomlCodec.instance { + case (value, defaults, index) => + self(value, defaults, index) match { + case Right(value) => f(value) + case left => left.asInstanceOf[Left[Parse.Error, B]] + } + } +} + +object TomlCodec { + def instance[A]( + f: (Value, Defaults, Int) => Either[Parse.Error, A] + ): Codec[A] = + new Codec[A] { + override def apply( + value: Value, + defaults: Defaults, + index: Int + ): Either[Parse.Error, A] = + f(value, defaults, index) + } +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlSupport.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlSupport.scala new file mode 100644 index 00000000..d39edbaf --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlSupport.scala @@ -0,0 +1,29 @@ +package org.morphir.toolbox.core.codecs + +import java.nio.file.Path + +import org.morphir.toolbox.core.ProjectPath +import toml._ + +import scala.language.implicitConversions + +trait TomlSupport { + + implicit def toCodecOps[A](codec: Codec[A]): TomlCodec[A] = + new TomlCodec[A](codec) + + implicit val pathCodec: Codec[Path] = Codec { + case (Value.Str(value), _, _) => + if (value.isBlank) + Left(List.empty -> s"A non-empty path is expected $value provided") + else Right(Path.of(value)) + case (value, _, _) => + Left(List.empty -> s"A path is expected, $value provided") + } + + implicit val projectPathCodec: Codec[ProjectPath] = + ProjectPath.projectPathCodec + +} + +object TomlSupport extends TomlSupport diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/package.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/package.scala new file mode 100644 index 00000000..77d9c985 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/package.scala @@ -0,0 +1,7 @@ +package org.morphir.toolbox + +package object core { + + type WorkspacePath = WorkspacePath.WrappedType + type ManifestFile = ManifestFile.WrappedType +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingSettings.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingSettings.scala new file mode 100644 index 00000000..22c78eb5 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingSettings.scala @@ -0,0 +1,14 @@ +package org.morphir.toolbox.workspace.config + +import java.nio.file.Path + +case class BindingSettings(srcDirs: List[Path], outDir: Path) + +object BindingSettings { + object defaults { + val elm: BindingSettings = BindingSettings( + srcDirs = List(Path.of("src/elm/")), + outDir = Path.of("out/") + ) + } +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingsSettings.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingsSettings.scala new file mode 100644 index 00000000..308462bd --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingsSettings.scala @@ -0,0 +1,7 @@ +package org.morphir.toolbox.workspace.config + +object BindingsSettings { + val default: BindingsConfig = Map( + "elm" -> BindingSettings.defaults.elm + ) +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/ProjectSettings.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/ProjectSettings.scala new file mode 100644 index 00000000..6945935b --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/ProjectSettings.scala @@ -0,0 +1,9 @@ +package org.morphir.toolbox.workspace.config + +import org.morphir.toolbox.core.ProjectPath + +case class ProjectSettings( + name: Option[String] = None, + projectDir: Option[ProjectPath] = None, + bindings: Map[String, BindingSettings] = BindingsSettings.default +) diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/WorkspaceSettings.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/WorkspaceSettings.scala new file mode 100644 index 00000000..aca1829a --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/WorkspaceSettings.scala @@ -0,0 +1,14 @@ +package org.morphir.toolbox.workspace.config +import org.morphir.toolbox.core.codecs.TomlSupport +import toml.Codecs._ +import toml._ + +case class WorkspaceSettings(projects: Map[String, ProjectSettings]) + +object WorkspaceSettings extends TomlSupport { + def default(): WorkspaceSettings = WorkspaceSettings(Map.empty) + + def fromToml(text: String): Either[toml.Parse.Error, WorkspaceSettings] = + Toml.parseAs[WorkspaceSettings](text) + +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/package.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/package.scala new file mode 100644 index 00000000..2825b0b3 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/package.scala @@ -0,0 +1,5 @@ +package org.morphir.toolbox.workspace + +package object config { + type BindingsConfig = Map[String, BindingSettings] +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/package.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/package.scala new file mode 100644 index 00000000..7857b1a1 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/package.scala @@ -0,0 +1,26 @@ +package org.morphir.toolbox + +import java.nio.file.Path +import core.Workspace +import zio._ + +package object workspace { + type WorkspaceModule = Has[WorkspaceModule.Service] + + def openFrom(path: Path): RIO[WorkspaceModule, Workspace] = + RIO.accessM[WorkspaceModule](_.get.openFrom(path)) + + object WorkspaceModule { + + trait Service { + def openFrom(path: Path): Task[Workspace] + } + + val live: ULayer[WorkspaceModule] = ZLayer.succeed(new Live()) + + class Live() extends Service { + def openFrom(path: Path): Task[Workspace] = ??? + } + + } +} diff --git a/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/core/ProjectPathSpec.scala b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/core/ProjectPathSpec.scala new file mode 100644 index 00000000..ba9a033f --- /dev/null +++ b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/core/ProjectPathSpec.scala @@ -0,0 +1,22 @@ +package org.morphir.toolbox.core + +import zio.test._ +import zio.test.Assertion._ +import toml.Codecs._ +import toml._ +import org.morphir.toolbox.core.codecs.TomlSupport._ + +object ProjectPathSpec extends DefaultRunnableSpec { + def spec = suite("ProjectPath Spec")( + suite("TOML Codec")( + test("A path-like string should decode from TOML") { + case class Root(path: ProjectPath) + val tomlDoc = """ path = "foo/bar/baz" """ + val result = Toml.parseAs[Root](tomlDoc) + assert(result)( + isRight(equalTo(Root(ProjectPath.of("foo", "bar", "baz")))) + ) + } + ) + ) +} diff --git a/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/workspace/config/WorkspaceSettingsSpec.scala b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/workspace/config/WorkspaceSettingsSpec.scala new file mode 100644 index 00000000..0a96a873 --- /dev/null +++ b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/workspace/config/WorkspaceSettingsSpec.scala @@ -0,0 +1,36 @@ +package org.morphir.toolbox.workspace.config + +import org.morphir.toolbox.core.ProjectPath +import zio.test._ +import zio.test.Assertion._ + +object WorkspaceSettingsSpec extends DefaultRunnableSpec { + def spec = suite("WorkspaceConfig Spec")( + suite("Parsing from TOML")( + test("Should parse from TOML") { + val doc = + """ + |[projects.foo] + |[projects.bar] + |projectDir = "models/bar/" + |""".stripMargin + + val config = WorkspaceSettings.fromToml(doc) + assert(config)( + isRight( + equalTo( + WorkspaceSettings( + Map( + "foo" -> ProjectSettings(), + "bar" -> ProjectSettings(projectDir = + Some(ProjectPath.of("models/bar/")) + ) + ) + ) + ) + ) + ) + } + ) + ) +} diff --git a/project/metals.sbt b/project/metals.sbt index a64ca305..6c823e72 100644 --- a/project/metals.sbt +++ b/project/metals.sbt @@ -1,4 +1,4 @@ // DO NOT EDIT! This file is auto-generated. // This file enables sbt-bloop to create bloop config files. -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.0-RC1-190-ef7d8dba") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.0-RC1-219-a3514983") diff --git a/project/plugins.sbt b/project/plugins.sbt index 21662df8..2f57943c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,12 +5,10 @@ addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.9.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.1.1") -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.3.5") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.0-RC1-219-a3514983") addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.3") addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.0") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.12") addSbtPlugin("com.thoughtworks.sbt-api-mappings" % "sbt-api-mappings" % "3.0.0") addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.1") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.13") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.0") -addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.2") From bbc54a7d835c818dbf1aa21b11243674009fc0f2 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 29 Apr 2020 11:34:16 -0400 Subject: [PATCH 2/5] Create an example of a multi-project workspace --- .gitignore | 3 ++ .../multi-project-workspace/.gitignore | 10 +++++++ .../models/order-taking/elm.json | 24 +++++++++++++++ .../elm/GlobalCo/OrderTaking/OrderInput.elm | 5 ++++ .../models/shared-domain/elm.json | 25 ++++++++++++++++ .../src/elm/GlobalCo/Comms/CustomerEmail.elm | 12 ++++++++ .../src/elm/GlobalCo/Comms/EmailAddress.elm | 30 +++++++++++++++++++ .../multi-project-workspace/morphir.toml | 4 +++ 8 files changed, 113 insertions(+) create mode 100644 examples/workspaces/multi-project-workspace/.gitignore create mode 100644 examples/workspaces/multi-project-workspace/models/order-taking/elm.json create mode 100644 examples/workspaces/multi-project-workspace/models/order-taking/src/elm/GlobalCo/OrderTaking/OrderInput.elm create mode 100644 examples/workspaces/multi-project-workspace/models/shared-domain/elm.json create mode 100644 examples/workspaces/multi-project-workspace/models/shared-domain/src/elm/GlobalCo/Comms/CustomerEmail.elm create mode 100644 examples/workspaces/multi-project-workspace/models/shared-domain/src/elm/GlobalCo/Comms/EmailAddress.elm create mode 100644 examples/workspaces/multi-project-workspace/morphir.toml diff --git a/.gitignore b/.gitignore index 633ea9d4..1a428425 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ mill.iml bsp.log contrib/bsp/test/ lowered.hnir +models/order-taking/elm.json +models/shared-domain/elm-stuff/ +models/shared-domain/elm.json diff --git a/examples/workspaces/multi-project-workspace/.gitignore b/examples/workspaces/multi-project-workspace/.gitignore new file mode 100644 index 00000000..4f9571a3 --- /dev/null +++ b/examples/workspaces/multi-project-workspace/.gitignore @@ -0,0 +1,10 @@ +# Created by .ignore support plugin (hsz.mobi) +### Elm template +# elm-package generated files +elm-stuff +# elm-repl generated files +repl-temp-* + +### Morphir +# Morphir output +.morphir \ No newline at end of file diff --git a/examples/workspaces/multi-project-workspace/models/order-taking/elm.json b/examples/workspaces/multi-project-workspace/models/order-taking/elm.json new file mode 100644 index 00000000..dea3450d --- /dev/null +++ b/examples/workspaces/multi-project-workspace/models/order-taking/elm.json @@ -0,0 +1,24 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.0" + }, + "indirect": { + "elm/json": "1.1.3", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/examples/workspaces/multi-project-workspace/models/order-taking/src/elm/GlobalCo/OrderTaking/OrderInput.elm b/examples/workspaces/multi-project-workspace/models/order-taking/src/elm/GlobalCo/OrderTaking/OrderInput.elm new file mode 100644 index 00000000..898cda62 --- /dev/null +++ b/examples/workspaces/multi-project-workspace/models/order-taking/src/elm/GlobalCo/OrderTaking/OrderInput.elm @@ -0,0 +1,5 @@ +module OrderInput exposing (..) + + +type alias UnvalidatedOrder = + { orderId : String } diff --git a/examples/workspaces/multi-project-workspace/models/shared-domain/elm.json b/examples/workspaces/multi-project-workspace/models/shared-domain/elm.json new file mode 100644 index 00000000..a9b913b1 --- /dev/null +++ b/examples/workspaces/multi-project-workspace/models/shared-domain/elm.json @@ -0,0 +1,25 @@ +{ + "type": "application", + "source-directories": [ + "src/elm" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.0", + "elm/regex": "1.0.0" + }, + "indirect": { + "elm/json": "1.1.3", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/examples/workspaces/multi-project-workspace/models/shared-domain/src/elm/GlobalCo/Comms/CustomerEmail.elm b/examples/workspaces/multi-project-workspace/models/shared-domain/src/elm/GlobalCo/Comms/CustomerEmail.elm new file mode 100644 index 00000000..2be14415 --- /dev/null +++ b/examples/workspaces/multi-project-workspace/models/shared-domain/src/elm/GlobalCo/Comms/CustomerEmail.elm @@ -0,0 +1,12 @@ +module GlobalCo.Comms.CustomerEmail exposing (CustomerEmail(..), VerifiedEmailAddress) + +import EmailAddress exposing (EmailAddress) + + +type VerifiedEmailAddress + = VerifiedEmailAddress String + + +type CustomerEmail + = Unverified EmailAddress + | Verified VerifiedEmailAddress diff --git a/examples/workspaces/multi-project-workspace/models/shared-domain/src/elm/GlobalCo/Comms/EmailAddress.elm b/examples/workspaces/multi-project-workspace/models/shared-domain/src/elm/GlobalCo/Comms/EmailAddress.elm new file mode 100644 index 00000000..00d4bd6f --- /dev/null +++ b/examples/workspaces/multi-project-workspace/models/shared-domain/src/elm/GlobalCo/Comms/EmailAddress.elm @@ -0,0 +1,30 @@ +module EmailAddress exposing (..) + + +type EmailAddress + = EmailAddress String + + +create : String -> Result String EmailAddress +create text = + let + len = + String.length text + + endIdx = + len - 1 + in + if String.isEmpty text then + "An email address cannot be empty" |> Result.Err + + else + case String.indices "@" text of + [] -> + "Invalid email address" |> Result.Err + + idx :: [] -> + if idx < endIdx then + EmailAddress text |> Result.Ok + + else + "Invalid email address" |> Result.Err diff --git a/examples/workspaces/multi-project-workspace/morphir.toml b/examples/workspaces/multi-project-workspace/morphir.toml new file mode 100644 index 00000000..566eac1e --- /dev/null +++ b/examples/workspaces/multi-project-workspace/morphir.toml @@ -0,0 +1,4 @@ +[projects.globalco-shared] +projectDir = "models/shared-domain" +[projects.globalco-order-taking] +projectDir = "models/order-taking" \ No newline at end of file From 26a85048390c1f83d4d5eee5198f40448b912a9f Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Fri, 1 May 2020 17:29:39 -0400 Subject: [PATCH 3/5] Basic CLI --- .scalafmt.conf | 16 +++- build.sbt | 36 ++++----- .../multi-project-workspace/morphir.toml | 1 + .../shared/src/main/org/morphir/Main.scala | 66 ----------------- .../shared/src/main/org/morphir/cli/Cli.scala | 65 ----------------- .../main/org/morphir/cli/CommandLine.scala | 73 ------------------- .../src/main/org/morphir/cli/package.scala | 27 ------- .../main/org/morphir/runtime/ExitCode.scala | 9 --- .../src/main/scala/org/morphir/cli/Cli.scala | 57 +++++++++++++++ .../src/main/scala/org/morphir/cli/Main.scala | 17 +++++ .../org/morphir/toolbox/cli/CliCommand.scala | 27 +++++++ .../org/morphir/toolbox/cli/CliEnv.scala | 20 +++++ .../org/morphir/toolbox/cli/ExitCode.scala | 15 ++++ .../cli/commands/WorkspaceInfoCommand.scala | 50 +++++++++++++ .../org/morphir/toolbox/cli/package.scala | 12 +++ .../morphir/toolbox/core/ManifestFile.scala | 36 +++++---- .../morphir/toolbox/core/ProjectPath.scala | 10 ++- .../org/morphir/toolbox/core/Workspace.scala | 26 ++++--- .../morphir/toolbox/core/WorkspaceDir.scala | 8 +- .../morphir/toolbox/core/WorkspacePath.scala | 10 +-- .../toolbox/core/codecs/TomlSupport.scala | 6 +- .../workspace/config/BindingSettings.scala | 6 +- .../morphir/toolbox/workspace/package.scala | 12 +-- project/BuildHelper.scala | 56 +++++++------- project/metals.sbt | 2 +- 25 files changed, 326 insertions(+), 337 deletions(-) delete mode 100644 morphir/cli/shared/src/main/org/morphir/Main.scala delete mode 100644 morphir/cli/shared/src/main/org/morphir/cli/Cli.scala delete mode 100644 morphir/cli/shared/src/main/org/morphir/cli/CommandLine.scala delete mode 100644 morphir/cli/shared/src/main/org/morphir/cli/package.scala delete mode 100644 morphir/cli/shared/src/main/org/morphir/runtime/ExitCode.scala create mode 100644 morphir/cli/shared/src/main/scala/org/morphir/cli/Cli.scala create mode 100644 morphir/cli/shared/src/main/scala/org/morphir/cli/Main.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliCommand.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliEnv.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/ExitCode.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/WorkspaceInfoCommand.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/package.scala diff --git a/.scalafmt.conf b/.scalafmt.conf index 98844070..d7b61d99 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1 +1,15 @@ -version = "2.4.1" +version = "2.4.2" +maxColumn = 120 +align = most +continuationIndent.defnSite = 2 +assumeStandardLibraryStripMargin = true +docstrings = JavaDoc +lineEndings = preserve +includeCurlyBraceInSelectChains = false +danglingParentheses = true +spaces { + inImportCurlyBraces = true +} +optIn.annotationNewlines = true + +rewrite.rules = [SortImports, RedundantBraces] \ No newline at end of file diff --git a/build.sbt b/build.sbt index d29a00da..a387daca 100644 --- a/build.sbt +++ b/build.sbt @@ -61,8 +61,6 @@ lazy val root = project morphirToolboxJS, morphirToolboxJVM, morphirCliJVM -// morphirBindingElmJS, -// morphirBindingElmJVM ) lazy val morphirCore = crossProject(JSPlatform, JVMPlatform) @@ -86,8 +84,8 @@ lazy val morphirSdkCore = crossProject(JSPlatform, JVMPlatform) .settings(buildInfoSettings("morphir.sdk.core")) .settings( libraryDependencies ++= Seq( - "dev.zio" %%% "zio" % Versions.zio, - "dev.zio" %%% "zio-test" % Versions.zio % "test", + "dev.zio" %%% "zio" % Versions.zio, + "dev.zio" %%% "zio-test" % Versions.zio % "test", "dev.zio" %%% "zio-test-sbt" % Versions.zio % "test" ) ) @@ -148,17 +146,19 @@ lazy val morphirSdkCoreJVM = morphirSdkCore.jvm lazy val morphirToolbox = crossProject(JSPlatform, JVMPlatform) .in(file("morphir/toolbox")) .dependsOn(morphirCore) - .settings(stdSettings("morphir-toolbox")) + .settings(stdSettings("morphir-toolbox", Some(Seq(ScalaVersions.Scala212, ScalaVersions.Scala213)))) .settings(crossProjectSettings) .settings(buildInfoSettings("org.morphir.toolbox")) .settings( libraryDependencies ++= Seq( - "dev.zio" %%% "zio-streams" % Versions.zio, - "io.circe" %%% "circe-generic" % Versions.circe, - "io.circe" %%% "circe-parser" % Versions.circe, - "dev.zio" %%% "zio-test" % Versions.zio % "test", - "dev.zio" %%% "zio-test-sbt" % Versions.zio % "test", - "tech.sparse" %%% "toml-scala" % "0.2.2" + "dev.zio" %%% "zio-streams" % Versions.zio, + "io.circe" %%% "circe-generic" % Versions.circe, + "io.circe" %%% "circe-parser" % Versions.circe, + "dev.zio" %%% "zio-test" % Versions.zio % "test", + "dev.zio" %%% "zio-test-sbt" % Versions.zio % "test", + "com.lihaoyi" %%% "pprint" % "0.5.9", + "com.lihaoyi" %%% "fansi" % "0.2.9", + "tech.sparse" %%% "toml-scala" % "0.2.2" ) ) .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) @@ -166,9 +166,9 @@ lazy val morphirToolbox = crossProject(JSPlatform, JVMPlatform) lazy val morphirToolboxJS = morphirToolbox.js .settings(testJsSettings) .settings(zioNioSettings("1.0.0-RC6")) + .settings(scalaJSModuleKind := ModuleKind.CommonJSModule) lazy val morphirToolboxJVM = morphirToolbox.jvm - .settings(dottySettings) .settings(zioNioSettings("1.0.0-RC6")) .settings( libraryDependencies ++= Seq( @@ -179,20 +179,22 @@ lazy val morphirToolboxJVM = morphirToolbox.jvm lazy val morphirCli = crossProject(JVMPlatform) .in(file("morphir/cli")) .dependsOn(morphirToolbox) - .settings(stdSettings("morphir-cli")) + .settings(stdSettings("morphir-cli", Some(Seq(ScalaVersions.Scala213, ScalaVersions.Scala212)))) .settings(crossProjectSettings) .settings(buildInfoSettings("org.morphir.cli")) .settings( libraryDependencies ++= Seq( - "dev.zio" %% "zio" % Versions.zio, - "dev.zio" %% "zio-test" % Versions.zio % "test", - "dev.zio" %% "zio-test-sbt" % Versions.zio % "test" + "dev.zio" %% "zio" % Versions.zio, + "dev.zio" %% "zio-test" % Versions.zio % "test", + "dev.zio" %% "zio-test-sbt" % Versions.zio % "test", + "dev.zio" %% "zio-logging" % "0.2.8", + "com.monovore" %% "decline-effect" % "1.2.0", + "com.lihaoyi" %% "pprint" % "0.5.9" ) ) .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) lazy val morphirCliJVM = morphirCli.jvm - .settings(dottySettings) lazy val docs = project .in(file("morphir-jvm-docs")) diff --git a/examples/workspaces/multi-project-workspace/morphir.toml b/examples/workspaces/multi-project-workspace/morphir.toml index 566eac1e..e0ee2b98 100644 --- a/examples/workspaces/multi-project-workspace/morphir.toml +++ b/examples/workspaces/multi-project-workspace/morphir.toml @@ -1,4 +1,5 @@ [projects.globalco-shared] projectDir = "models/shared-domain" + [projects.globalco-order-taking] projectDir = "models/order-taking" \ No newline at end of file diff --git a/morphir/cli/shared/src/main/org/morphir/Main.scala b/morphir/cli/shared/src/main/org/morphir/Main.scala deleted file mode 100644 index 4b425da3..00000000 --- a/morphir/cli/shared/src/main/org/morphir/Main.scala +++ /dev/null @@ -1,66 +0,0 @@ -package org.morphir -import zio._ -import zio.blocking.Blocking -import zio.console._ -import caseapp.core.app.CommandApp -import org.morphir.cli._ -import caseapp.core.RemainingArgs -import org.morphir.runtime._ -import zio.stream.Sink -import org.morphir.sdk.ModelLoader -import java.nio.file.Paths - -object Main { - - def main(args: Array[String]): Unit = { - val program = - for { - cmdLine <- CommandLine.make(args.toIndexedSeq) - exitCode <- handleCommands(cmdLine).provideLayer( - ModelLoader.live ++ Cli.live ++ Blocking.live ++ Console.live - ) - } yield exitCode - - Runtime.default.unsafeRun(program) - } - - def run(args: List[String]): ZIO[zio.ZEnv, Nothing, Int] = - (for { - cmdLine <- ZIO.effect(new CommandLine(args)) - - } yield ()).fold(_ => 1, _ => 0) - - def handleCommands(commandLine: CommandLine) = - ZIO.effectSuspend { - commandLine.subcommands match { - case commandLine.elm :: commandLine.elm.make :: Nil => - cli - .elmMake( - commandLine.elm.make.projectDir.toOption, - commandLine.elm.make.output.toOption - ) - .foreach(line => putStrLn(line)) - .orElseSucceed(ExitCode.Failure) *> UIO.succeed(ExitCode.Success) - case commandLine.elm :: commandLine.elm.gen :: Nil => - putStrLn(s"morphir elm gen>") *> UIO.succeed(ExitCode.Success) - case commandLine.generate :: commandLine.generate.scala :: Nil => - (for { - modelPath <- ZIO.fromOption( - commandLine.generate.scala.modelPath.toOption - ) - outputPath <- ZIO.succeed( - commandLine.generate.scala.output.toOption getOrElse Paths.get( - "." - ) - ) - _ <- cli.generateScala(modelPath, outputPath) - } yield ()).orElseSucceed(ExitCode.Failure) *> UIO.succeed( - ExitCode.Success - ) - - case _ => - ZIO.effect(commandLine.printHelp()) *> UIO.succeed(ExitCode.Failure) - } - } - -} diff --git a/morphir/cli/shared/src/main/org/morphir/cli/Cli.scala b/morphir/cli/shared/src/main/org/morphir/cli/Cli.scala deleted file mode 100644 index f99cebdc..00000000 --- a/morphir/cli/shared/src/main/org/morphir/cli/Cli.scala +++ /dev/null @@ -1,65 +0,0 @@ -package org.morphir.cli - -import java.nio.file.{Path => JPath} -import zio._ -import zio.console._ -import zio.process._ -import zio.blocking.`package`.Blocking -import zio.nio.core.file.Path -import zio.stream.ZStream -import morphir.sdk.{ModelLoader, modelLoader} - -object Cli { - trait Service { - def generateScala( - modelPath: JPath, - output: JPath - ): ZIO[ModelLoader with Blocking with Console, Throwable, Unit] - - def elmMake( - projectDir: Option[JPath] = None, - output: Option[JPath] = None, - help: Boolean = false - ): ZStream[Blocking, Throwable, String] - } - - val live = ZLayer.succeed { - new Service { - - def generateScala(modelPath: JPath, output: JPath): ZIO[ - morphir.sdk.ModelLoader with Blocking with Console, - Throwable, - Unit - ] = - (for { - json <- modelLoader.loadJsonFromFile(modelPath) - _ <- putStrLn("JSON:") - _ <- putStrLn(json.render(2)) - } yield ()) - - def elmMake( - projectDir: Option[JPath] = None, - output: Option[JPath] = None, - help: Boolean = false - ) = - (for { - argsRef <- ZStream.fromEffect(Ref.make[Array[String]](Array("make"))) - _ <- ZStream.fromEffect(ZIO.whenCase(projectDir) { - case Some(dir) => - argsRef.update(args => - args ++ Array("-p", dir.toFile().getAbsolutePath()) - ) - }) - _ <- ZStream.fromEffect(ZIO.whenCase(output) { - case Some(filePath) => - argsRef.update(args => - args ++ Array("-o", filePath.toFile().getAbsolutePath()) - ) - }) - args <- ZStream.fromEffect(argsRef.get) - result <- Command("morphir-elm", args.toIndexedSeq: _*).linesStream - } yield result) - - } - } -} diff --git a/morphir/cli/shared/src/main/org/morphir/cli/CommandLine.scala b/morphir/cli/shared/src/main/org/morphir/cli/CommandLine.scala deleted file mode 100644 index ba5fb938..00000000 --- a/morphir/cli/shared/src/main/org/morphir/cli/CommandLine.scala +++ /dev/null @@ -1,73 +0,0 @@ -package morphir.cli - -import org.rogach.scallop._ -import java.nio.file.{Path, Paths} -import upickle.default -import org.morphir.BuildInfo -import zio._ -import org.rogach.scallop.exceptions.ScallopException - -class CommandLine(arguments: Seq[String]) extends ScallopConf(arguments) { - version(s"${BuildInfo.appName} ${BuildInfo.productVersion}") - printedName = BuildInfo.appName - shortSubcommandsHelp(true) - val generate = new Subcommand("generate") { - banner { - """Usage: morphir generate - |""".stripMargin - } - val scala = new Subcommand("scala") { - banner { - """Usage: morphir generate scala [Options] - |Options: - |""".stripMargin - } - - val output = opt[Path](argName = "output-path") - val modelPath = trailArg[Path](required = true) - } - addSubcommand(scala) - requireSubcommand() - } - - addSubcommand(generate) - - val elm = new Subcommand("elm") { - printedName = s"${BuildInfo.appName} elm" - val make = new Subcommand("make") { - val projectDir = opt[Path]( - argName = "path", - descr = - """Root directory of the project where morphir.json is located. (default: ".")""", - default = Some(Paths.get(".")) - ) - val output = opt[Path]( - argName = "output-path", - descr = - "Target location where the Morphir IR will be sent. Defaults to STDOUT." - ) - } - addSubcommand(make) - val gen = new Subcommand("gen") - addSubcommand(gen) - - addValidation { - subcommands match { - case Nil => - val msg = s""" $printedName - |${helpFormatter.formatHelp(builder, "")} - |""".stripMargin - - Left(msg) - case _ => Right(()) - } - } - } - addSubcommand(elm) - verify() -} - -object CommandLine { - def make(arguments: Seq[String]) = ZIO.effect(new CommandLine(arguments)) - -} diff --git a/morphir/cli/shared/src/main/org/morphir/cli/package.scala b/morphir/cli/shared/src/main/org/morphir/cli/package.scala deleted file mode 100644 index 6876d703..00000000 --- a/morphir/cli/shared/src/main/org/morphir/cli/package.scala +++ /dev/null @@ -1,27 +0,0 @@ -package org.morphir - -import java.nio.file.{Path => JPath} - -import zio._ -import zio.blocking.Blocking -import zio.console.Console -import zio.stream.ZStream - -package object cli { - - type Cli = Has[Cli.Service] - - def generateScala(modelPath: JPath, output: JPath) = - ZIO.accessM[Cli with morphir.sdk.ModelLoader with Blocking with Console]( - _.get.generateScala(modelPath, output) - ) - - def elmMake( - projectDir: Option[JPath] = None, - output: Option[JPath] = None, - help: Boolean = false - ) = - ZStream.accessStream[Cli with Blocking]( - _.get.elmMake(projectDir, output, help) - ) -} diff --git a/morphir/cli/shared/src/main/org/morphir/runtime/ExitCode.scala b/morphir/cli/shared/src/main/org/morphir/runtime/ExitCode.scala deleted file mode 100644 index 5539c012..00000000 --- a/morphir/cli/shared/src/main/org/morphir/runtime/ExitCode.scala +++ /dev/null @@ -1,9 +0,0 @@ -package morphir.runtime - -sealed abstract class ExitCode(val code: Int) - -object ExitCode { - case object Success extends ExitCode(0) - case object Failure extends ExitCode(1) - case class Custom(override val code: Int) extends ExitCode(code) -} diff --git a/morphir/cli/shared/src/main/scala/org/morphir/cli/Cli.scala b/morphir/cli/shared/src/main/scala/org/morphir/cli/Cli.scala new file mode 100644 index 00000000..9ed9c4b5 --- /dev/null +++ b/morphir/cli/shared/src/main/scala/org/morphir/cli/Cli.scala @@ -0,0 +1,57 @@ +package org.morphir.cli + +import java.nio.file.Path + +import com.monovore.decline._ +import org.morphir.toolbox.cli.CliCommand +import org.morphir.toolbox.cli.commands.WorkspaceInfoCommand +import zio.{ IO, ZIO } + +object Cli { + + def parse(args: Seq[String]): IO[Help, CliCommand] = + ZIO.fromEither(Cli.rootCommand.parse(args)) + + lazy val rootCommand: Command[CliCommand] = Command("morphir", "Morphir CLI")( + buildCommand orElse projectCommand orElse workspaceCommand + ) + + lazy val buildCommand: Opts[CliCommand.Build] = Opts + .subcommand("build", help = "Build the workspace")( + workspaceOpt.orNone + ) + .map(CliCommand.Build) + + lazy val projectCommand: Opts[CliCommand.ProjectList] = Opts.subcommand("project", help = "Work with projects")( + Opts + .subcommand(name = "list", help = "List projects in the workspace")( + workspaceOpt.orNone + ) + .map(CliCommand.ProjectList) + ) + + lazy val workspaceCommand: Opts[CliCommand] = + Opts.subcommand("workspace", help = "Work wth workspaces") { + val initCmd = Opts + .subcommand("init", help = "Initialize a workspace")( + workspaceOpt.orNone + ) + .map(CliCommand.WorkspaceInit) + + val infoCmd = Opts + .subcommand("info", help = "Get information about a workspace")( + workspaceOpt.orNone + ) + .map(WorkspaceInfoCommand) + + infoCmd orElse initCmd + } + + private lazy val workspaceOpt = Opts.option[Path]( + "workspace", + short = "w", + metavar = "workspace-path", + help = "The path to the workspace folder or manifest file." + ) + +} diff --git a/morphir/cli/shared/src/main/scala/org/morphir/cli/Main.scala b/morphir/cli/shared/src/main/scala/org/morphir/cli/Main.scala new file mode 100644 index 00000000..de67c14b --- /dev/null +++ b/morphir/cli/shared/src/main/scala/org/morphir/cli/Main.scala @@ -0,0 +1,17 @@ +package org.morphir.cli + +import org.morphir.toolbox.workspace.WorkspaceModule +import zio._ + +object Main extends App { + + override def run(args: List[String]): ZIO[zio.ZEnv, Nothing, Int] = + (for { + cmd <- ZIO.fromEither(Cli.rootCommand.parse(args)) + exitCode <- cmd.execute + ec = exitCode.code + } yield ec) + .provideSomeLayer[zio.ZEnv](WorkspaceModule.live) + .catchAll(help => ZIO.effectTotal(System.err.println(help)) *> ZIO.succeed(1)) + +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliCommand.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliCommand.scala new file mode 100644 index 00000000..e245f255 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliCommand.scala @@ -0,0 +1,27 @@ +package org.morphir.toolbox.cli + +import java.nio.file.Path + +import zio._ + +abstract class CliCommand extends Product { + def execute: ZIO[CliEnv, Nothing, ExitCode] +} + +object CliCommand { + + final case class Build(workspacePath: Option[Path]) extends CliCommand { + val execute: ZIO[CliEnv, Nothing, ExitCode] = + console.putStrLn(s"Executed: $this") *> ExitCode.success + } + final case class ProjectList(workspacePath: Option[Path]) extends CliCommand { + val execute: ZIO[CliEnv, Nothing, ExitCode] = + console.putStrLn(s"Executed: $this") *> ExitCode.success + } + + final case class WorkspaceInit(workspacePath: Option[Path]) extends CliCommand { + val execute: ZIO[CliEnv, Nothing, ExitCode] = + console.putStrLn(s"Executed: $this") *> ExitCode.success + } + +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliEnv.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliEnv.scala new file mode 100644 index 00000000..59ab8618 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliEnv.scala @@ -0,0 +1,20 @@ +package org.morphir.toolbox.cli + +import org.morphir.toolbox.workspace.WorkspaceModule +import zio.ZLayer +import zio._ +import zio.blocking.Blocking +import zio.clock.Clock +import zio.console.Console +import zio.random.Random + +object CliEnv { + val live + : ZLayer[Any, Throwable, Clock with Console with system.System with Random with WorkspaceModule with Blocking] = + Clock.live ++ + Console.live ++ + system.System.live ++ + Random.live ++ + (Blocking.live >>> WorkspaceModule.live) ++ Blocking.live + +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/ExitCode.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/ExitCode.scala new file mode 100644 index 00000000..ee528eea --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/ExitCode.scala @@ -0,0 +1,15 @@ +package org.morphir.toolbox.cli + +import zio.{ UIO, ZIO } + +sealed abstract class ExitCode(val code: Int) + +object ExitCode { + case object Success extends ExitCode(0) + case object Failure extends ExitCode(1) + case class Custom(override val code: Int) extends ExitCode(code) + + def success: UIO[Success.type] = ZIO.succeed(Success) + def failure: UIO[Failure.type] = ZIO.succeed(Failure) + def custom(code: Int): UIO[Custom] = ZIO.succeed(Custom(code)) +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/WorkspaceInfoCommand.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/WorkspaceInfoCommand.scala new file mode 100644 index 00000000..3e061dc6 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/WorkspaceInfoCommand.scala @@ -0,0 +1,50 @@ +package org.morphir.toolbox.cli.commands + +import java.nio.file.Path + +import org.morphir.toolbox.cli.{ CliCommand, CliEnv, ExitCode } +import org.morphir.toolbox.core.{ Project, SourceFile } +import org.morphir.toolbox.workspace +import zio._ +import zio.console.Console +import zio.stream.ZStream + +final case class WorkspaceInfoCommand(workspacePath: Option[Path]) extends CliCommand { + val execute: ZIO[CliEnv, Nothing, ExitCode] = (for { + theWorkspace <- workspace.openFrom(workspacePath) + projects <- theWorkspace.projects + _ <- reportProjectInfo(projects) + } yield ExitCode.Success).catchAll { error => + for { + _ <- console.putStrLn(s"Error enountered: $error") + } yield ExitCode.Failure + } + + private def reportProjectInfo(projects: Seq[Project]): ZIO[Console, Option[Nothing], Unit] = + for { + _ <- console.putStrLn("=========================================================================") + _ <- ZStream.fromIterable(projects).foreach(reportProjectInfo) + _ <- console.putStrLn("=========================================================================") + } yield () + + private def reportProjectInfo(project: Project): ZIO[Console, Nothing, Unit] = + for { + _ <- console.putStrLn(pprint.apply(s"|-Project:").render) + _ <- console.putStrLn(s" \\") + _ <- console.putStrLn(s" |-Name: ${project.name}") + _ <- console.putStrLn(s" |-Dir: ${project.projectDir}") + _ <- console.putStrLn(s" +-Sources: ") + _ <- reportSources(project.sources) + } yield () + + private def reportSources(sources: Seq[SourceFile[Any]]) = + for { + _ <- ZStream.fromIterable(sources).foreach(reportSourceInfo) + } yield () + + private def reportSourceInfo(source: SourceFile[Any]): ZIO[Console, Nothing, Unit] = + for { + _ <- console.putStrLn(s"${source.path}") + } yield () + +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/package.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/package.scala new file mode 100644 index 00000000..5464cdeb --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/package.scala @@ -0,0 +1,12 @@ +package org.morphir.toolbox + +import org.morphir.toolbox.workspace.WorkspaceModule +import zio.blocking.Blocking +import zio.clock.Clock +import zio.console.Console +import zio.random.Random +import zio.system.System + +package object cli { + type CliEnv = Clock with Console with System with Random with Blocking with WorkspaceModule +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ManifestFile.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ManifestFile.scala index f680666f..27461689 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ManifestFile.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ManifestFile.scala @@ -1,6 +1,7 @@ package org.morphir.toolbox.core -import java.io.{File, IOException} +import java.io.{ File, IOException } +import java.nio.charset.StandardCharsets import java.nio.file.Paths import org.morphir.core.newtype._ @@ -14,7 +15,7 @@ object ManifestFile extends Newtype[File] { val DefaultName: String = "morphir.toml" def fromDirectory( - workspaceDir: WorkspaceDir + workspaceDir: WorkspaceDir ): ZIO[Any, Throwable, ManifestFile] = workspaceDir .join(DefaultName) @@ -32,37 +33,40 @@ object ManifestFile extends Newtype[File] { else ZIO.effect(ManifestFile(file)) } - implicit class RichManifestFile(val manifestFile: ManifestFile) - extends AnyVal { + implicit class RichManifestFile(val manifestFile: ManifestFile) extends AnyVal { def toPath: UIO[Path] = ZIO.effectTotal { val file = ManifestFile.unwrap(manifestFile) Path.fromJava(file.toPath) } - def readAllBytes: ZIO[Blocking, IOException, Chunk[Byte]] = { + def readAllBytes: ZIO[Blocking, IOException, Chunk[Byte]] = for { path <- toPath data <- Files.readAllBytes(path) } yield data - } def readAllText: ZIO[Blocking, IOException, String] = - readAllBytes.map(_.mkString) + readAllBytes.map(chunk => new String(chunk.toArray, StandardCharsets.UTF_8)) def load: ZIO[Blocking, Throwable, WorkspaceSettings] = for { - contents <- readAllText - path <- toPath + contents <- readAllText + path <- toPath absolutePath <- path.toAbsolutePath settings <- ZIO - .fromEither(WorkspaceSettings.fromToml(contents)) - .mapError(e => - Errors.ManifestFileParseError( - e, - s"Failed to parse the morphir workspace manifest file at: $absolutePath." - ) - ) + .fromEither(WorkspaceSettings.fromToml(contents)) + .mapError { e => + val (_, message) = e + Errors.ManifestFileParseError( + e, + s"""Failed to parse the morphir workspace manifest file at: $absolutePath. + |Cause: + |$message + |Contents: + |$contents""".stripMargin + ) + } } yield settings } } diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectPath.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectPath.scala index 8969fa25..82f025c1 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectPath.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectPath.scala @@ -1,23 +1,25 @@ package org.morphir.toolbox.core -import java.nio.file.Path +import java.nio.file.{ Path, Paths } -import toml.{Codec, Value} +import toml.{ Codec, Value } case class ProjectPath(path: Path) extends AnyVal { def /(segment: String): ProjectPath = ProjectPath.of(path.toString, segment) + override def toString: String = path.toAbsolutePath.toString + } object ProjectPath { @inline def of(path: String, segments: String*): ProjectPath = - ProjectPath(Path.of(path, segments: _*)) + ProjectPath(Paths.get(path, segments: _*)) implicit val projectPathCodec: Codec[ProjectPath] = Codec { case (Value.Str(value), _, _) => - if (value.isBlank) + if (value.trim.isEmpty) Left(List.empty -> s"A non-empty path is expected $value provided") else Right(ProjectPath.of(value)) case (value, _, _) => diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala index 076b0459..09547e33 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala @@ -3,27 +3,29 @@ package org.morphir.toolbox.core import java.io.File import java.nio.file.Path -import org.morphir.toolbox.workspace.config.{ProjectSettings, WorkspaceSettings} +import org.morphir.toolbox.workspace.config.{ ProjectSettings, WorkspaceSettings } import org.morphir.toolbox.core.ManifestFile._ import zio._ import zio.blocking.Blocking case class Workspace( - rootDir: WorkspaceDir, - projects: Map[ProjectName, Project] + rootDir: WorkspaceDir, + projectMap: Map[ProjectName, Project] ) { def manifestFile: Task[File] = rootDir .join(Workspace.WorkspaceFilename) .flatMap(p => ZIO.effect(p.toFile)) + def projects: UIO[List[Project]] = UIO.succeed(projectMap.values.toList) + } object Workspace { val WorkspaceFilename = "morphir.toml" def fromSettings( - settings: WorkspaceSettings + settings: WorkspaceSettings )(implicit workspaceDir: WorkspaceDir): UIO[Workspace] = for { dir <- ZIO.succeed(workspaceDir) @@ -34,19 +36,19 @@ object Workspace { } } yield Workspace(dir, projects) - def load(path: Path): ZIO[Blocking, Throwable, Workspace] = + def load(path: Option[Path] = None): ZIO[Blocking, Throwable, Workspace] = for { workspacePath <- WorkspacePath.from(path) - workspaceDir <- WorkspaceDir.fromPath(workspacePath) - manifestFile <- ManifestFile.fromPath(workspacePath) - settings <- manifestFile.load - workspace <- fromSettings(settings)(workspaceDir) + workspaceDir <- WorkspaceDir.fromPath(workspacePath) + manifestFile <- ManifestFile.fromPath(workspacePath) + settings <- manifestFile.load + workspace <- fromSettings(settings)(workspaceDir) } yield workspace private[core] def createProject( - settigs: ProjectSettings, - name: String, - workspaceDir: WorkspaceDir + settigs: ProjectSettings, + name: String, + workspaceDir: WorkspaceDir ): Project = { val projectName = ProjectName(name, settigs.name) val projectDir = diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala index 44022adc..2d75b9cb 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala @@ -1,12 +1,12 @@ package org.morphir.toolbox.core -import java.nio.file.Path +import java.nio.file.{ Path, Paths } import zio._ final case class WorkspaceDir private (path: Path) extends AnyVal { def /[R](segment: String): RIO[R, WorkspaceDir] = - RIO.effect(Path.of(path.toString, segment).toFile).flatMap { file => + RIO.effect(Paths.get(path.toString, segment).toFile).flatMap { file => if (file.isDirectory) Task.effect(WorkspaceDir(file.toPath)) else RIO.fail( @@ -18,10 +18,10 @@ final case class WorkspaceDir private (path: Path) extends AnyVal { } def join(segments: String*): UIO[Path] = - ZIO.succeed(Path.of(path.toString, segments: _*)) + ZIO.succeed(Paths.get(path.toString, segments: _*)) def joinPath(segments: String*): Path = - Path.of(path.toString, segments: _*) + Paths.get(path.toString, segments: _*) } diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspacePath.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspacePath.scala index 51541a28..9a60478b 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspacePath.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspacePath.scala @@ -1,15 +1,15 @@ package org.morphir.toolbox.core -import java.nio.file.Path +import java.nio.file.{ Path, Paths } import org.morphir.core.newtype._ import zio._ object WorkspacePath extends Subtype[Path] { - def of(path: String): WorkspacePath = WorkspacePath(Path.of(path)) + def of(path: String): WorkspacePath = WorkspacePath(Paths.get(path)) def from(path: Option[Path] = None): Task[WorkspacePath] = - Task.effect(path.getOrElse(Path.of("."))).map(WorkspacePath(_)) + Task.effect(path.getOrElse(Paths.get("."))).map(WorkspacePath(_)) def from(path: Path): UIO[WorkspacePath] = ZIO.succeed(WorkspacePath(path)) @@ -25,8 +25,8 @@ object WorkspacePath extends Subtype[Path] { Task.effect(self.toFile.isDirectory) def ifIsDirectoryM[R, A]( - whenTrue: => RIO[R, A], - whenFalse: => RIO[R, A] + whenTrue: => RIO[R, A], + whenFalse: => RIO[R, A] ): RIO[R, A] = ZIO.ifM(isDirectory)(whenTrue, whenFalse) diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlSupport.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlSupport.scala index d39edbaf..e8f54485 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlSupport.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/codecs/TomlSupport.scala @@ -1,6 +1,6 @@ package org.morphir.toolbox.core.codecs -import java.nio.file.Path +import java.nio.file.{ Path, Paths } import org.morphir.toolbox.core.ProjectPath import toml._ @@ -14,9 +14,9 @@ trait TomlSupport { implicit val pathCodec: Codec[Path] = Codec { case (Value.Str(value), _, _) => - if (value.isBlank) + if (value.trim.isEmpty) Left(List.empty -> s"A non-empty path is expected $value provided") - else Right(Path.of(value)) + else Right(Paths.get(value)) case (value, _, _) => Left(List.empty -> s"A path is expected, $value provided") } diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingSettings.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingSettings.scala index 22c78eb5..3f73afaf 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingSettings.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingSettings.scala @@ -1,14 +1,14 @@ package org.morphir.toolbox.workspace.config -import java.nio.file.Path +import java.nio.file.{ Path, Paths } case class BindingSettings(srcDirs: List[Path], outDir: Path) object BindingSettings { object defaults { val elm: BindingSettings = BindingSettings( - srcDirs = List(Path.of("src/elm/")), - outDir = Path.of("out/") + srcDirs = List(Paths.get("src/elm/")), + outDir = Paths.get("out/") ) } } diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/package.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/package.scala index 7857b1a1..b16899c8 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/package.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/package.scala @@ -1,25 +1,27 @@ package org.morphir.toolbox import java.nio.file.Path + import core.Workspace import zio._ +import zio.blocking.Blocking package object workspace { type WorkspaceModule = Has[WorkspaceModule.Service] - def openFrom(path: Path): RIO[WorkspaceModule, Workspace] = + def openFrom(path: Option[Path] = None): RIO[WorkspaceModule, Workspace] = RIO.accessM[WorkspaceModule](_.get.openFrom(path)) object WorkspaceModule { trait Service { - def openFrom(path: Path): Task[Workspace] + def openFrom(path: Option[Path] = None): Task[Workspace] } - val live: ULayer[WorkspaceModule] = ZLayer.succeed(new Live()) + val live: RLayer[Blocking, WorkspaceModule] = ZLayer.fromFunction(env => new Live(env)) - class Live() extends Service { - def openFrom(path: Path): Task[Workspace] = ??? + class Live(blocking: Blocking) extends Service { + def openFrom(path: Option[Path] = None): Task[Workspace] = Workspace.load(path).provide(blocking) } } diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index eafd593f..0a93a569 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -1,6 +1,5 @@ -import sbt._ +import sbt.{ Def, _ } import sbt.Keys._ - import explicitdeps.ExplicitDepsPlugin.autoImport._ import sbtcrossproject.CrossPlugin.autoImport._ import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ @@ -11,10 +10,17 @@ import scalafix.sbt.ScalafixPlugin.autoImport.scalafixSemanticdb object BuildHelper { - private val Scala211 = "2.11.12" - private val Scala212 = "2.12.10" - private val Scala213 = "2.13.1" - val DottyVersion = "0.23.0-RC1" + object ScalaVersions { + val Scala211 = "2.11.12" + val Scala212 = "2.12.10" + val Scala213 = "2.13.1" + val Dotty = "0.23.0-RC1" + } + + private val Scala211 = ScalaVersions.Scala211 + private val Scala212 = ScalaVersions.Scala212 + private val Scala213 = ScalaVersions.Scala213 + private val DottyVersion = ScalaVersions.Dotty val isCIBuild: Boolean = { val res = sys.env.getOrElse("CI", "false") @@ -118,22 +124,20 @@ object BuildHelper { } def platformSpecificSources( - platform: String, - conf: String, - baseDirectory: File + platform: String, + conf: String, + baseDirectory: File )(versions: String*) = - List("scala" :: versions.toList.map("scala-" + _): _*) - .map { version => - baseDirectory.getParentFile / platform.toLowerCase / "src" / conf / version - } - .filter(_.exists) + List("scala" :: versions.toList.map("scala-" + _): _*).map { version => + baseDirectory.getParentFile / platform.toLowerCase / "src" / conf / version + }.filter(_.exists) def crossPlatformSources( - scalaVer: String, - platform: String, - conf: String, - baseDir: File, - isDotty: Boolean + scalaVer: String, + platform: String, + conf: String, + baseDir: File, + isDotty: Boolean ) = CrossVersion.partialVersion(scalaVer) match { case Some((2, x)) if x <= 11 => @@ -187,26 +191,26 @@ object BuildHelper { lazy val crossProjectSettings = Seq( Compile / unmanagedSourceDirectories ++= { val platform = crossProjectPlatform.value.identifier - val baseDir = baseDirectory.value + val baseDir = baseDirectory.value val scalaVer = scalaVersion.value - val isDot = isDotty.value + val isDot = isDotty.value crossPlatformSources(scalaVer, platform, "main", baseDir, isDot) }, Test / unmanagedSourceDirectories ++= { val platform = crossProjectPlatform.value.identifier - val baseDir = baseDirectory.value + val baseDir = baseDirectory.value val scalaVer = scalaVersion.value - val isDot = isDotty.value + val isDot = isDotty.value crossPlatformSources(scalaVer, platform, "test", baseDir, isDot) } ) - def stdSettings(prjName: String) = Seq( + def stdSettings(prjName: String, customScalaVersions: Option[Seq[String]] = None) = Seq( name := s"$prjName", scalacOptions := stdOptions, - crossScalaVersions := Seq(Scala213, Scala212, Scala211), + crossScalaVersions := customScalaVersions getOrElse Seq(Scala213, Scala212, Scala211), scalaVersion in ThisBuild := crossScalaVersions.value.head, scalacOptions := stdOptions ++ extraOptions( scalaVersion.value, @@ -396,7 +400,7 @@ object BuildHelper { if (isDotty.value) Seq() else Seq( - "org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided", + "org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided", "org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided" ) } diff --git a/project/metals.sbt b/project/metals.sbt index 6c823e72..a64ca305 100644 --- a/project/metals.sbt +++ b/project/metals.sbt @@ -1,4 +1,4 @@ // DO NOT EDIT! This file is auto-generated. // This file enables sbt-bloop to create bloop config files. -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.0-RC1-219-a3514983") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.0-RC1-190-ef7d8dba") From 4c641876a2adc0538f12b699606029672643f4ad Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Mon, 4 May 2020 19:42:49 -0400 Subject: [PATCH 4/5] Working on the elm binding --- .../src/main/scala/org/morphir/cli/Cli.scala | 6 ++-- .../toolbox/binding/FrontendBinding.scala | 16 ++++++--- .../binding/elm/ElmFrontendBinding.scala | 26 +++++++++++++- .../toolbox/binding/elm/ElmManifest.scala | 19 ++++++++++ .../morphir/toolbox/binding/elm/package.scala | 7 ++++ .../org/morphir/toolbox/cli/CliCommand.scala | 4 --- .../toolbox/cli/commands/BuildCommand.scala | 19 ++++++++++ .../cli/commands/WorkspaceInfoCommand.scala | 30 +++++++++++----- .../org/morphir/toolbox/core/Binding.scala | 5 +++ .../org/morphir/toolbox/core/Bindings.scala | 9 +++++ .../org/morphir/toolbox/core/Errors.scala | 21 ++++++++--- .../org/morphir/toolbox/core/Project.scala | 12 +++---- .../morphir/toolbox/core/ProjectInfo.scala | 3 ++ .../org/morphir/toolbox/core/Target.scala | 3 ++ .../morphir/toolbox/core/ToolboxContext.scala | 11 ++++++ .../org/morphir/toolbox/core/Workspace.scala | 26 +++++++++----- .../morphir/toolbox/core/WorkspaceDir.scala | 36 ++++++++++++++----- .../org/morphir/toolbox/core/package.scala | 2 +- .../workspace/config/BindingsSettings.scala | 2 +- .../workspace/config/ProjectSettings.scala | 6 ++-- .../toolbox/workspace/config/package.scala | 2 +- .../toolbox/binding/elm/ElmManifestSpec.scala | 28 +++++++++++++++ 22 files changed, 237 insertions(+), 56 deletions(-) create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmManifest.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/package.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/BuildCommand.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Binding.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Bindings.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectInfo.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Target.scala create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ToolboxContext.scala create mode 100644 morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/binding/elm/ElmManifestSpec.scala diff --git a/morphir/cli/shared/src/main/scala/org/morphir/cli/Cli.scala b/morphir/cli/shared/src/main/scala/org/morphir/cli/Cli.scala index 9ed9c4b5..aadaa454 100644 --- a/morphir/cli/shared/src/main/scala/org/morphir/cli/Cli.scala +++ b/morphir/cli/shared/src/main/scala/org/morphir/cli/Cli.scala @@ -4,7 +4,7 @@ import java.nio.file.Path import com.monovore.decline._ import org.morphir.toolbox.cli.CliCommand -import org.morphir.toolbox.cli.commands.WorkspaceInfoCommand +import org.morphir.toolbox.cli.commands.{ BuildCommand, WorkspaceInfoCommand } import zio.{ IO, ZIO } object Cli { @@ -16,11 +16,11 @@ object Cli { buildCommand orElse projectCommand orElse workspaceCommand ) - lazy val buildCommand: Opts[CliCommand.Build] = Opts + lazy val buildCommand: Opts[BuildCommand] = Opts .subcommand("build", help = "Build the workspace")( workspaceOpt.orNone ) - .map(CliCommand.Build) + .map(BuildCommand) lazy val projectCommand: Opts[CliCommand.ProjectList] = Opts.subcommand("project", help = "Work with projects")( Opts diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/FrontendBinding.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/FrontendBinding.scala index c1c82171..ccc0d53c 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/FrontendBinding.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/FrontendBinding.scala @@ -1,11 +1,14 @@ package org.morphir.toolbox.binding + import zio.stream._ -import org.morphir.toolbox.binding.FrontendBinding.{InputPort, OutputPort} +import org.morphir.toolbox.binding.FrontendBinding.{ FrontendCommand, FrontendEvent, InputPort, OutputPort } +import org.morphir.toolbox.core.ProjectInfo case class FrontendBinding[S]( - input: InputPort[Any], - output: OutputPort[Any], - initialState: S + name: String, + input: InputPort[FrontendCommand[Any]], + output: OutputPort[FrontendEvent[Any]], + initialState: S ) object FrontendBinding { @@ -15,5 +18,10 @@ object FrontendBinding { type OutputPort[+A] = Stream[Nothing, FrontendEvent[A]] sealed trait FrontendCommand[+A] + object FrontendCommand { + final case class ConnectProject(project: ProjectInfo) extends FrontendCommand[Nothing] + } + sealed trait FrontendEvent[+A] + object FrontendEvent {} } diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmFrontendBinding.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmFrontendBinding.scala index 96db890f..7a86ef43 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmFrontendBinding.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmFrontendBinding.scala @@ -1,3 +1,27 @@ package org.morphir.toolbox.binding.elm -class ElmFrontendBinding {} +import org.morphir.toolbox.binding.FrontendBinding +import org.morphir.toolbox.core.{ Errors, Project } +import zio._ +import zio.stream._ + +object ElmFrontendBinding { + val BindingName = "elm" + + sealed trait ElmFrontendState + object ElmFrontendState { + final case class New(project: Project) extends ElmFrontendState + } + + def make(project: Project): Task[ElmFrontendBinding] = + ZIO.succeed(FrontendBinding(BindingName, Stream.empty, Stream.empty, ElmFrontendState.New(project))) + + private[elm] def createMorphirElmManifest(project: Project): ZIO[Any, Errors.NoSuchBinding, ElmManifest] = + for { + name <- ZIO.succeed(project.name) + binding <- project.bindings.getAsEffect(BindingName) + srcDir = binding.srcDirs.head + manifest = ElmManifest(name.toString, srcDir, List.empty) + } yield manifest + +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmManifest.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmManifest.scala new file mode 100644 index 00000000..965b5781 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmManifest.scala @@ -0,0 +1,19 @@ +package org.morphir.toolbox.binding.elm + +import io.circe.{ Decoder, Encoder } +import java.nio.file.{ Path, Paths } + +case class ElmManifest(name: String, sourceDirectory: Path, exposedModules: List[String]) {} + +object ElmManifest { + implicit val elmManifestEncoder: Encoder[ElmManifest] = + Encoder.forProduct3("name", "sourceDirectory", "exposedModules")(manifest => + (manifest.name, manifest.sourceDirectory.toString, manifest.exposedModules) + ) + + implicit val elmManifestDecoder: Decoder[ElmManifest] = + Decoder.forProduct3[ElmManifest, String, String, List[String]]("name", "sourceDirectory", "exposedModules") { + case (name, sourceDirectory, exposedModules) => + ElmManifest(name, Paths.get(sourceDirectory), exposedModules) + } +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/package.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/package.scala new file mode 100644 index 00000000..48a69581 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/package.scala @@ -0,0 +1,7 @@ +package org.morphir.toolbox.binding + +import org.morphir.toolbox.binding.elm.ElmFrontendBinding.ElmFrontendState + +package object elm { + type ElmFrontendBinding = FrontendBinding[ElmFrontendState] +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliCommand.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliCommand.scala index e245f255..4e39b974 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliCommand.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliCommand.scala @@ -10,10 +10,6 @@ abstract class CliCommand extends Product { object CliCommand { - final case class Build(workspacePath: Option[Path]) extends CliCommand { - val execute: ZIO[CliEnv, Nothing, ExitCode] = - console.putStrLn(s"Executed: $this") *> ExitCode.success - } final case class ProjectList(workspacePath: Option[Path]) extends CliCommand { val execute: ZIO[CliEnv, Nothing, ExitCode] = console.putStrLn(s"Executed: $this") *> ExitCode.success diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/BuildCommand.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/BuildCommand.scala new file mode 100644 index 00000000..7413b206 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/BuildCommand.scala @@ -0,0 +1,19 @@ +package org.morphir.toolbox.cli.commands + +import java.nio.file.Path + +import org.morphir.toolbox.cli.{ CliCommand, CliEnv, ExitCode } +import org.morphir.toolbox.workspace +import zio.{ console, ZIO } + +final case class BuildCommand(workspacePath: Option[Path]) extends CliCommand { + override def execute: ZIO[CliEnv, Nothing, ExitCode] = + (for { + theWorkspace <- workspace.openFrom(workspacePath) + _ <- theWorkspace.projects + } yield ExitCode.Success).catchAll { error => + for { + _ <- console.putStrLn(s"Error enountered: $error") + } yield ExitCode.Failure + } +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/WorkspaceInfoCommand.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/WorkspaceInfoCommand.scala index 3e061dc6..184dc52b 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/WorkspaceInfoCommand.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/WorkspaceInfoCommand.scala @@ -2,8 +2,9 @@ package org.morphir.toolbox.cli.commands import java.nio.file.Path +import fansi.Color import org.morphir.toolbox.cli.{ CliCommand, CliEnv, ExitCode } -import org.morphir.toolbox.core.{ Project, SourceFile } +import org.morphir.toolbox.core.{ Binding, Bindings, Project } import org.morphir.toolbox.workspace import zio._ import zio.console.Console @@ -29,22 +30,33 @@ final case class WorkspaceInfoCommand(workspacePath: Option[Path]) extends CliCo private def reportProjectInfo(project: Project): ZIO[Console, Nothing, Unit] = for { - _ <- console.putStrLn(pprint.apply(s"|-Project:").render) + _ <- console.putStrLn(s"|-${Color.Green("Project")}:") _ <- console.putStrLn(s" \\") - _ <- console.putStrLn(s" |-Name: ${project.name}") - _ <- console.putStrLn(s" |-Dir: ${project.projectDir}") - _ <- console.putStrLn(s" +-Sources: ") - _ <- reportSources(project.sources) + _ <- console.putStrLn(s" |-${Color.Yellow("Name")}: ${Color.Blue(project.name.toString)}") + _ <- console.putStrLn(s" |-${Color.Yellow("Dir")}: ${Color.Blue(project.projectDir.toString)}") + _ <- console.putStrLn(s" +-${Color.Yellow("Bindings")}: ") + _ <- reportBindings(project.bindings) } yield () - private def reportSources(sources: Seq[SourceFile[Any]]) = + private def reportBindings(bindings: Bindings) = + ZStream.fromIterable(bindings.value.values).foreach(reportBinding) + + private def reportBinding(binding: Binding) = + for { + _ <- console.putStrLn(s" |-${Color.Yellow("Binding")}:") + _ <- console.putStrLn(s" \\-${Color.Yellow("Name")}: ${Color.Blue(binding.name.toString)}") + _ <- console.putStrLn(s" |-${Color.Yellow("Sources")}:") + _ <- reportSources(binding.srcDirs) + } yield () + + private def reportSources(sources: Seq[Path]) = for { _ <- ZStream.fromIterable(sources).foreach(reportSourceInfo) } yield () - private def reportSourceInfo(source: SourceFile[Any]): ZIO[Console, Nothing, Unit] = + private def reportSourceInfo(sourceDir: Path): ZIO[Console, Nothing, Unit] = for { - _ <- console.putStrLn(s"${source.path}") + _ <- console.putStrLn(s" - ${Color.Blue(sourceDir.toAbsolutePath.toString)}") } yield () } diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Binding.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Binding.scala new file mode 100644 index 00000000..fbf35f44 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Binding.scala @@ -0,0 +1,5 @@ +package org.morphir.toolbox.core + +import java.nio.file.Path + +case class Binding(name: String, srcDirs: List[Path], outDir: Path) diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Bindings.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Bindings.scala new file mode 100644 index 00000000..e0a9ef61 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Bindings.scala @@ -0,0 +1,9 @@ +package org.morphir.toolbox.core + +import zio.ZIO + +final case class Bindings(value: Map[String, Binding]) extends AnyVal { + def getAsEffect(name: String): ZIO[Any, Errors.NoSuchBinding, Binding] = + ZIO.fromOption(value.get(name)).orElseFail(Errors.NoSuchBinding(name)) +} +object Bindings {} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Errors.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Errors.scala index 5a70fae6..30b845d4 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Errors.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Errors.scala @@ -8,14 +8,25 @@ import toml.Parse object Errors { final case class PathIsNotADirectoryError( - path: Path, - message: String, - cause: Option[IOException] = None + path: Path, + message: String, + cause: Option[IOException] = None ) extends IOException(message, cause.orNull) final case class ManifestFileParseError( - details: Parse.Error, - message: String = "Failed to parse morphir workspace manifest file." + details: Parse.Error, + message: String = "Failed to parse morphir workspace manifest file." ) extends Exception(message) + final case class NoSuchBinding( + name: String, + message: String + ) extends Exception(message) { + def this(name: String) = + this(name, s"A Binding by the name of $name was not found.") + } + + object NoSuchBinding { + def apply(name: String): NoSuchBinding = new NoSuchBinding(name) + } } diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Project.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Project.scala index 0ac3a5f3..c607e6b2 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Project.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Project.scala @@ -3,14 +3,14 @@ package org.morphir.toolbox.core import java.nio.file.Path case class Project( - name: ProjectName, - projectDir: ProjectPath, - targets: List[Target[Any]], - sources: List[SourceFile[Any]] -) + name: ProjectName, + projectDir: ProjectPath, + bindings: Bindings, + targets: List[Target[Any]], + sources: List[SourceFile[Any]] +) {} object Project {} -case class Target[A](artifacts: List[Artifact[A]]) case class Artifact[A](path: Path, manifest: ArtifactManifest, data: A) case class ArtifactManifest() diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectInfo.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectInfo.scala new file mode 100644 index 00000000..9b0e959a --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectInfo.scala @@ -0,0 +1,3 @@ +package org.morphir.toolbox.core + +case class ProjectInfo(name: ProjectName, projectDir: ProjectPath, workspaceDir: WorkspaceDir) diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Target.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Target.scala new file mode 100644 index 00000000..850d098e --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Target.scala @@ -0,0 +1,3 @@ +package org.morphir.toolbox.core + +case class Target[A](name: String, artifacts: List[Artifact[A]]) diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ToolboxContext.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ToolboxContext.scala new file mode 100644 index 00000000..73e7feb0 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ToolboxContext.scala @@ -0,0 +1,11 @@ +package org.morphir.toolbox.core + +import zio._ + +sealed abstract class ToolboxContext +object ToolboxContext { + private case class UnverifiedContext(workspaceDir: WorkspaceDir) extends ToolboxContext + + def make(workspaceDir: WorkspaceDir): UIO[ToolboxContext] = + ZIO.succeed(UnverifiedContext(workspaceDir)) +} diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala index 09547e33..3347075e 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala @@ -1,7 +1,7 @@ package org.morphir.toolbox.core import java.io.File -import java.nio.file.Path +import java.nio.file.{ Path, Paths } import org.morphir.toolbox.workspace.config.{ ProjectSettings, WorkspaceSettings } import org.morphir.toolbox.core.ManifestFile._ @@ -46,16 +46,24 @@ object Workspace { } yield workspace private[core] def createProject( - settigs: ProjectSettings, + settings: ProjectSettings, name: String, workspaceDir: WorkspaceDir ): Project = { - val projectName = ProjectName(name, settigs.name) - val projectDir = - settigs.projectDir getOrElse ProjectPath.of(projectName.resolvedName) - val projectPath = ProjectPath( - workspaceDir.joinPath(projectDir.path.toString) - ) - Project(projectName, projectPath, List.empty, List.empty) + val projectName = ProjectName(name, settings.name) + val projectDir = { + val prjPath: ProjectPath = settings.projectDir getOrElse ProjectPath.of(projectName.resolvedName) + workspaceDir.joinPath(prjPath.path) + } + val projectPath = ProjectPath(projectDir) + val bindings: Map[String, Binding] = settings.bindings.map { + case (name, bindingSettings) => + name -> Binding( + name, + bindingSettings.srcDirs.map(dir => Paths.get(projectDir.toString, dir.toString)), + outDir = Paths.get(projectDir.toString, bindingSettings.outDir.toString) + ) + } + Project(projectName, projectPath, Bindings(bindings), List.empty, List.empty) } } diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala index 2d75b9cb..e993e7e5 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala @@ -2,8 +2,11 @@ package org.morphir.toolbox.core import java.nio.file.{ Path, Paths } +import io.circe.{ Decoder, Encoder } import zio._ +import scala.util.Try + final case class WorkspaceDir private (path: Path) extends AnyVal { def /[R](segment: String): RIO[R, WorkspaceDir] = RIO.effect(Paths.get(path.toString, segment).toFile).flatMap { file => @@ -23,17 +26,32 @@ final case class WorkspaceDir private (path: Path) extends AnyVal { def joinPath(segments: String*): Path = Paths.get(path.toString, segments: _*) + def joinPath(segment: Path, rest: Path*): Path = + Paths.get(path.toString, (segment :: rest.toList).map(_.toString): _*) + } object WorkspaceDir { - def fromPath[R](path: Path): RIO[R, WorkspaceDir] = - RIO.ifM(Task.effect(path.toFile.isDirectory))( - RIO.effect(WorkspaceDir(path)), - RIO.fail( - Errors.PathIsNotADirectoryError( - path, - s"The workspace directory must point at a directory, but $path is not a directory." + + implicit val encodeWorkspaceDir: Encoder[WorkspaceDir] = Encoder.encodeString.contramap(dir => dir.toString) + implicit val decodeWorkspaceDir: Decoder[WorkspaceDir] = Decoder.decodeString.emapTry(tryGet(_)) + + def tryGet(segment: String, otherSegments: String*): Try[WorkspaceDir] = + Try(Paths.get(segment, otherSegments: _*)).flatMap(tryMakeFromPath) + + def tryMakeFromPath(path: Path): Try[WorkspaceDir] = + Try(path.toFile).flatMap { file => + if (file.isDirectory) + scala.util.Success(WorkspaceDir(path)) + else + scala.util.Failure( + Errors.PathIsNotADirectoryError( + file.toPath, + s"The workspace directory must point at a directory, but $path is not a directory." + ) ) - ) - ) + } + + def fromPath[R](path: Path): Task[WorkspaceDir] = + ZIO.fromTry(tryMakeFromPath(path)) } diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/package.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/package.scala index 77d9c985..b8a49dca 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/package.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/package.scala @@ -3,5 +3,5 @@ package org.morphir.toolbox package object core { type WorkspacePath = WorkspacePath.WrappedType - type ManifestFile = ManifestFile.WrappedType + type ManifestFile = ManifestFile.WrappedType } diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingsSettings.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingsSettings.scala index 308462bd..a60c64cf 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingsSettings.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/BindingsSettings.scala @@ -1,7 +1,7 @@ package org.morphir.toolbox.workspace.config object BindingsSettings { - val default: BindingsConfig = Map( + val default: BindingsSettings = Map( "elm" -> BindingSettings.defaults.elm ) } diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/ProjectSettings.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/ProjectSettings.scala index 6945935b..69d9ccee 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/ProjectSettings.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/ProjectSettings.scala @@ -3,7 +3,7 @@ package org.morphir.toolbox.workspace.config import org.morphir.toolbox.core.ProjectPath case class ProjectSettings( - name: Option[String] = None, - projectDir: Option[ProjectPath] = None, - bindings: Map[String, BindingSettings] = BindingsSettings.default + name: Option[String] = None, + projectDir: Option[ProjectPath] = None, + bindings: BindingsSettings = BindingsSettings.default ) diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/package.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/package.scala index 2825b0b3..8bd8c892 100644 --- a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/package.scala +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/config/package.scala @@ -1,5 +1,5 @@ package org.morphir.toolbox.workspace package object config { - type BindingsConfig = Map[String, BindingSettings] + type BindingsSettings = Map[String, BindingSettings] } diff --git a/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/binding/elm/ElmManifestSpec.scala b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/binding/elm/ElmManifestSpec.scala new file mode 100644 index 00000000..20f89346 --- /dev/null +++ b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/binding/elm/ElmManifestSpec.scala @@ -0,0 +1,28 @@ +package org.morphir.toolbox.binding.elm + +import java.nio.file.Paths + +import io.circe.Json +import io.circe.syntax._ +import zio.test._ +import zio.test.Assertion._ + +object ElmManifestSpec extends DefaultRunnableSpec { + def spec = suite("ElmManifest Spec")( + suite("Serializing to JSON")( + test("An ElmManifest should serialize to JSON") { + val manifest = ElmManifest("hello-world", Paths.get("./src/elm"), List("Hello")) + val json = manifest.asJson + assert(json)( + equalTo( + Json.obj( + "name" -> Json.fromString("hello-world"), + "sourceDirectory" -> Json.fromString("./src/elm"), + "exposedModules" -> Json.arr(Json.fromString("Hello")) + ) + ) + ) + } + ) + ) +} From 7a1e16778d4250c4bd4238b59a9b80c8d626cdec Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Tue, 5 May 2020 17:06:29 -0400 Subject: [PATCH 5/5] Working on Path handling --- .../scala/org/morphir/toolbox/io/Path.scala | 403 ++++++++++++++++++ .../org/morphir/toolbox/io/PathSpec.scala | 60 +++ .../morphir/toolbox/io/PosixPathSpec.scala | 157 +++++++ .../morphir/toolbox/io/WindowsPathSpec.scala | 29 ++ 4 files changed, 649 insertions(+) create mode 100644 morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/io/Path.scala create mode 100644 morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/PathSpec.scala create mode 100644 morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/PosixPathSpec.scala create mode 100644 morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/WindowsPathSpec.scala diff --git a/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/io/Path.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/io/Path.scala new file mode 100644 index 00000000..96fa2efc --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/io/Path.scala @@ -0,0 +1,403 @@ +package org.morphir.toolbox.io + +import cats.data.NonEmptyList +import cats.{ Order, Show } +import cats.implicits._ +import io.circe.{ Decoder, Encoder } + +import scala.annotation.tailrec + +sealed trait Path[+B, +T, +S] { + def isAbsolute: Boolean + def isRelative: Boolean = !isAbsolute +} + +object Path { + sealed trait Relative + sealed trait Absolute + + sealed trait File + sealed trait Dir + + sealed trait Sandboxed + sealed trait Unsandboxed + + final case class FileName(value: String) extends AnyVal { + def extension: String = { + val index = value.lastIndexOf('.') + if (index == -1) "" else value.substring(index + 1) + } + + def withoutExtension: String = { + val index = value.lastIndexOf(".") + if (index == -1) this.value else value.substring(0, index) + } + + def FileNameWithoutExtension: FileName = { + val index = value.lastIndexOf(".") + if (index == -1) this else FileName(value.substring(0, index)) + } + + def changeExtension(f: String => String): FileName = + FileName(withoutExtension + "." + f(extension)) + + def changeExtension(newExtension: String): FileName = + FileName(withoutExtension + "." + newExtension) + } + + object FileName { + + implicit val encodeFileNme: Encoder[FileName] = Encoder.encodeString.contramap(fn => fn.value) + + implicit val decodeFileName: Decoder[FileName] = Decoder.decodeString.map(FileName(_)) + + implicit val show: Show[FileName] = Show.show(_.value) + implicit val order: Order[FileName] = Order.by(_.value) + } + + final case class DirName(value: String) extends AnyVal + object DirName { + + implicit val encodeDirName: Encoder[DirName] = Encoder.encodeString.contramap(fn => fn.value) + + implicit val decodeDirName: Decoder[DirName] = Decoder.decodeString.map(DirName(_)) + + implicit val show: Show[DirName] = Show.show(_.value) + implicit val order: Order[DirName] = Order.by(_.value) + } + + private case object Current extends Path[Nothing, Nothing, Nothing] { + def isAbsolute = false + } + + private case object Root extends Path[Nothing, Nothing, Nothing] { + def isAbsolute = true + } + + private final case class InternalParent[B, T, S](parent: Path[B, T, S]) extends Path[B, T, S] { + def isAbsolute: Boolean = parent.isAbsolute + } + + private final case class InternalDir[B, T, S](parent: Path[B, T, S], name: DirName) extends Path[B, T, S] { + def isAbsolute: Boolean = parent.isAbsolute + } + + private final case class InternalFile[B, T, S](parent: Path[B, T, S], name: FileName) extends Path[B, T, S] { + def isAbsolute: Boolean = parent.isAbsolute + } + + type RelativeFile[S] = Path[Relative, File, S] + type AbsoluteFile[S] = Path[Absolute, File, S] + type RelativeDir[S] = Path[Relative, Dir, S] + type AbsoluteDir[S] = Path[Absolute, Dir, S] + + def currentDir[S]: RelativeDir[S] = Current + def rootDir[S]: AbsoluteDir[S] = Root + def file[S](name: String): RelativeFile[S] = file1[S](FileName(name)) + def file1[S](name: FileName): RelativeFile[S] = InternalFile(Current, name) + + def fileName[B, S](path: Path[B, File, S]): FileName = path match { + case InternalFile(_, name) => name + case _ => sys.error("impossible!") + } + + def dir[S](name: String): Path[Relative, Dir, S] = dir1[S](DirName(name)) + def dir1[S](name: DirName): Path[Relative, Dir, S] = InternalDir(Current, name) + def dirname[B, S](path: Path[B, Dir, S]): Option[DirName] = path match { + case InternalDir(_, name) => Some(name) + case _ => None + } + + implicit class PathOps[B, T, S](path: Path[B, T, S]) { + def relativeTo[SS](dir: Path[B, Dir, SS]): Option[Path[Relative, T, SS]] = { + @SuppressWarnings(Array("org.wartremover.warts.Recursion")) + def go[TT](p1: Path[B, TT, S], p2: Path[B, Dir, SS]): Option[Path[Relative, TT, SS]] = + if (identicalPath(p1, p2)) Some(Current) + else + peel(p1) match { + case None => + (p1, p2) match { + case (Root, Root) => Some(Current) + case (Current, Current) => Some(Current) + case _ => None + } + case Some((p1p, v)) => + go(p1p, p2).map(p => + p v.fold[Path[Relative, TT, SS]](InternalDir(Current, _), InternalFile(Current, _)) + ) + } + go(canonicalize(path), canonicalize(dir)) + } + } + + implicit class DirOps[B, S](dir: Path[B, Dir, S]) { + def [T](rel: Path[Relative, T, S]): Path[B, T, S] = + (dir, rel) match { + case (Current, Current) => Current + case (Root, Current) => Root + case (InternalParent(p1), Current) => InternalParent(p1 Current) + case (InternalFile(p1, f1), Current) => InternalFile(p1 Current, f1) + case (InternalDir(p1, d1), Current) => InternalDir(p1 Current, d1) + + // these don't make sense, but cannot exist anyway + case (Current, Root) => Current + case (Root, Root) => Root + case (InternalParent(p1), Root) => InternalParent(p1 Current) + case (InternalFile(p1, f1), Root) => InternalFile(p1 Current, f1) + case (InternalDir(p1, d1), Root) => InternalDir(p1 Current, d1) + + case (p1, InternalParent(p2)) => InternalParent(p1 p2) + case (p1, InternalFile(p2, f2)) => InternalFile(p1 p2, f2) + case (p1, InternalDir(p2, d2)) => InternalDir(p1 p2, d2) + } + + // NB: scala doesn't cotton to `<..>` + def <::>[T](rel: Path[Relative, T, S]): Path[B, T, Unsandboxed] = + parentDir1(dir) unsandbox(rel) + + @inline def up[T](rel: Path[Relative, T, S]): Path[B, T, Unsandboxed] = + <::>(rel) + } + + implicit class FileOps[B, S](file: Path[B, File, S]) { + // NB: scala doesn't cotton to `<.>` + def <:>(ext: String): Path[B, File, S] = + renameFile(file, name => name.changeExtension(_ => ext)) + } + + def refineType[B, T, S](path: Path[B, T, S]): Either[Path[B, Dir, S], Path[B, File, S]] = path match { + case Current => Left(Current) + case Root => Left(Root) + case InternalParent(p) => Left(InternalParent(unsafeCoerceType(p))) + case InternalFile(p, f) => Right(InternalFile(unsafeCoerceType(p), f)) + case InternalDir(p, d) => Left(InternalDir(unsafeCoerceType(p), d)) + } + + def maybeDir[B, T, S](path: Path[B, T, S]): Option[Path[B, Dir, S]] = + refineType(path).swap.toOption + + def maybeFile[B, T, S](path: Path[B, T, S]): Option[Path[B, File, S]] = + refineType(path).toOption + + @scala.annotation.tailrec + def peel[B, T, S](path: Path[B, T, S]): Option[(Path[B, Dir, S], Either[DirName, FileName])] = path match { + case Current => None + case Root => None + case p @ InternalParent(_) => + val (c, p1) = canonicalize1(p) + if (c) peel(p1) else None + case InternalDir(p, d) => Some(unsafeCoerceType(p) -> Left(d)) + case InternalFile(p, f) => Some(unsafeCoerceType(p) -> Right(f)) + } + + def depth[B, T, S](path: Path[B, T, S]): Int = path match { + case Current => 0 + case Root => 0 + case InternalParent(p) => depth(p) - 1 + case InternalFile(p, _) => depth(p) + 1 + case InternalDir(p, _) => depth(p) + 1 + } + + def identicalPath[B, T, S, BB, TT, SS](p1: Path[B, T, S], p2: Path[BB, TT, SS]): Boolean = + p1.show == p2.show + + def parentDir[B, T, S](path: Path[B, T, S]): Option[Path[B, Dir, S]] = + peel(path).map(_._1) + + def fileParent[B, S](file: Path[B, File, S]): Path[B, Dir, S] = file match { + case InternalFile(p, _) => unsafeCoerceType(p) + case _ => sys.error("impossible!") + } + + def unsandbox[B, T, S](path: Path[B, T, S]): Path[B, T, Unsandboxed] = path match { + case Current => Current + case Root => Root + case InternalParent(p) => InternalParent(unsandbox(p)) + case InternalDir(p, d) => InternalDir(unsandbox(p), d) + case InternalFile(p, f) => InternalFile(unsandbox(p), f) + } + + def sandbox[B, T, S](dir: Path[B, Dir, Sandboxed], path: Path[B, T, S]): Option[Path[Relative, T, Sandboxed]] = + path relativeTo dir + + def parentDir1[B, T, S](path: Path[B, T, S]): Path[B, Dir, Unsandboxed] = + InternalParent(unsafeCoerceType(unsandbox(path))) + + private def unsafeCoerceType[B, T, TT, S](path: Path[B, T, S]): Path[B, TT, S] = path match { + case Current => Current + case Root => Root + case InternalParent(p) => InternalParent(unsafeCoerceType(p)) + case InternalDir(p, d) => InternalDir(unsafeCoerceType(p), d) + case InternalFile(p, f) => InternalFile(unsafeCoerceType(p), f) + } + + def renameFile[B, S](path: Path[B, File, S], f: FileName => FileName): Path[B, File, S] = + path match { + case InternalFile(p, f0) => InternalFile(p, f(f0)) + case p => p + } + + def renameDir[B, S](path: Path[B, Dir, S], f: DirName => DirName): Path[B, Dir, S] = + path match { + case InternalDir(p, d) => InternalDir(p, f(d)) + case p => p + } + + def canonicalize[B, T, S](path: Path[B, T, S]): Path[B, T, S] = + canonicalize1(path)._2 + + private def canonicalize1[B, T, S](path: Path[B, T, S]): (Boolean, Path[B, T, S]) = + path match { + case Current => false -> Current + case Root => false -> Root + case InternalParent(InternalFile(p, _)) => true -> canonicalize1(p)._2 + case InternalParent(InternalDir(p, _)) => true -> canonicalize1(p)._2 + case InternalParent(p) => + val (ch, p1) = canonicalize1(p) + val p2 = InternalParent(p1) + if (ch) canonicalize1(p2) else ch -> p2 // ??? + case InternalFile(p, f) => + val (ch, p1) = canonicalize1(p) + ch -> InternalFile(p1, f) + case InternalDir(p, d) => + val (ch, p1) = canonicalize1(p) + ch -> InternalDir(p1, d) + } + + def flatten[X]( + root: => X, + currentDir: => X, + parentDir: => X, + dirName: String => X, + fileName: String => X, + path: Path[_, _, _] + ): NonEmptyList[X] = { + @tailrec + def go(xs: NonEmptyList[X], at: Path[_, _, _]): NonEmptyList[X] = { + val tl = xs.head :: xs.tail + + at match { + case Current => NonEmptyList(currentDir, tl) + case Root => NonEmptyList(root, tl) + case InternalParent(p) => go(NonEmptyList(parentDir, tl), p) + case InternalDir(p, d) => go(NonEmptyList(dirName(d.value), tl), p) + case InternalFile(p, f) => go(NonEmptyList(fileName(f.value), tl), p) + } + } + + path match { + case Current => NonEmptyList(currentDir, List.empty) + case Root => NonEmptyList(root, List.empty) + case InternalParent(p) => go(NonEmptyList(parentDir, List.empty), p) + case InternalDir(p, d) => go(NonEmptyList(dirName(d.value), List.empty), p) + case InternalFile(p, f) => go(NonEmptyList(fileName(f.value), List.empty), p) + } + } + + val posixCodec: PathCodec = PathCodec placeholder '/' + val windowsCodec: PathCodec = PathCodec placeholder '\\' + + final case class PathCodec(separator: Char, escape: String => String, unescape: String => String) { + def unsafePrintPath(path: Path[_, _, _]): String = { + val s: String = flatten("", ".", "..", escape, escape, path) + .intercalate(separator.toString) + + maybeDir(path) match { + case Some(_) => show"$s$separator" + case None => s + } + } + + def printPath[B, T](path: Path[B, T, Sandboxed]): String = + unsafePrintPath(path) + + def parsePath[Z]( + rf: RelativeFile[Unsandboxed] => Z, + af: AbsoluteFile[Unsandboxed] => Z, + rd: RelativeDir[Unsandboxed] => Z, + ad: AbsoluteDir[Unsandboxed] => Z + )(str: String): Z = { + + val segs = str.split(separator) + val isAbs = str.startsWith(separator.toString) + val isDir = + List(separator.toString, s"$separator.", s"$separator..").exists(str.endsWith) || str === "." || str === ".." + + def folder[B, S](base: Path[B, Dir, S], segments: String): Path[B, Dir, S] = segments match { + case "" => base + case "." => base + case ".." => InternalParent(base) + case seg => base dir(unescape(seg)) + } + + if (str === "") + rd(Current) + else if (isAbs && !isDir) + af( + segs.init + .foldLeft[AbsoluteDir[Unsandboxed]](rootDir[Unsandboxed])(folder) file[Unsandboxed](unescape(segs.last)) + ) + else if (isAbs && isDir) + ad(segs.foldLeft[AbsoluteDir[Unsandboxed]](rootDir[Unsandboxed])(folder)) + else if (!isAbs && !isDir) + rf(segs.init.foldLeft[RelativeDir[Unsandboxed]](Current)(folder) file[Unsandboxed](unescape(segs.last))) + else + rd(segs.foldLeft[RelativeDir[Unsandboxed]](Current)(folder)) + + } + + val parseRelFile: String => Option[RelativeFile[Unsandboxed]] = + parsePath[Option[RelativeFile[Unsandboxed]]](Some(_), _ => None, _ => None, _ => None) + + val parseAbsFile: String => Option[AbsoluteFile[Unsandboxed]] = + parsePath[Option[AbsoluteFile[Unsandboxed]]](_ => None, Some(_), _ => None, _ => None) + + val parseRelDir: String => Option[RelativeDir[Unsandboxed]] = + parsePath[Option[RelativeDir[Unsandboxed]]](_ => None, _ => None, Some(_), _ => None) + + val parseAbsDir: String => Option[AbsoluteDir[Unsandboxed]] = + parsePath[Option[AbsoluteDir[Unsandboxed]]](_ => None, _ => None, _ => None, Some(_)) + + private def asDir[B, S](path: Path[B, File, S]): Path[B, Dir, S] = path match { + case InternalFile(p, FileName(n)) => InternalDir(unsafeCoerceType(p), DirName(n)) + case _ => sys.error("impossible!") + } + + val parseRelAsDir: String => Option[RelativeDir[Unsandboxed]] = + parsePath[Option[RelativeDir[Unsandboxed]]](p => Some(asDir(p)), _ => None, Some(_), _ => None) + + val parseAbsAsDir: String => Option[AbsoluteDir[Unsandboxed]] = + parsePath[Option[AbsoluteDir[Unsandboxed]]](_ => None, p => Some(asDir(p)), _ => None, Some(_)) + + implicit def encodePath[B, T, S]: Encoder[Path[B, T, S]] = Encoder.encodeString.contramap(p => unsafePrintPath(p)) + } + + object PathCodec { + + def placeholder(sep: Char): PathCodec = { + val escapeSep = (_: String).replaceAllLiterally(sep.toString, $sep$) + val unescapeSep = (_: String).replaceAllLiterally($sep$, sep.toString) + + PathCodec(sep, escapeRel compose escapeSep, unescapeSep compose unescapeRel) + } + + private val escapeRel = (s: String) => if (s === "..") $dotdot$ else if (s === ".") $dot$ else s + + private val unescapeRel = (s: String) => if (s === $dotdot$) ".." else if (s == $dot$) "." else s + + private val $sep$ = "$sep$" + private val $dot$ = "$dot$" + private val $dotdot$ = "$dotdot$" + } + + implicit def pathShow[B, T, S]: Show[Path[B, T, S]] = new Show[Path[B, T, S]] { + override def show(t: Path[B, T, S]): String = t match { + case Current => "currentDir" + case Root => "rootDir" + case InternalParent(p) => show"parentDir($p)" + case InternalDir(p, DirName(d)) => show"$p dir($d)" + case InternalFile(p, FileName(f)) => show"$p file($f)" + } + } + +} diff --git a/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/PathSpec.scala b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/PathSpec.scala new file mode 100644 index 00000000..e17eac65 --- /dev/null +++ b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/PathSpec.scala @@ -0,0 +1,60 @@ +package org.morphir.toolbox.io + +import io.circe.Json +import io.circe.syntax._ +import io.circe.parser.decode +import org.morphir.toolbox.io.Path.FileName +import zio.test._ +import zio.test.Assertion._ + +object PathSpec extends DefaultRunnableSpec { + + def spec = suite("Path Spec")( + suite("FileName Spec")( + suite("Changing extension")( + test("Directly to .txt")( + assert(FileName("README.md").changeExtension("txt"))(equalTo(FileName("README.txt"))) + ), + test("Based on current")( + assert(FileName("Notes.txt").changeExtension(x => x + ".swp"))(equalTo(FileName("Notes.txt.swp"))) + ) + ), + suite("JSON encoding")( + test("Should encode as a JSON String") { + val sut = Path.FileName("morphir.yml") + assert(sut.asJson)(equalTo(Json.fromString("morphir.yml"))) + } + ), + suite("JSON decoding")( + test("Should decode from a JSON String") { + val input = "\"morphir.json\"" + assert(decode[Path.FileName](input))(isRight(equalTo(Path.FileName("morphir.json")))) + } + ) + ), + suite("DirName Spec")( + suite("JSON encoding")( + test("Should encode as a JSON String") { + val sut = Path.DirName("alpha/beta") + assert(sut.asJson)(equalTo(Json.fromString("alpha/beta"))) + } + ), + suite("JSON decoding")( + test("Should decode from a JSON String") { + val input = "\".morphir/work\"" + assert(decode[Path.DirName](input))(isRight(equalTo(Path.DirName(".morphir/work")))) + } + ) + ), + suite("Path")( + suite("JSON encoding")( + test("Should encode as a JSON String") { + import Path._ + import posixCodec._ + val path = currentDir dir(".morphir") file("projects.json") + assert(path.asJson)(equalTo(Json.fromString("./.morphir/projects.json"))) + } + ) + ) + ) +} diff --git a/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/PosixPathSpec.scala b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/PosixPathSpec.scala new file mode 100644 index 00000000..daceaf8f --- /dev/null +++ b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/PosixPathSpec.scala @@ -0,0 +1,157 @@ +package org.morphir.toolbox.io +import zio.test._ +import zio.test.Assertion._ + +object PosixPathSpec extends DefaultRunnableSpec { + import Path._ + import posixCodec._ + + def spec = + suite("Path Spec")( + suite("Posix print path specs")( + test("file in directory")( + assert(unsafePrintPath(dir("foo") file("bar")))(equalTo("./foo/bar")) + ), + test("two directories") { + assert(unsafePrintPath(dir("foo") dir("bar")))(equalTo("./foo/bar/")) + }, + test("file with two parents") { + assert(unsafePrintPath(dir("foo") dir("bar") file("image.png")))(equalTo("./foo/bar/image.png")) + }, + test("file without extension") { + assert(unsafePrintPath(file("image") <:> "png"))(equalTo("./image.png")) + }, + test("file with extension") { + assert(unsafePrintPath(file("image.jpg") <:> "png"))(equalTo("./image.png")) + }, + test("printPath - ./..")( + assert(unsafePrintPath(parentDir1(currentDir)))(equalTo("./../")) + ) + ), + suite("Posix path parsing specs")( + suite("parseRelFile")( + suite("should successfully parse")( + test("simple file name with extension")( + assert(parseRelFile("image.png"))(isSome(equalTo(file("image.png")))) + ), + test("preceded with current directory")( + assert(parseRelFile("./image.png"))(isSome(equalTo(file("image.png")))) + ), + test("path with two segments")( + assert(parseRelFile("foo/image.png"))(isSome(equalTo(dir("foo") file("image.png")))) + ), + test("preceded by a parent directory reference")( + assert(parseRelFile("../foo/image.png"))( + isSome(equalTo(currentDir <::> dir("foo") file("image.png"))) + ) + ) + ), + suite("should fail parse")( + test("with a leading /")( + assert(parseRelFile("/foo/image.png"))(isNone) + ), + test("with a trailing /")( + assert(parseRelFile("foo/"))(isNone) + ), + test(".")( + assert(parseRelFile("."))(isNone) + ), + test("foo/..")( + assert(parseRelFile("foo/.."))(isNone) + ) + ) + ), + suite("parseAbsFile")( + suite("should successfully parse")( + test("a simple filename with extensions as long as there is a leading /")( + assert(parseAbsFile("/image.png"))(isSome(equalTo(rootDir file("image.png")))) + ), + test("a path with two segments")( + assert(parseAbsFile("/foo/image.png"))(isSome(equalTo(rootDir dir("foo") file("image.png")))) + ) + ), + suite("fail to parse")( + test("with a trailing /")( + assert(parseAbsFile("/foo/"))(isNone) + ), + test("with no leading /")( + assert(parseAbsFile("foo/image.png"))(isNone) + ), + test("/.")( + assert(parseAbsFile("/."))(isNone) + ), + test("/foo/..")( + assert(parseAbsFile("/foo/.."))(isNone) + ) + ) + ), + suite("parseRelDir")( + suite("should successfully parse")( + test("empty string")( + assert(parseRelDir(""))(isSome(equalTo(currentDir[Unsandboxed]))) + ), + test("./../")( + assert(parseRelDir("./../"))(isSome(equalTo(currentDir <::> currentDir))) + ), + test("segment with trailing /")( + assert(parseRelDir("foo/"))(isSome(equalTo(dir("foo")))) + ), + test("segment with trailing .")( + assert(parseRelDir("foo/."))(isSome(equalTo(dir("foo") currentDir))) + ), + test("two segments with trailing /")( + assert(parseRelDir("foo/bar/"))(isSome(equalTo(dir("foo") dir("bar")))) + ), + test("two segments starting with a reference to current directory")( + assert(parseRelDir("./foo/bar/"))(isSome(equalTo(currentDir dir("foo") dir("bar")))) + ) + ), + suite("fail to parse")( + test("leading with a /")( + assert(parseRelDir("/foo/"))(isNone) + ), + test("simple name")( + assert(parseRelDir("foo"))(isNone) + ) + ) + ), + suite("parseRelAsDir")( + test("./foo/bar")( + assert(parseRelAsDir("./foo/bar"))(isSome(equalTo(dir("foo") dir("bar")))) + ), + test("./foo/bar/")( + assert(parseRelAsDir("./foo/bar/"))(isSome(equalTo(dir("foo") dir("bar")))) + ) + ), + suite("parseAbsDir")( + suite("should successfully parse")( + test("/")( + assert(parseAbsDir("/"))(isSome(equalTo(rootDir[Unsandboxed]))) + ), + test("/foo/")( + assert(parseAbsDir("/foo/"))(isSome(equalTo(rootDir dir("foo")))) + ), + test("/foo/bar/")( + assert(parseAbsDir("/foo/bar/"))(isSome(equalTo(rootDir dir("foo") dir("bar")))) + ) + ), + suite("should fail to parse")( + test("/foo")( + assert(parseAbsDir("/foo"))(isNone) + ), + test("foo")( + assert(parseAbsDir("foo"))(isNone) + ) + ) + ), + suite("parseAbsAsDir")( + test("/foo/bar/")( + assert(parseAbsAsDir("/foo/bar/"))(isSome(equalTo(rootDir dir("foo") dir("bar")))) + ), + test("/foo/bar")( + assert(parseAbsAsDir("/foo/bar"))(isSome(equalTo(rootDir dir("foo") dir("bar")))) + ) + ) + ) + ) +} diff --git a/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/WindowsPathSpec.scala b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/WindowsPathSpec.scala new file mode 100644 index 00000000..b8de97c2 --- /dev/null +++ b/morphir/toolbox/shared/src/test/scala/org/morphir/toolbox/io/WindowsPathSpec.scala @@ -0,0 +1,29 @@ +package org.morphir.toolbox.io +import zio.test._ +import zio.test.Assertion._ + +object WindowsPathSpec extends DefaultRunnableSpec { + import Path._ + import windowsCodec._ + + def spec = suite("Posix print path specs")( + test("file in directory")( + assert(unsafePrintPath(dir("foo") file("bar")))(equalTo(".\\foo\\bar")) + ), + test("two directories") { + assert(unsafePrintPath(dir("foo") dir("bar")))(equalTo(".\\foo\\bar\\")) + }, + test("file with two parents") { + assert(unsafePrintPath(dir("foo") dir("bar") file("image.png")))(equalTo(".\\foo\\bar\\image.png")) + }, + test("file without extension") { + assert(unsafePrintPath(file("image") <:> "png"))(equalTo(".\\image.png")) + }, + test("file with extension") { + assert(unsafePrintPath(file("image.jpg") <:> "png"))(equalTo(".\\image.png")) + }, + test("printPath - ./..")( + assert(unsafePrintPath(parentDir1(currentDir)))(equalTo(".\\..\\")) + ) + ) +}