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/.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/.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/.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 51aeaf35..a387daca 100644 --- a/build.sbt +++ b/build.sbt @@ -50,26 +50,42 @@ lazy val root = project ) ) .aggregate( + morphirCoreJS, + morphirCoreJVM, morphirSdkCoreJS, - morphirSdkCoreJVM + morphirSdkCoreJVM, //morphirSdkJsonJS, //morphirSdkJsonJVM, // morphirIRCoreJS, // morphirIRCoreJVM, -// morphirCliJS, -// morphirCliJVM + morphirToolboxJS, + morphirToolboxJVM, + morphirCliJVM ) +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" % Versions.zio, + "dev.zio" %%% "zio-test" % Versions.zio % "test", "dev.zio" %%% "zio-test-sbt" % Versions.zio % "test" ) ) @@ -106,7 +122,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 +137,64 @@ 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", 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", + "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")) + +lazy val morphirToolboxJS = morphirToolbox.js + .settings(testJsSettings) + .settings(zioNioSettings("1.0.0-RC6")) + .settings(scalaJSModuleKind := ModuleKind.CommonJSModule) + +lazy val morphirToolboxJVM = morphirToolbox.jvm + .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", 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-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 lazy val docs = project .in(file("morphir-jvm-docs")) 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..e0ee2b98 --- /dev/null +++ b/examples/workspaces/multi-project-workspace/morphir.toml @@ -0,0 +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/scala/org/morphir/cli/Cli.scala b/morphir/cli/shared/src/main/scala/org/morphir/cli/Cli.scala new file mode 100644 index 00000000..aadaa454 --- /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.{ BuildCommand, 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[BuildCommand] = Opts + .subcommand("build", help = "Build the workspace")( + workspaceOpt.orNone + ) + .map(BuildCommand) + + 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/cli/src/morphir/Main.scala b/morphir/cli/src/morphir/Main.scala deleted file mode 100644 index a14f0a46..00000000 --- a/morphir/cli/src/morphir/Main.scala +++ /dev/null @@ -1,66 +0,0 @@ -package morphir -import zio._ -import zio.blocking.Blocking -import zio.console._ -import caseapp.core.app.CommandApp -import morphir.cli._ -import caseapp.core.RemainingArgs -import morphir.runtime._ -import zio.stream.Sink -import 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/src/morphir/cli/Cli.scala b/morphir/cli/src/morphir/cli/Cli.scala deleted file mode 100644 index b75faa23..00000000 --- a/morphir/cli/src/morphir/cli/Cli.scala +++ /dev/null @@ -1,65 +0,0 @@ -package 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/src/morphir/cli/CommandLine.scala b/morphir/cli/src/morphir/cli/CommandLine.scala deleted file mode 100644 index 0bb6fe1a..00000000 --- a/morphir/cli/src/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 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/src/morphir/cli/package.scala b/morphir/cli/src/morphir/cli/package.scala deleted file mode 100644 index 8eef48e7..00000000 --- a/morphir/cli/src/morphir/cli/package.scala +++ /dev/null @@ -1,27 +0,0 @@ -package 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/src/morphir/runtime/ExitCode.scala b/morphir/cli/src/morphir/runtime/ExitCode.scala deleted file mode 100644 index 5539c012..00000000 --- a/morphir/cli/src/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/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..ccc0d53c --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/FrontendBinding.scala @@ -0,0 +1,27 @@ +package org.morphir.toolbox.binding + +import zio.stream._ +import org.morphir.toolbox.binding.FrontendBinding.{ FrontendCommand, FrontendEvent, InputPort, OutputPort } +import org.morphir.toolbox.core.ProjectInfo + +case class FrontendBinding[S]( + name: String, + input: InputPort[FrontendCommand[Any]], + output: OutputPort[FrontendEvent[Any]], + initialState: S +) + +object FrontendBinding { + + type InputPort[+A] = Stream[Nothing, FrontendCommand[A]] + + 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 new file mode 100644 index 00000000..7a86ef43 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/binding/elm/ElmFrontendBinding.scala @@ -0,0 +1,27 @@ +package org.morphir.toolbox.binding.elm + +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 new file mode 100644 index 00000000..4e39b974 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/CliCommand.scala @@ -0,0 +1,23 @@ +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 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/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 new file mode 100644 index 00000000..184dc52b --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/cli/commands/WorkspaceInfoCommand.scala @@ -0,0 +1,62 @@ +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.{ Binding, Bindings, Project } +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(s"|-${Color.Green("Project")}:") + _ <- console.putStrLn(s" \\") + _ <- 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 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(sourceDir: Path): ZIO[Console, Nothing, Unit] = + for { + _ <- console.putStrLn(s" - ${Color.Blue(sourceDir.toAbsolutePath.toString)}") + } 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/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 new file mode 100644 index 00000000..30b845d4 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Errors.scala @@ -0,0 +1,32 @@ +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) + + 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/ManifestFile.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ManifestFile.scala new file mode 100644 index 00000000..27461689 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ManifestFile.scala @@ -0,0 +1,72 @@ +package org.morphir.toolbox.core + +import java.io.{ File, IOException } +import java.nio.charset.StandardCharsets +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(chunk => new String(chunk.toArray, StandardCharsets.UTF_8)) + + def load: ZIO[Blocking, Throwable, WorkspaceSettings] = + for { + contents <- readAllText + path <- toPath + absolutePath <- path.toAbsolutePath + settings <- ZIO + .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/Project.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Project.scala new file mode 100644 index 00000000..c607e6b2 --- /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, + bindings: Bindings, + targets: List[Target[Any]], + sources: List[SourceFile[Any]] +) {} + +object Project {} + +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/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..82f025c1 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/ProjectPath.scala @@ -0,0 +1,28 @@ +package org.morphir.toolbox.core + +import java.nio.file.{ Path, Paths } + +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(Paths.get(path, segments: _*)) + + implicit val projectPathCodec: Codec[ProjectPath] = Codec { + case (Value.Str(value), _, _) => + if (value.trim.isEmpty) + 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/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 new file mode 100644 index 00000000..3347075e --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/Workspace.scala @@ -0,0 +1,69 @@ +package org.morphir.toolbox.core + +import java.io.File +import java.nio.file.{ Path, Paths } + +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, + 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 + )(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: 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) + } yield workspace + + private[core] def createProject( + settings: ProjectSettings, + name: String, + workspaceDir: WorkspaceDir + ): Project = { + 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 new file mode 100644 index 00000000..e993e7e5 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspaceDir.scala @@ -0,0 +1,57 @@ +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 => + 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(Paths.get(path.toString, segments: _*)) + + 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 { + + 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/WorkspacePath.scala b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/core/WorkspacePath.scala new file mode 100644 index 00000000..9a60478b --- /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, Paths } + +import org.morphir.core.newtype._ +import zio._ + +object WorkspacePath extends Subtype[Path] { + def of(path: String): WorkspacePath = WorkspacePath(Paths.get(path)) + + def from(path: Option[Path] = None): Task[WorkspacePath] = + Task.effect(path.getOrElse(Paths.get("."))).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..e8f54485 --- /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, Paths } + +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.trim.isEmpty) + Left(List.empty -> s"A non-empty path is expected $value provided") + else Right(Paths.get(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..b8a49dca --- /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/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/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..3f73afaf --- /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, Paths } + +case class BindingSettings(srcDirs: List[Path], outDir: Path) + +object BindingSettings { + object defaults { + val elm: BindingSettings = BindingSettings( + srcDirs = List(Paths.get("src/elm/")), + outDir = Paths.get("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..a60c64cf --- /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: 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 new file mode 100644 index 00000000..69d9ccee --- /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: BindingsSettings = 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..8bd8c892 --- /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 BindingsSettings = 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..b16899c8 --- /dev/null +++ b/morphir/toolbox/shared/src/main/scala/org/morphir/toolbox/workspace/package.scala @@ -0,0 +1,28 @@ +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: Option[Path] = None): RIO[WorkspaceModule, Workspace] = + RIO.accessM[WorkspaceModule](_.get.openFrom(path)) + + object WorkspaceModule { + + trait Service { + def openFrom(path: Option[Path] = None): Task[Workspace] + } + + val live: RLayer[Blocking, WorkspaceModule] = ZLayer.fromFunction(env => new Live(env)) + + class Live(blocking: Blocking) extends Service { + def openFrom(path: Option[Path] = None): Task[Workspace] = Workspace.load(path).provide(blocking) + } + + } +} 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")) + ) + ) + ) + } + ) + ) +} 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/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(".\\..\\")) + ) + ) +} 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/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/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")