mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 16:11:45 +03:00
Eliminate circe-yaml dependency (#10326)
* Eliminating circe-yaml This change adds our very-own YAML parser on top of SnakeYAML. Compared to Circe parser on top of SnakeYAML. The advantage? In some not-so-distant future we might actually get rid of circe and the related performance issues. The logic is similar to what circe does i.e. analyzing SnakeYAML to build our own structure. This change is not complete, as there are still some tests failing, but most common Configs are already parseable. We _could_ auto-generate some of the code but still some of the logic would have to be tweaked by hand; the current logic has a number of special cases, as I found out the hard way. * wip: more tests passing * Fix remaining tests in ConfigSpec * Fixing YAML decoder for editions Dropping circe as a decoder for Editions revealed some problems. Turns out the current implementation had even more special cases to deal with. * nit * Allow for empty exports * Mostly complete encodin part Replaced almost all `toYAML` locations with SnakeYAML equivalent. The encoding has to use Java collections for which there exists a built-in support. If we were to use Scala collections we would have to deal with tagging, at the very least. * Remove the last remaining Circe's YAML parser * Bug fix + further loop optimization * removal of some dependencies * Remove circe-yaml Added a custom SnakeYAML Node updater to mimick the JSON -> YAML -> JSON conversion needed for updating fields. The algorithm recursively follows the key-path and inserts the desired Node. This is not a performance oriented code on purpose. * Fix compilation issues `circe-core` was marked as `provided` but no one eventually included it in the final jar, hence `NoClassFoundException`. * fix licensing * Removing obsolete circe definitions * fmt * nits * s/SnakeYamlDecoder/YamlDecoder * fmt * Partial revert, PM needs JSON decoders/encoders * style * incremental compilation gone wrong
This commit is contained in:
parent
0661f17d1c
commit
c54c3b7e9d
32
build.sbt
32
build.sbt
@ -290,6 +290,7 @@ lazy val enso = (project in file("."))
|
||||
`syntax-rust-definition`,
|
||||
`text-buffer`,
|
||||
yaml,
|
||||
`scala-yaml`,
|
||||
pkg,
|
||||
cli,
|
||||
`task-progress-notifications`,
|
||||
@ -417,10 +418,10 @@ val catsVersion = "2.9.0"
|
||||
// === Circe ==================================================================
|
||||
|
||||
val circeVersion = "0.14.7"
|
||||
val circeYamlVersion = "0.15.1"
|
||||
val circeGenericExtrasVersion = "0.14.3"
|
||||
val circe = Seq("circe-core", "circe-generic", "circe-parser")
|
||||
.map("io.circe" %% _ % circeVersion)
|
||||
val snakeyamlVersion = "2.2"
|
||||
|
||||
// === Commons ================================================================
|
||||
|
||||
@ -751,7 +752,16 @@ lazy val yaml = (project in file("lib/java/yaml"))
|
||||
frgaalJavaCompilerSetting,
|
||||
version := "0.1",
|
||||
libraryDependencies ++= Seq(
|
||||
"io.circe" %% "circe-yaml" % circeYamlVersion % "provided"
|
||||
"org.yaml" % "snakeyaml" % snakeyamlVersion % "provided"
|
||||
)
|
||||
)
|
||||
|
||||
lazy val `scala-yaml` = (project in file("lib/scala/yaml"))
|
||||
.configs(Test)
|
||||
.settings(
|
||||
frgaalJavaCompilerSetting,
|
||||
libraryDependencies ++= Seq(
|
||||
"org.yaml" % "snakeyaml" % snakeyamlVersion % "provided"
|
||||
)
|
||||
)
|
||||
|
||||
@ -762,7 +772,8 @@ lazy val pkg = (project in file("lib/scala/pkg"))
|
||||
version := "0.1",
|
||||
libraryDependencies ++= Seq(
|
||||
"org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided",
|
||||
"io.circe" %% "circe-yaml" % circeYamlVersion % "provided",
|
||||
"io.circe" %% "circe-core" % circeVersion % "provided",
|
||||
"org.yaml" % "snakeyaml" % snakeyamlVersion % "provided",
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
|
||||
"org.apache.commons" % "commons-compress" % commonsCompressVersion
|
||||
)
|
||||
@ -930,10 +941,12 @@ lazy val cli = project
|
||||
version := "0.1",
|
||||
libraryDependencies ++= circe ++ Seq(
|
||||
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
|
||||
"org.yaml" % "snakeyaml" % snakeyamlVersion % "provided",
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test
|
||||
),
|
||||
Test / parallelExecution := false
|
||||
)
|
||||
.dependsOn(`scala-yaml`)
|
||||
|
||||
lazy val `task-progress-notifications` = project
|
||||
.in(file("lib/scala/task-progress-notifications"))
|
||||
@ -1461,11 +1474,11 @@ lazy val `polyglot-api` = project
|
||||
"runtime-fat-jar"
|
||||
) / Compile / fullClasspath).value,
|
||||
libraryDependencies ++= Seq(
|
||||
"io.circe" %% "circe-core" % circeVersion % "provided",
|
||||
"org.graalvm.sdk" % "polyglot-tck" % graalMavenPackagesVersion % "provided",
|
||||
"org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided",
|
||||
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion,
|
||||
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % jsoniterVersion,
|
||||
"io.circe" %% "circe-yaml" % circeYamlVersion % "provided", // as required by `pkg` and `editions`
|
||||
"com.google.flatbuffers" % "flatbuffers-java" % flatbuffersVersion,
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
|
||||
"org.scalacheck" %% "scalacheck" % scalacheckVersion % Test
|
||||
@ -2764,7 +2777,7 @@ lazy val `distribution-manager` = project
|
||||
resolvers += Resolver.bintrayRepo("gn0s1s", "releases"),
|
||||
libraryDependencies ++= Seq(
|
||||
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
|
||||
"io.circe" %% "circe-yaml" % circeYamlVersion,
|
||||
"org.yaml" % "snakeyaml" % snakeyamlVersion,
|
||||
"commons-io" % "commons-io" % commonsIoVersion,
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test
|
||||
)
|
||||
@ -2944,7 +2957,8 @@ lazy val editions = project
|
||||
.settings(
|
||||
frgaalJavaCompilerSetting,
|
||||
libraryDependencies ++= Seq(
|
||||
"io.circe" %% "circe-yaml" % circeYamlVersion % "provided",
|
||||
"io.circe" %% "circe-core" % circeVersion % "provided",
|
||||
"org.yaml" % "snakeyaml" % snakeyamlVersion % "provided",
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test
|
||||
)
|
||||
)
|
||||
@ -2973,7 +2987,8 @@ lazy val semver = project
|
||||
.settings(
|
||||
frgaalJavaCompilerSetting,
|
||||
libraryDependencies ++= Seq(
|
||||
"io.circe" %% "circe-yaml" % circeYamlVersion % "provided",
|
||||
"io.circe" %% "circe-core" % circeVersion % "provided",
|
||||
"org.yaml" % "snakeyaml" % snakeyamlVersion % "provided",
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
|
||||
"junit" % "junit" % junitVersion % Test,
|
||||
"com.github.sbt" % "junit-interface" % junitIfVersion % Test
|
||||
@ -2996,6 +3011,7 @@ lazy val semver = project
|
||||
cleanFiles += baseDirectory.value / ".." / ".." / "distribution" / "editions"
|
||||
)
|
||||
.dependsOn(testkit % Test)
|
||||
.dependsOn(`scala-yaml`)
|
||||
|
||||
lazy val downloader = (project in file("lib/scala/downloader"))
|
||||
.settings(
|
||||
@ -3040,7 +3056,7 @@ lazy val `edition-uploader` = project
|
||||
.settings(
|
||||
frgaalJavaCompilerSetting,
|
||||
libraryDependencies ++= Seq(
|
||||
"io.circe" %% "circe-yaml" % circeYamlVersion % "provided"
|
||||
"io.circe" %% "circe-core" % circeVersion % "provided"
|
||||
)
|
||||
)
|
||||
.dependsOn(editions)
|
||||
|
@ -211,16 +211,6 @@ The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `io.circe.circe-parser_2.13-0.14.7`.
|
||||
|
||||
|
||||
'circe-yaml-common_2.13', licensed under the Apache-2.0, is distributed with the engine.
|
||||
The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml-common_2.13-0.15.1`.
|
||||
|
||||
|
||||
'circe-yaml_2.13', licensed under the Apache-2.0, is distributed with the engine.
|
||||
The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml_2.13-0.15.1`.
|
||||
|
||||
|
||||
'helidon-builder-api', licensed under the Apache 2.0, is distributed with the engine.
|
||||
The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `io.helidon.builder.helidon-builder-api-4.0.8`.
|
||||
|
@ -1,15 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 circe
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
@ -1,15 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 circe
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
@ -86,16 +86,6 @@ The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `io.circe.circe-parser_2.13-0.14.7`.
|
||||
|
||||
|
||||
'circe-yaml-common_2.13', licensed under the Apache-2.0, is distributed with the launcher.
|
||||
The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml-common_2.13-0.15.1`.
|
||||
|
||||
|
||||
'circe-yaml_2.13', licensed under the Apache-2.0, is distributed with the launcher.
|
||||
The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml_2.13-0.15.1`.
|
||||
|
||||
|
||||
'commons-compress', licensed under the Apache-2.0, is distributed with the launcher.
|
||||
The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `org.apache.commons.commons-compress-1.23.0`.
|
||||
|
@ -1,15 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 circe
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
@ -1,15 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 circe
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
@ -186,16 +186,6 @@ The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `io.circe.circe-parser_2.13-0.14.7`.
|
||||
|
||||
|
||||
'circe-yaml-common_2.13', licensed under the Apache-2.0, is distributed with the project-manager.
|
||||
The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml-common_2.13-0.15.1`.
|
||||
|
||||
|
||||
'circe-yaml_2.13', licensed under the Apache-2.0, is distributed with the project-manager.
|
||||
The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml_2.13-0.15.1`.
|
||||
|
||||
|
||||
'helidon-builder-api', licensed under the Apache 2.0, is distributed with the project-manager.
|
||||
The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `io.helidon.builder.helidon-builder-api-4.0.8`.
|
||||
|
@ -1,15 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 circe
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
@ -1,15 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 circe
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
@ -2,7 +2,6 @@ package org.enso.launcher
|
||||
|
||||
import java.nio.file.Path
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import io.circe.Json
|
||||
import org.enso.semver.SemVer
|
||||
import org.enso.distribution.config.DefaultVersion
|
||||
import org.enso.editions.updater.EditionManager
|
||||
@ -368,7 +367,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
s"(${configurationManager.configLocation.toAbsolutePath})."
|
||||
)
|
||||
} else {
|
||||
configurationManager.updateConfigRaw(key, Json.fromString(value))
|
||||
configurationManager.updateConfigRaw(key, value)
|
||||
InfoLogger.info(
|
||||
s"""Key `$key` set to "$value" in the global configuration file """ +
|
||||
s"(${configurationManager.configLocation.toAbsolutePath})."
|
||||
|
@ -1,7 +1,9 @@
|
||||
package org.enso.launcher.releases.fallback.staticwebsite
|
||||
|
||||
import io.circe.Decoder
|
||||
import org.enso.yaml.YamlDecoder
|
||||
import org.yaml.snakeyaml.nodes.{MappingNode, Node}
|
||||
|
||||
import java.io.StringReader
|
||||
import scala.util.Try
|
||||
|
||||
/** Manifest of the fallback mechanism.
|
||||
@ -13,6 +15,23 @@ case class FallbackManifest(enabled: Boolean)
|
||||
|
||||
object FallbackManifest {
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[FallbackManifest] =
|
||||
new YamlDecoder[FallbackManifest] {
|
||||
override def decode(node: Node) = {
|
||||
node match {
|
||||
case node: MappingNode =>
|
||||
val booleanDecoder = implicitly[YamlDecoder[Boolean]]
|
||||
val bindings = mappingKV(node)
|
||||
for {
|
||||
enabled <- bindings
|
||||
.get(Fields.enabled)
|
||||
.map(booleanDecoder.decode(_))
|
||||
.getOrElse(Right(false))
|
||||
} yield FallbackManifest(enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Defines a part of the URL scheme of the fallback mechanism - the name of
|
||||
* manifest file.
|
||||
*
|
||||
@ -25,17 +44,10 @@ object FallbackManifest {
|
||||
val enabled = "enabled"
|
||||
}
|
||||
|
||||
/** [[Decoder]] instance for [[FallbackManifest]].
|
||||
*
|
||||
* It should always remain backwards compatible, since the fallback mechanism
|
||||
* must work for all released launcher versions.
|
||||
*/
|
||||
implicit val decoder: Decoder[FallbackManifest] = { json =>
|
||||
for {
|
||||
enabled <- json.get[Boolean](Fields.enabled)
|
||||
} yield FallbackManifest(enabled)
|
||||
def parseString(yamlString: String): Try[FallbackManifest] = {
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
Try(snakeYaml.compose(new StringReader(yamlString))).toEither
|
||||
.flatMap(implicitly[YamlDecoder[FallbackManifest]].decode(_))
|
||||
.toTry
|
||||
}
|
||||
|
||||
def parseString(string: String): Try[FallbackManifest] =
|
||||
io.circe.yaml.parser.parse(string).flatMap(_.as[FallbackManifest]).toTry
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
package org.enso.launcher.releases.launcher
|
||||
|
||||
import io.circe.{yaml, Decoder}
|
||||
import org.enso.launcher.releases.launcher
|
||||
import org.enso.semver.SemVer
|
||||
import org.enso.runtimeversionmanager.releases.ReleaseProviderException
|
||||
import org.enso.semver.SemVerJson._
|
||||
import org.enso.semver.SemVerYaml._
|
||||
import org.enso.yaml.{ParseError, YamlDecoder}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
import org.yaml.snakeyaml.nodes.{MappingNode, Node}
|
||||
|
||||
import java.io.StringReader
|
||||
import scala.util.{Failure, Try}
|
||||
|
||||
/** Contains release metadata associated with a launcher release.
|
||||
@ -37,28 +41,52 @@ object LauncherManifest {
|
||||
val directoriesToCopy = "directories-to-copy"
|
||||
}
|
||||
|
||||
/** [[Decoder]] instance for [[LauncherManifest]].
|
||||
*/
|
||||
implicit val decoder: Decoder[LauncherManifest] = { json =>
|
||||
for {
|
||||
minimumVersionToUpgrade <-
|
||||
json.get[SemVer](Fields.minimumVersionForUpgrade)
|
||||
files <- json.getOrElse[Seq[String]](Fields.filesToCopy)(Seq())
|
||||
directories <-
|
||||
json.getOrElse[Seq[String]](Fields.directoriesToCopy)(Seq())
|
||||
} yield LauncherManifest(
|
||||
minimumVersionForUpgrade = minimumVersionToUpgrade,
|
||||
filesToCopy = files,
|
||||
directoriesToCopy = directories
|
||||
)
|
||||
}
|
||||
implicit val yamlDecoder: YamlDecoder[LauncherManifest] =
|
||||
new YamlDecoder[LauncherManifest] {
|
||||
override def decode(node: Node): Either[Throwable, LauncherManifest] = {
|
||||
node match {
|
||||
case node: MappingNode =>
|
||||
val bindings = mappingKV(node)
|
||||
val semverDecoder = implicitly[YamlDecoder[SemVer]]
|
||||
val seqStringDecoder = implicitly[YamlDecoder[Seq[String]]]
|
||||
for {
|
||||
minimumVersionForUpgrade <- bindings
|
||||
.get(Fields.minimumVersionForUpgrade)
|
||||
.map(semverDecoder.decode)
|
||||
.getOrElse(
|
||||
Left(
|
||||
new YAMLException(
|
||||
s"Required `${Fields.minimumVersionForUpgrade}` field is missing"
|
||||
)
|
||||
)
|
||||
)
|
||||
filesToCopy <- bindings
|
||||
.get(Fields.filesToCopy)
|
||||
.map(seqStringDecoder.decode)
|
||||
.getOrElse(Right(Seq.empty))
|
||||
directoriesToCopy <- bindings
|
||||
.get(Fields.directoriesToCopy)
|
||||
.map(seqStringDecoder.decode)
|
||||
.getOrElse(Right(Seq.empty))
|
||||
} yield LauncherManifest(
|
||||
minimumVersionForUpgrade,
|
||||
filesToCopy,
|
||||
directoriesToCopy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Tries to parse the [[LauncherManifest]] from a [[String]].
|
||||
*/
|
||||
def fromYAML(string: String): Try[LauncherManifest] =
|
||||
yaml.parser
|
||||
.parse(string)
|
||||
.flatMap(_.as[LauncherManifest])
|
||||
def fromYAML(string: String): Try[LauncherManifest] = {
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
Try(snakeYaml.compose(new StringReader(string))).toEither
|
||||
.flatMap(
|
||||
implicitly[YamlDecoder[launcher.LauncherManifest]].decode(_)
|
||||
)
|
||||
.left
|
||||
.map(ParseError(_))
|
||||
.toTry
|
||||
.recoverWith { error =>
|
||||
// TODO [RW] more readable errors in #1111
|
||||
@ -69,4 +97,5 @@ object LauncherManifest {
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ package org.enso.cli
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import io.circe.{Decoder, DecodingFailure}
|
||||
import org.enso.yaml.YamlDecoder
|
||||
import org.yaml.snakeyaml.nodes.{Node, ScalarNode}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
|
||||
/** Represents one of the supported platforms (operating systems).
|
||||
*/
|
||||
@ -29,7 +32,7 @@ object OS {
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
def configName: String = "linux"
|
||||
val configName: String = "linux"
|
||||
}
|
||||
|
||||
/** Represents the macOS operating system.
|
||||
@ -38,7 +41,7 @@ object OS {
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
def configName: String = "macos"
|
||||
val configName: String = "macos"
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
@ -52,7 +55,7 @@ object OS {
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
def configName: String = "windows"
|
||||
val configName: String = "windows"
|
||||
}
|
||||
|
||||
/** Checks if the application is being run on Windows.
|
||||
@ -143,4 +146,18 @@ object OS {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[OS] = (node: Node) => {
|
||||
node match {
|
||||
case s: ScalarNode =>
|
||||
s.getValue match {
|
||||
case Linux.configName => Right(Linux)
|
||||
case Windows.configName => Right(Windows)
|
||||
case MacOS.configName => Right(MacOS)
|
||||
case os => Left(new YAMLException(s"Unsupported os `$os`"))
|
||||
}
|
||||
case _ =>
|
||||
Left(new YAMLException("Expected a plain string value"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.enso.distribution.config
|
||||
|
||||
import io.circe.syntax._
|
||||
import io.circe.{Decoder, Encoder, Json}
|
||||
import org.enso.semver.SemVer
|
||||
import org.enso.cli.arguments.{Argument, OptsParseError}
|
||||
import org.enso.semver.SemVerJson._
|
||||
import org.enso.semver.SemVerYaml._
|
||||
import org.enso.yaml.{YamlDecoder, YamlEncoder}
|
||||
import org.yaml.snakeyaml.nodes.{Node, ScalarNode}
|
||||
|
||||
/** Default version that is used when launching Enso outside of projects and
|
||||
* when creating new projects.
|
||||
@ -17,9 +17,11 @@ object DefaultVersion {
|
||||
*/
|
||||
case object LatestInstalled extends DefaultVersion {
|
||||
|
||||
val name = "latest-installed"
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def toString: String = "latest-installed"
|
||||
override def toString: String = name
|
||||
}
|
||||
|
||||
/** Defaults to a specified version.
|
||||
@ -31,24 +33,34 @@ object DefaultVersion {
|
||||
override def toString: String = version.toString
|
||||
}
|
||||
|
||||
/** [[Encoder]] instance for [[DefaultVersion]].
|
||||
*/
|
||||
implicit val encoder: Encoder[DefaultVersion] = {
|
||||
case LatestInstalled =>
|
||||
Json.Null
|
||||
case Exact(version) =>
|
||||
version.asJson
|
||||
}
|
||||
implicit val yamlDecoder: YamlDecoder[DefaultVersion] =
|
||||
new YamlDecoder[DefaultVersion] {
|
||||
override def decode(node: Node) = {
|
||||
node match {
|
||||
case node if node == null =>
|
||||
Right(LatestInstalled)
|
||||
case scalarNode: ScalarNode =>
|
||||
scalarNode.getValue match {
|
||||
case LatestInstalled.name =>
|
||||
Right(LatestInstalled)
|
||||
case _ =>
|
||||
implicitly[YamlDecoder[SemVer]]
|
||||
.decode(scalarNode)
|
||||
.map(Exact(_))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** [[Decoder]] instance for [[DefaultVersion]].
|
||||
*/
|
||||
implicit val decoder: Decoder[DefaultVersion] = { json =>
|
||||
if (json.value.isNull) Right(LatestInstalled)
|
||||
else
|
||||
for {
|
||||
version <- json.as[SemVer]
|
||||
} yield Exact(version)
|
||||
}
|
||||
implicit val yamlEncoder: YamlEncoder[DefaultVersion] =
|
||||
new YamlEncoder[DefaultVersion] {
|
||||
override def encode(value: DefaultVersion): AnyRef = {
|
||||
value match {
|
||||
case latest @ LatestInstalled => latest.toString
|
||||
case Exact(version) => version.toString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** [[Argument]] instance for [[DefaultVersion]].
|
||||
*/
|
||||
|
@ -1,8 +1,10 @@
|
||||
package org.enso.distribution.config
|
||||
|
||||
import io.circe.syntax._
|
||||
import io.circe.{Decoder, Encoder, Json}
|
||||
import org.enso.distribution.config
|
||||
import org.enso.yaml.{YamlDecoder, YamlEncoder}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
import org.yaml.snakeyaml.nodes.{MappingNode, Node}
|
||||
|
||||
import java.util
|
||||
|
||||
/** Global user configuration.
|
||||
*
|
||||
@ -25,20 +27,18 @@ case class GlobalConfig(
|
||||
editionProviders: Seq[String]
|
||||
) {
|
||||
def findByKey(key: String): Option[String] = {
|
||||
val jsonValue: Option[Json] = key match {
|
||||
key match {
|
||||
case GlobalConfig.Fields.DefaultVersion =>
|
||||
Option(defaultVersion).map(_.asJson)
|
||||
Option(defaultVersion).map(_.toString)
|
||||
case GlobalConfig.Fields.AuthorName =>
|
||||
authorName.map(_.asJson)
|
||||
authorName
|
||||
case GlobalConfig.Fields.AuthorEmail =>
|
||||
authorEmail.map(_.asJson)
|
||||
authorEmail
|
||||
case GlobalConfig.Fields.EditionProviders =>
|
||||
Option(editionProviders).map(_.asJson)
|
||||
Option(editionProviders).map(_.toString())
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
|
||||
jsonValue.map(j => j.asString.getOrElse(j.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,36 +66,103 @@ object GlobalConfig {
|
||||
val EditionProviders = "edition-providers"
|
||||
}
|
||||
|
||||
/** [[Decoder]] instance for [[GlobalConfig]].
|
||||
*/
|
||||
implicit val decoder: Decoder[GlobalConfig] = { json =>
|
||||
for {
|
||||
defaultVersion <- json.getOrElse[DefaultVersion](Fields.DefaultVersion)(
|
||||
DefaultVersion.LatestInstalled
|
||||
)
|
||||
authorName <- json.getOrElse[Option[String]](Fields.AuthorName)(None)
|
||||
authorEmail <- json.getOrElse[Option[String]](Fields.AuthorEmail)(None)
|
||||
editionProviders <- json.getOrElse[Seq[String]](Fields.EditionProviders)(
|
||||
defaultEditionProviders
|
||||
)
|
||||
} yield config.GlobalConfig(
|
||||
defaultVersion = defaultVersion,
|
||||
authorName = authorName,
|
||||
authorEmail = authorEmail,
|
||||
editionProviders = editionProviders
|
||||
)
|
||||
}
|
||||
implicit val yamlDecoder: YamlDecoder[GlobalConfig] =
|
||||
new YamlDecoder[GlobalConfig] {
|
||||
override def decode(node: Node) = node match {
|
||||
case node: MappingNode =>
|
||||
val bindings = mappingKV(node)
|
||||
val defaultVersionDecoder =
|
||||
implicitly[YamlDecoder[DefaultVersion]]
|
||||
val stringDecoder = implicitly[YamlDecoder[String]]
|
||||
val seqStringDecoder = implicitly[YamlDecoder[Seq[String]]]
|
||||
|
||||
/** [[Encoder]] instance for [[GlobalConfig]].
|
||||
*/
|
||||
implicit val encoder: Encoder[GlobalConfig] = { config =>
|
||||
val overrides =
|
||||
Json.obj(
|
||||
Fields.DefaultVersion -> config.defaultVersion.asJson,
|
||||
Fields.AuthorName -> config.authorName.asJson,
|
||||
Fields.AuthorEmail -> config.authorEmail.asJson,
|
||||
Fields.EditionProviders -> config.editionProviders.asJson
|
||||
)
|
||||
overrides.dropNullValues.asJson
|
||||
}
|
||||
val defaultVersionOpt = bindings.get("default") match {
|
||||
case Some(versionNode: MappingNode) =>
|
||||
val versionBindings = mappingKV(versionNode)
|
||||
versionBindings
|
||||
.get("enso-version")
|
||||
.toRight(
|
||||
new YAMLException(s"missing '${Fields.DefaultVersion}' field")
|
||||
)
|
||||
.flatMap(defaultVersionDecoder.decode)
|
||||
case _ =>
|
||||
// Fallback
|
||||
bindings
|
||||
.get(Fields.DefaultVersion)
|
||||
.map(defaultVersionDecoder.decode)
|
||||
.getOrElse(Right(DefaultVersion.LatestInstalled))
|
||||
}
|
||||
val (nameOpt, emailOpt) = bindings.get("author") match {
|
||||
case Some(authorNode: MappingNode) =>
|
||||
val authorBindings = mappingKV(authorNode)
|
||||
(
|
||||
authorBindings
|
||||
.get("name")
|
||||
.map(stringDecoder.decode)
|
||||
.getOrElse(Right(None))
|
||||
.asInstanceOf[Either[Throwable, Option[String]]],
|
||||
authorBindings
|
||||
.get("email")
|
||||
.map(stringDecoder.decode)
|
||||
.getOrElse(Right(None))
|
||||
.asInstanceOf[Either[Throwable, Option[String]]]
|
||||
)
|
||||
case _ =>
|
||||
// Fallback
|
||||
(
|
||||
bindings
|
||||
.get(Fields.AuthorName)
|
||||
.map(stringDecoder.decode(_).map(Some(_)))
|
||||
.getOrElse(Right(None)),
|
||||
bindings
|
||||
.get(Fields.AuthorEmail)
|
||||
.map(stringDecoder.decode(_).map(Some(_)))
|
||||
.getOrElse(Right(None))
|
||||
)
|
||||
}
|
||||
val editionProviderOpt = bindings
|
||||
.get(Fields.EditionProviders)
|
||||
.map(seqStringDecoder.decode)
|
||||
.getOrElse(Right(Seq.empty))
|
||||
for {
|
||||
defaultVersion <- defaultVersionOpt
|
||||
name <- nameOpt
|
||||
email <- emailOpt
|
||||
editionProvider <- editionProviderOpt
|
||||
} yield GlobalConfig(defaultVersion, name, email, editionProvider)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlEncoder: YamlEncoder[GlobalConfig] =
|
||||
new YamlEncoder[GlobalConfig] {
|
||||
override def encode(value: GlobalConfig): AnyRef = {
|
||||
val defaultVersionEncoder = implicitly[YamlEncoder[DefaultVersion]]
|
||||
val editionProviders = implicitly[YamlEncoder[Seq[String]]]
|
||||
val elements = new util.ArrayList[(String, AnyRef)]()
|
||||
elements.add(
|
||||
(
|
||||
"default",
|
||||
toMap(
|
||||
"enso-version",
|
||||
defaultVersionEncoder.encode(value.defaultVersion)
|
||||
)
|
||||
)
|
||||
)
|
||||
if (value.authorName.nonEmpty || value.authorEmail.nonEmpty) {
|
||||
val authorElements = new util.ArrayList[(String, AnyRef)]()
|
||||
value.authorName.foreach(v => authorElements.add(("name", v)))
|
||||
value.authorEmail.foreach(v => authorElements.add(("email", v)))
|
||||
elements.add(("author", toMap(authorElements)))
|
||||
}
|
||||
if (value.editionProviders.nonEmpty) {
|
||||
elements.add(
|
||||
(
|
||||
Fields.EditionProviders,
|
||||
editionProviders.encode(value.editionProviders)
|
||||
)
|
||||
)
|
||||
}
|
||||
toMap(elements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
package org.enso.distribution.config
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import io.circe.syntax._
|
||||
import io.circe.yaml.Parser
|
||||
import io.circe.{yaml, Json}
|
||||
import org.enso.distribution.DistributionManager
|
||||
import org.enso.distribution.FileSystem.PathSyntax
|
||||
import org.enso.yaml.{YamlDecoder, YamlEncoder}
|
||||
import org.yaml.snakeyaml.{DumperOptions, Yaml}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
import org.yaml.snakeyaml.nodes.{MappingNode, Node, NodeTuple, ScalarNode, Tag}
|
||||
|
||||
import java.io.BufferedWriter
|
||||
import java.io.{BufferedWriter, StringReader}
|
||||
import java.nio.file.{Files, NoSuchFileException, Path}
|
||||
import scala.jdk.CollectionConverters.{CollectionHasAsScala, SeqHasAsJava}
|
||||
import scala.util.{Failure, Success, Try, Using}
|
||||
|
||||
/** Manages the global configuration of the distribution. */
|
||||
@ -48,31 +50,159 @@ class GlobalConfigurationManager(distributionManager: DistributionManager) {
|
||||
* (because an invalid value has been set for a known field), the config is
|
||||
* not saved and an exception is thrown.
|
||||
*/
|
||||
def updateConfigRaw(key: String, value: Json): Unit = {
|
||||
val updated = GlobalConfig.encoder(getConfig).asObject.get.add(key, value)
|
||||
GlobalConfigurationManager
|
||||
.writeConfigRaw(configLocation, updated.asJson)
|
||||
.recoverWith { case e: InvalidConfigError =>
|
||||
Failure(
|
||||
InvalidConfigError(
|
||||
s"Invalid value for key `$key`. Config changes were not saved.",
|
||||
e
|
||||
)
|
||||
)
|
||||
}
|
||||
def updateConfigRaw(key: String, value: String): Unit = {
|
||||
stringToYamlNode(value)
|
||||
.flatMap(newValueNode =>
|
||||
updateYamlNode(key.split("\\.").toList, getConfig, newValueNode)
|
||||
.flatMap { updatedNode =>
|
||||
GlobalConfigurationManager
|
||||
.writeConfigRaw(configLocation, updatedNode)
|
||||
.recoverWith { case e: InvalidConfigError =>
|
||||
Failure(
|
||||
InvalidConfigError(
|
||||
s"Invalid value for key `$key`. Config changes were not saved",
|
||||
e
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
.get
|
||||
}
|
||||
|
||||
private def stringToYamlNode(value: String): Try[Node] = {
|
||||
if (value == null) {
|
||||
Success(null)
|
||||
} else {
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
Try(snakeYaml.compose(new StringReader(value)))
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates GlobalConfig's YAML representation at the provided key-path */
|
||||
private def updateYamlNode(
|
||||
keys: List[String],
|
||||
config: GlobalConfig,
|
||||
yamlNode: Node
|
||||
): Try[Node] = {
|
||||
val encoder = implicitly[YamlEncoder[GlobalConfig]]
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
updateYamlNode(keys, snakeYaml.represent(encoder.encode(config)), yamlNode)
|
||||
}
|
||||
|
||||
/** Updates the given YAML node at the provided key-path.
|
||||
* If the new `yamlNode` is null, the node at the provided key-path should be removed.
|
||||
*
|
||||
* @param keys list of keys representing the parent-child relation in YAML nodes
|
||||
* @param original the currently traversed YAML node
|
||||
* @param yamlNode the node to be placed at the end of the key-path
|
||||
* @return the updated YAML node
|
||||
*/
|
||||
private def updateYamlNode(
|
||||
keys: List[String],
|
||||
original: Node,
|
||||
yamlNode: Node
|
||||
): Try[Node] = {
|
||||
keys match {
|
||||
case Nil =>
|
||||
Success(yamlNode)
|
||||
case head :: rest =>
|
||||
original match {
|
||||
case mappingNode: MappingNode =>
|
||||
val tuples = mappingNode.getValue.asScala
|
||||
val (failures, mappings) = tuples
|
||||
.map { tuple =>
|
||||
tuple.getKeyNode match {
|
||||
case s: ScalarNode =>
|
||||
Right((s.getValue, (tuple.getValueNode, s)))
|
||||
case _ =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Internal error: Unexpected key in the mapping node"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.span(_.isLeft)
|
||||
if (failures.isEmpty) {
|
||||
val m = mappings.map(_.toOption.get).toMap
|
||||
m.get(head) match {
|
||||
case Some((entryNode, scalarKeyNode)) =>
|
||||
val others = m.removed(head)
|
||||
updateYamlNode(rest, entryNode, yamlNode) map { node =>
|
||||
createMappingNode(
|
||||
others.toList.map { case (_, (valueNode, keyNode)) =>
|
||||
(keyNode, valueNode)
|
||||
},
|
||||
scalarKeyNode,
|
||||
node,
|
||||
mappingNode
|
||||
)
|
||||
}
|
||||
case None if yamlNode == null =>
|
||||
// cannot remove node that is not present
|
||||
Success(mappingNode)
|
||||
case None =>
|
||||
val scalarKeyNode = new ScalarNode(
|
||||
Tag.YAML,
|
||||
keys.mkString("."),
|
||||
null,
|
||||
null,
|
||||
DumperOptions.ScalarStyle.PLAIN
|
||||
)
|
||||
Success(
|
||||
createMappingNode(
|
||||
m.toList.map { case (_, (valueNode, keyNode)) =>
|
||||
(keyNode, valueNode)
|
||||
},
|
||||
scalarKeyNode,
|
||||
yamlNode,
|
||||
mappingNode
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Failure(failures.head.left.toOption.get)
|
||||
}
|
||||
case _ =>
|
||||
Failure(
|
||||
new YAMLException(s"Cannot replace `$head` in the non-map field")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def createMappingNode(
|
||||
existingTuples: List[(Node, Node)],
|
||||
newKeyNode: Node,
|
||||
newValueNode: Node,
|
||||
existingMappingNode: MappingNode
|
||||
): Node = {
|
||||
val allTuples =
|
||||
if (newValueNode != null)
|
||||
existingTuples ++ List((newKeyNode, newValueNode))
|
||||
else existingTuples
|
||||
new MappingNode(
|
||||
existingMappingNode.getTag,
|
||||
true,
|
||||
allTuples.map(v => new NodeTuple(v._1, v._2)).asJava,
|
||||
existingMappingNode.getStartMark,
|
||||
existingMappingNode.getEndMark,
|
||||
existingMappingNode.getFlowStyle
|
||||
)
|
||||
}
|
||||
|
||||
/** Removes the `key` from the config.
|
||||
*
|
||||
* If removing that setting would result in the config becoming unreadable,
|
||||
* the config is not saved and an exception is thrown.
|
||||
*/
|
||||
def removeFromConfig(key: String): Unit = {
|
||||
val updated = GlobalConfig.encoder(getConfig).asObject.get.remove(key)
|
||||
GlobalConfigurationManager.writeConfigRaw(
|
||||
configLocation,
|
||||
updated.asJson
|
||||
updateYamlNode(key.split("\\.").toList, getConfig, null).map(updatedNode =>
|
||||
GlobalConfigurationManager.writeConfigRaw(
|
||||
configLocation,
|
||||
updatedNode
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -85,24 +215,31 @@ object GlobalConfigurationManager {
|
||||
/** Tries to read the global config from the given `path`. */
|
||||
private def readConfig(path: Path): Try[GlobalConfig] =
|
||||
Using(Files.newBufferedReader(path)) { reader =>
|
||||
for {
|
||||
json <- Parser.default.parse(reader)
|
||||
config <- json.as[GlobalConfig]
|
||||
} yield config
|
||||
}.flatMap(_.toTry)
|
||||
val snakeYaml = new Yaml()
|
||||
Try(snakeYaml.compose(reader)).toEither
|
||||
.flatMap(implicitly[YamlDecoder[GlobalConfig]].decode(_))
|
||||
.toTry
|
||||
}.flatten
|
||||
|
||||
/** Tries to write the provided `config` to the given `path`. */
|
||||
private def writeConfig(path: Path, config: GlobalConfig): Try[Unit] =
|
||||
writeConfigRaw(path, GlobalConfig.encoder(config))
|
||||
private def writeConfig(path: Path, config: GlobalConfig): Try[Unit] = {
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
writeConfigRaw(
|
||||
path,
|
||||
snakeYaml.represent(
|
||||
implicitly[YamlEncoder[GlobalConfig]].encode(config)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** Tries to write the config from a raw JSON value to the given `path`.
|
||||
*
|
||||
* The config will not be saved if it is invalid, instead an exception is
|
||||
* thrown.
|
||||
*/
|
||||
private def writeConfigRaw(path: Path, rawConfig: Json): Try[Unit] = {
|
||||
def verifyConfig: Try[Unit] =
|
||||
rawConfig.as[GlobalConfig] match {
|
||||
private def writeConfigRaw(path: Path, rawNode: Node): Try[Unit] = {
|
||||
def verifyConfig: Try[Unit] = {
|
||||
implicitly[YamlDecoder[GlobalConfig]].decode(rawNode) match {
|
||||
case Left(failure) =>
|
||||
Failure(
|
||||
InvalidConfigError(
|
||||
@ -112,16 +249,19 @@ object GlobalConfigurationManager {
|
||||
)
|
||||
case Right(_) => Success(())
|
||||
}
|
||||
}
|
||||
|
||||
def bufferedWriter: BufferedWriter = {
|
||||
Files.createDirectories(path.getParent)
|
||||
Files.newBufferedWriter(path)
|
||||
}
|
||||
def writeConfig: Try[Unit] =
|
||||
Using(bufferedWriter) { writer =>
|
||||
val string = yaml.Printer.spaces2
|
||||
.copy(preserveOrder = true)
|
||||
.pretty(rawConfig)
|
||||
writer.write(string)
|
||||
val dumperOptions = new DumperOptions()
|
||||
dumperOptions.setIndent(2)
|
||||
dumperOptions.setPrettyFlow(true)
|
||||
val yaml = new Yaml(dumperOptions)
|
||||
yaml.serialize(rawNode, writer)
|
||||
writer.newLine()
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
package org.enso.editions
|
||||
|
||||
import io.circe.syntax.EncoderOps
|
||||
import io.circe.{Decoder, DecodingFailure, Encoder, Json}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
import org.yaml.snakeyaml.nodes.{Node, ScalarNode, Tag}
|
||||
import org.enso.yaml.{YamlDecoder, YamlEncoder}
|
||||
|
||||
/** A helper type to handle special parsing logic of edition names.
|
||||
*
|
||||
@ -23,32 +24,29 @@ object EditionName {
|
||||
/** A helper method for constructing an [[EditionName]]. */
|
||||
def apply(name: String): EditionName = new EditionName(name)
|
||||
|
||||
/** A [[Decoder]] instance for [[EditionName]] that accepts not only strings
|
||||
* but also numbers as valid edition names.
|
||||
*/
|
||||
implicit val editionNameDecoder: Decoder[EditionName] = { json =>
|
||||
json
|
||||
.as[String]
|
||||
.fold[Either[DecodingFailure, Any]](
|
||||
_ =>
|
||||
if (json.value == Json.Null)
|
||||
Left(DecodingFailure("edition cannot be empty", Nil))
|
||||
else
|
||||
json.as[Int].orElse(json.as[Float]),
|
||||
Right(_)
|
||||
)
|
||||
.map(v => EditionName(v.toString))
|
||||
}
|
||||
implicit val yamlDecoder: YamlDecoder[EditionName] =
|
||||
new YamlDecoder[EditionName] {
|
||||
override def decode(node: Node): Either[Throwable, EditionName] =
|
||||
node match {
|
||||
case scalarNode: ScalarNode =>
|
||||
scalarNode.getTag match {
|
||||
case Tag.NULL =>
|
||||
Left(new YAMLException("edition cannot be empty"))
|
||||
case _ =>
|
||||
val stringDecoder = implicitly[YamlDecoder[String]]
|
||||
stringDecoder.decode(scalarNode).map(EditionName(_))
|
||||
}
|
||||
case _ =>
|
||||
Left(new YAMLException("unexpected edition name"))
|
||||
}
|
||||
}
|
||||
|
||||
/** An [[Encoder]] instance for serializing [[EditionName]].
|
||||
*
|
||||
* Regardless of the original representation, the edition name is always
|
||||
* serialized as string as this is the most portable and precise format for
|
||||
* this datatype.
|
||||
*/
|
||||
implicit val encoder: Encoder[EditionName] = { case EditionName(name) =>
|
||||
name.asJson
|
||||
}
|
||||
implicit val yamlEncoder: YamlEncoder[EditionName] =
|
||||
new YamlEncoder[EditionName] {
|
||||
override def encode(value: EditionName): Object = {
|
||||
value.name
|
||||
}
|
||||
}
|
||||
|
||||
/** The filename suffix that is used to create a filename corresponding to a
|
||||
* named edition.
|
||||
|
@ -1,17 +1,10 @@
|
||||
package org.enso.editions
|
||||
|
||||
import cats.Show
|
||||
import io.circe._
|
||||
import io.circe.syntax.EncoderOps
|
||||
import io.circe.yaml.Parser
|
||||
import org.enso.editions.Editions.{Raw, Repository}
|
||||
import org.enso.semver.SemVerJson._
|
||||
import org.enso.editions.Editions.Raw
|
||||
import org.enso.yaml.YamlHelper
|
||||
|
||||
import java.io.FileReader
|
||||
import java.nio.file.Path
|
||||
import scala.util.{Failure, Try, Using}
|
||||
import org.enso.semver.SemVer
|
||||
import scala.util.{Failure, Try}
|
||||
|
||||
/** Gathers methods for decoding and encoding of Raw editions. */
|
||||
object EditionSerialization {
|
||||
@ -26,12 +19,8 @@ object EditionSerialization {
|
||||
|
||||
/** Tries to load an edition definition from a YAML file. */
|
||||
def loadEdition(path: Path): Try[Raw.Edition] =
|
||||
Using(new FileReader(path.toFile)) { reader =>
|
||||
Parser.default
|
||||
.parse(reader)
|
||||
.flatMap(_.as[Raw.Edition])
|
||||
.toTry
|
||||
}.flatten
|
||||
YamlHelper
|
||||
.load[Raw.Edition](path)
|
||||
.recoverWith { error =>
|
||||
Failure(
|
||||
EditionResolutionError.wrapLoadingError(
|
||||
@ -41,82 +30,6 @@ object EditionSerialization {
|
||||
)
|
||||
}
|
||||
|
||||
/** A [[Decoder]] instance for [[Raw.Edition]].
|
||||
*
|
||||
* It can be used to decode nested editions in other kinds of configurations
|
||||
* files, for example in `package.yaml`.
|
||||
*/
|
||||
implicit val editionDecoder: Decoder[Raw.Edition] = { json =>
|
||||
for {
|
||||
parent <- json.get[Option[EditionName]](Fields.parent)
|
||||
engineVersion <- json.get[Option[SemVer]](Fields.engineVersion)
|
||||
_ <-
|
||||
if (parent.isEmpty && engineVersion.isEmpty)
|
||||
Left(
|
||||
DecodingFailure(
|
||||
s"The edition must specify at least one of " +
|
||||
s"${Fields.engineVersion} or ${Fields.parent}.",
|
||||
json.history
|
||||
)
|
||||
)
|
||||
else Right(())
|
||||
repositories <-
|
||||
json.getOrElse[Seq[Repository]](Fields.repositories)(Seq())
|
||||
libraries <- json.getOrElse[Seq[Raw.Library]](Fields.libraries)(Seq())
|
||||
res <- {
|
||||
val repositoryMap = Map.from(repositories.map(r => (r.name, r)))
|
||||
val libraryMap = Map.from(libraries.map(l => (l.name, l)))
|
||||
if (libraryMap.size != libraries.size)
|
||||
Left(
|
||||
DecodingFailure(
|
||||
"Names of libraries defined within a single edition file must be unique.",
|
||||
json.downField(Fields.libraries).history
|
||||
)
|
||||
)
|
||||
else if (repositoryMap.size != repositories.size)
|
||||
Left(
|
||||
DecodingFailure(
|
||||
"Names of repositories defined within a single edition file must be unique.",
|
||||
json.downField(Fields.libraries).history
|
||||
)
|
||||
)
|
||||
else
|
||||
Right(
|
||||
Raw.Edition(
|
||||
parent = parent.map(_.name),
|
||||
engineVersion = engineVersion,
|
||||
repositories = repositoryMap,
|
||||
libraries = libraryMap
|
||||
)
|
||||
)
|
||||
}
|
||||
} yield res
|
||||
}
|
||||
|
||||
/** An [[Encoder]] instance for [[Raw.Edition]]. */
|
||||
implicit val editionEncoder: Encoder[Raw.Edition] = { edition =>
|
||||
val parent = edition.parent.map { parent => Fields.parent -> parent.asJson }
|
||||
val engineVersion = edition.engineVersion.map { version =>
|
||||
Fields.engineVersion -> version.asJson
|
||||
}
|
||||
|
||||
if (parent.isEmpty && engineVersion.isEmpty) {
|
||||
throw new IllegalArgumentException(
|
||||
"Internal error: An edition must specify at least the engine version or extends clause"
|
||||
)
|
||||
}
|
||||
|
||||
val repositories =
|
||||
if (edition.repositories.isEmpty) None
|
||||
else Some(Fields.repositories -> edition.repositories.values.asJson)
|
||||
val libraries =
|
||||
if (edition.libraries.isEmpty) None
|
||||
else Some(Fields.libraries -> edition.libraries.values.asJson)
|
||||
Json.obj(
|
||||
parent.toSeq ++ engineVersion.toSeq ++ repositories.toSeq ++ libraries.toSeq: _*
|
||||
)
|
||||
}
|
||||
|
||||
object Fields {
|
||||
val name = "name"
|
||||
val version = "version"
|
||||
@ -136,99 +49,4 @@ object EditionSerialization {
|
||||
override def toString: String = message
|
||||
}
|
||||
|
||||
object EditionLoadingError {
|
||||
|
||||
/** Creates a [[EditionLoadingError]] by wrapping another [[Throwable]].
|
||||
*
|
||||
* Special logic is used for [[io.circe.Error]] to display the error
|
||||
* summary in a human-readable way.
|
||||
*/
|
||||
def fromThrowable(throwable: Throwable): EditionLoadingError =
|
||||
throwable match {
|
||||
case decodingError: io.circe.Error =>
|
||||
val errorMessage =
|
||||
implicitly[Show[io.circe.Error]].show(decodingError)
|
||||
EditionLoadingError(
|
||||
s"Could not parse the edition file: $errorMessage",
|
||||
decodingError
|
||||
)
|
||||
case other =>
|
||||
EditionLoadingError(s"Could not load the edition file: $other", other)
|
||||
}
|
||||
}
|
||||
|
||||
implicit private val libraryDecoder: Decoder[Raw.Library] = { json =>
|
||||
def makeLibrary(
|
||||
name: LibraryName,
|
||||
repository: String,
|
||||
version: Option[SemVer]
|
||||
) =
|
||||
if (repository == Fields.localRepositoryName)
|
||||
if (version.isDefined)
|
||||
Left(
|
||||
DecodingFailure(
|
||||
"Version field must not be set for libraries associated with the local repository.",
|
||||
json.history
|
||||
)
|
||||
)
|
||||
else Right(Raw.LocalLibrary(name))
|
||||
else {
|
||||
version match {
|
||||
case Some(versionValue) =>
|
||||
Right(Raw.PublishedLibrary(name, versionValue, repository))
|
||||
case None =>
|
||||
Left(
|
||||
DecodingFailure(
|
||||
"Version field is mandatory for non-local libraries.",
|
||||
json.history
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
for {
|
||||
name <- json.get[LibraryName](Fields.name)
|
||||
repository <- json.get[String](Fields.repository)
|
||||
version <- json.get[Option[SemVer]](Fields.version)
|
||||
res <- makeLibrary(name, repository, version)
|
||||
} yield res
|
||||
}
|
||||
|
||||
implicit private val libraryEncoder: Encoder[Raw.Library] = {
|
||||
case Raw.LocalLibrary(name) =>
|
||||
Json.obj(
|
||||
Fields.name -> name.asJson,
|
||||
Fields.repository -> Fields.localRepositoryName.asJson
|
||||
)
|
||||
case Raw.PublishedLibrary(name, version, repository) =>
|
||||
Json.obj(
|
||||
Fields.name -> name.asJson,
|
||||
Fields.version -> version.asJson,
|
||||
Fields.repository -> repository.asJson
|
||||
)
|
||||
}
|
||||
|
||||
implicit private val repositoryEncoder: Encoder[Repository] = { repo =>
|
||||
Json.obj(
|
||||
Fields.name -> repo.name.asJson,
|
||||
Fields.url -> repo.url.asJson
|
||||
)
|
||||
}
|
||||
|
||||
implicit private val repositoryDecoder: Decoder[Repository] = { json =>
|
||||
val nameField = json.downField(Fields.name)
|
||||
for {
|
||||
name <- nameField.as[String]
|
||||
url <- json.get[String](Fields.url)
|
||||
res <-
|
||||
if (name == Fields.localRepositoryName)
|
||||
Left(
|
||||
DecodingFailure(
|
||||
s"A defined repository cannot be called " +
|
||||
s"`${Fields.localRepositoryName}` which is a reserved keyword.",
|
||||
nameField.history
|
||||
)
|
||||
)
|
||||
else Right(Repository(name, url))
|
||||
} yield res
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
package org.enso.editions
|
||||
|
||||
import org.enso.semver.SemVer
|
||||
import org.enso.semver.{SemVer, SemVerYaml}
|
||||
import org.yaml.snakeyaml.nodes.{MappingNode, Node, ScalarNode}
|
||||
import org.enso.yaml.{YamlDecoder, YamlEncoder}
|
||||
import org.enso.yaml.YamlDecoder.MapKeyField
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
|
||||
import java.util
|
||||
|
||||
/** Defines the general edition structure.
|
||||
*
|
||||
@ -43,6 +49,136 @@ trait Editions {
|
||||
def name: LibraryName
|
||||
}
|
||||
|
||||
implicit def nestedEditionTypeDecoder: YamlDecoder[NestedEditionType]
|
||||
implicit def libraryRepositoryTypeDecoder: YamlDecoder[LibraryRepositoryType]
|
||||
|
||||
implicit def nestedEditionTypeEncoder: YamlEncoder[NestedEditionType]
|
||||
implicit def libraryRepositoryTypeEncoder: YamlEncoder[LibraryRepositoryType]
|
||||
|
||||
object Library {
|
||||
|
||||
trait LibraryFields {
|
||||
val Name = "name"
|
||||
}
|
||||
|
||||
object LocalLibraryFields extends LibraryFields
|
||||
object PublishedLibraryFields extends LibraryFields {
|
||||
val Version = "version"
|
||||
val Repository = "repository"
|
||||
}
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[Library] =
|
||||
new YamlDecoder[Library] {
|
||||
override def decode(node: Node): Either[Throwable, Library] =
|
||||
node match {
|
||||
case mappingNode: MappingNode =>
|
||||
val bindings = mappingKV(mappingNode)
|
||||
|
||||
bindings.get(LocalLibraryFields.Name) match {
|
||||
case Some(node) =>
|
||||
val repoField =
|
||||
bindings.get(PublishedLibraryFields.Repository)
|
||||
|
||||
val isLocalRepo = repoField
|
||||
.map(node =>
|
||||
node.isInstanceOf[ScalarNode] && node
|
||||
.asInstanceOf[ScalarNode]
|
||||
.getValue == "local"
|
||||
)
|
||||
.getOrElse(false)
|
||||
if (isLocalRepo) {
|
||||
if (bindings.contains(PublishedLibraryFields.Version))
|
||||
Left(
|
||||
new YAMLException(
|
||||
s"'${PublishedLibraryFields.Version}' field must not be set for libraries associated with the local repository"
|
||||
)
|
||||
)
|
||||
else
|
||||
implicitly[YamlDecoder[LibraryName]]
|
||||
.decode(node)
|
||||
.map(LocalLibrary(_))
|
||||
} else {
|
||||
val libraryNameDecoder =
|
||||
implicitly[YamlDecoder[LibraryName]]
|
||||
val versionDecoder = SemVerYaml.yamlSemverDecoder
|
||||
val repositoryDecoder =
|
||||
implicitly[YamlDecoder[LibraryRepositoryType]]
|
||||
for {
|
||||
name <- bindings
|
||||
.get(PublishedLibraryFields.Name)
|
||||
.toRight(
|
||||
new YAMLException(
|
||||
s"Missing '${PublishedLibraryFields.Name}' field"
|
||||
)
|
||||
)
|
||||
.flatMap(libraryNameDecoder.decode)
|
||||
version <- bindings
|
||||
.get(PublishedLibraryFields.Version)
|
||||
.toRight(
|
||||
new YAMLException(
|
||||
s"'${PublishedLibraryFields.Version}' field is mandatory for non-local libraries"
|
||||
)
|
||||
)
|
||||
.flatMap(versionDecoder.decode)
|
||||
repository <- bindings
|
||||
.get(PublishedLibraryFields.Repository)
|
||||
.toRight(
|
||||
new YAMLException(
|
||||
s"Missing '${PublishedLibraryFields.Repository}' field"
|
||||
)
|
||||
)
|
||||
.flatMap(repositoryDecoder.decode)
|
||||
} yield PublishedLibrary(name, version, repository)
|
||||
}
|
||||
case None =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
s"Library requires `${LocalLibraryFields.Name}` field"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlEncoder: YamlEncoder[Library] =
|
||||
new YamlEncoder[Library] {
|
||||
import SemVerYaml._
|
||||
override def encode(value: Library) = {
|
||||
val libraryNamencoder = implicitly[YamlEncoder[LibraryName]]
|
||||
val elements = new util.ArrayList[(String, Object)](1)
|
||||
value match {
|
||||
case local: LocalLibrary =>
|
||||
elements.add(
|
||||
(LocalLibraryFields.Name, libraryNamencoder.encode(local.name))
|
||||
)
|
||||
case remote: PublishedLibrary =>
|
||||
val semverEncoder = implicitly[YamlEncoder[SemVer]]
|
||||
val repoEncoder =
|
||||
implicitly[YamlEncoder[LibraryRepositoryType]]
|
||||
elements.add(
|
||||
(
|
||||
PublishedLibraryFields.Name,
|
||||
libraryNamencoder.encode(remote.name)
|
||||
)
|
||||
)
|
||||
elements.add(
|
||||
(
|
||||
PublishedLibraryFields.Version,
|
||||
semverEncoder.encode(remote.version)
|
||||
)
|
||||
)
|
||||
elements.add(
|
||||
(
|
||||
PublishedLibraryFields.Repository,
|
||||
repoEncoder.encode(remote.repository)
|
||||
)
|
||||
)
|
||||
}
|
||||
toMap(elements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents a local library. */
|
||||
case class LocalLibrary(override val name: LibraryName) extends Library
|
||||
|
||||
@ -111,6 +247,99 @@ trait Editions {
|
||||
libraries.map(l => (l.name, l)).toMap
|
||||
)
|
||||
}
|
||||
|
||||
object Fields {
|
||||
val Parent = "extends"
|
||||
val EngineVersion = "engine-version"
|
||||
val Repositories = "repositories"
|
||||
val Libraries = "libraries"
|
||||
}
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[Edition] =
|
||||
new YamlDecoder[Edition] {
|
||||
import org.enso.semver.SemVerYaml._
|
||||
override def decode(node: Node): Either[Throwable, Edition] = node match {
|
||||
case mappingNode: MappingNode =>
|
||||
val clazzMap = mappingKV(mappingNode)
|
||||
val parentDecoder =
|
||||
implicitly[YamlDecoder[Option[NestedEditionType]]]
|
||||
val semverDecoder = implicitly[YamlDecoder[Option[SemVer]]]
|
||||
implicit val mapKey = MapKeyField.plainField("name")
|
||||
val repositoriesDecoder =
|
||||
implicitly[YamlDecoder[Map[String, Editions.Repository]]]
|
||||
val librariesDecoder =
|
||||
implicitly[YamlDecoder[Map[LibraryName, Library]]]
|
||||
for {
|
||||
parent <- clazzMap
|
||||
.get(Fields.Parent)
|
||||
.map(parentDecoder.decode)
|
||||
.getOrElse(Right(None))
|
||||
engineVersion <- clazzMap
|
||||
.get(Fields.EngineVersion)
|
||||
.map(semverDecoder.decode)
|
||||
.getOrElse(Right(None))
|
||||
_ <-
|
||||
if (parent.isEmpty && engineVersion.isEmpty)
|
||||
Left(
|
||||
new YAMLException(
|
||||
s"The edition must specify at least one of " +
|
||||
s"`${Fields.EngineVersion}` or `${Fields.Parent}`"
|
||||
)
|
||||
)
|
||||
else Right(())
|
||||
repositories <- clazzMap
|
||||
.get(Fields.Repositories)
|
||||
.map(repositoriesDecoder.decode)
|
||||
.getOrElse(Right(Map.empty[String, Editions.Repository]))
|
||||
libraries <- clazzMap
|
||||
.get(Fields.Libraries)
|
||||
.map(librariesDecoder.decode)
|
||||
.getOrElse(Right(Map.empty[LibraryName, Library]))
|
||||
|
||||
} yield Edition(parent, engineVersion, repositories, libraries)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlEncoder: YamlEncoder[Edition] =
|
||||
new YamlEncoder[Edition] {
|
||||
import SemVerYaml._
|
||||
override def encode(value: Edition) = {
|
||||
val parentEncoder = implicitly[YamlEncoder[NestedEditionType]]
|
||||
val semverEncoder = implicitly[YamlEncoder[SemVer]]
|
||||
val repositoriesEncoder =
|
||||
implicitly[YamlEncoder[Seq[Editions.Repository]]]
|
||||
val librariesEncoder = implicitly[YamlEncoder[Seq[Library]]]
|
||||
|
||||
if (value.parent.isEmpty && value.engineVersion.isEmpty)
|
||||
throw new YAMLException(
|
||||
s"The edition must specify at least one of " +
|
||||
s"`${Fields.EngineVersion}` or `${Fields.Parent}`"
|
||||
)
|
||||
val elements = new util.ArrayList[(String, Object)]()
|
||||
value.parent
|
||||
.map(parentEncoder.encode)
|
||||
.foreach(n => elements.add((Fields.Parent, n)))
|
||||
value.engineVersion
|
||||
.map(semverEncoder.encode)
|
||||
.foreach(n => elements.add((Fields.EngineVersion, n)))
|
||||
if (value.libraries.nonEmpty)
|
||||
elements.add(
|
||||
(
|
||||
Fields.Repositories,
|
||||
repositoriesEncoder.encode(value.repositories.values.toSeq)
|
||||
)
|
||||
)
|
||||
if (value.repositories.nonEmpty)
|
||||
elements.add(
|
||||
(
|
||||
Fields.Libraries,
|
||||
librariesEncoder.encode(value.libraries.values.toSeq)
|
||||
)
|
||||
)
|
||||
toMap(elements)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Editions {
|
||||
@ -120,11 +349,53 @@ object Editions {
|
||||
|
||||
object Repository {
|
||||
|
||||
object Fields {
|
||||
val Name = "name"
|
||||
val Url = "url"
|
||||
}
|
||||
|
||||
/** An alternative constructor for unnamed repositories.
|
||||
*
|
||||
* The URL is used as the repository name.
|
||||
*/
|
||||
def apply(url: String): Repository = Repository(url, url)
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[Repository] = {
|
||||
new YamlDecoder[Repository] {
|
||||
override def decode(node: Node): Either[Throwable, Repository] =
|
||||
node match {
|
||||
case mappingNode: MappingNode =>
|
||||
val stringDecoder = implicitly[YamlDecoder[String]]
|
||||
val clazzMap = mappingKV(mappingNode)
|
||||
for {
|
||||
name <- clazzMap
|
||||
.get(Fields.Name)
|
||||
.toRight(
|
||||
new YAMLException(s"Missing '${Fields.Name}' field")
|
||||
)
|
||||
.flatMap(stringDecoder.decode)
|
||||
url <- clazzMap
|
||||
.get(Fields.Url)
|
||||
.toRight(
|
||||
new YAMLException(s"Missing '${Fields.Url}' field")
|
||||
)
|
||||
.flatMap(stringDecoder.decode)
|
||||
} yield Repository(name, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlEncoder: YamlEncoder[Repository] = {
|
||||
new YamlEncoder[Repository] {
|
||||
override def encode(value: Repository) = {
|
||||
val elements = new util.ArrayList[(String, Object)](2)
|
||||
elements.add((Fields.Name, value.name))
|
||||
elements.add((Fields.Url, value.url))
|
||||
toMap(elements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Implements the Raw editions that can be directly parsed from a YAML
|
||||
@ -136,6 +407,18 @@ object Editions {
|
||||
object Raw extends Editions {
|
||||
override type NestedEditionType = String
|
||||
override type LibraryRepositoryType = String
|
||||
|
||||
implicit override def nestedEditionTypeDecoder
|
||||
: YamlDecoder[NestedEditionType] = YamlDecoder.stringDecoderYaml
|
||||
implicit override def libraryRepositoryTypeDecoder
|
||||
: YamlDecoder[LibraryRepositoryType] =
|
||||
YamlDecoder.stringDecoderYaml
|
||||
|
||||
implicit override def nestedEditionTypeEncoder: YamlEncoder[String] =
|
||||
YamlEncoder.stringEncoderYaml
|
||||
|
||||
implicit override def libraryRepositoryTypeEncoder: YamlEncoder[String] =
|
||||
YamlEncoder.stringEncoderYaml
|
||||
}
|
||||
|
||||
/** Implements the Resolved editions which are obtained by analyzing the Raw
|
||||
@ -144,6 +427,17 @@ object Editions {
|
||||
object Resolved extends Editions {
|
||||
override type NestedEditionType = this.Edition
|
||||
override type LibraryRepositoryType = Repository
|
||||
|
||||
implicit override def nestedEditionTypeDecoder
|
||||
: YamlDecoder[NestedEditionType] = yamlDecoder
|
||||
implicit override def libraryRepositoryTypeDecoder
|
||||
: YamlDecoder[Repository] = Repository.yamlDecoder
|
||||
|
||||
implicit override def nestedEditionTypeEncoder
|
||||
: YamlEncoder[NestedEditionType] = yamlEncoder
|
||||
|
||||
implicit override def libraryRepositoryTypeEncoder
|
||||
: YamlEncoder[Repository] = Repository.yamlEncoder
|
||||
}
|
||||
|
||||
/** An alias for Raw editions. */
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.enso.editions
|
||||
|
||||
import io.circe.syntax._
|
||||
import io.circe.syntax.EncoderOps
|
||||
import io.circe.{Decoder, DecodingFailure, Encoder}
|
||||
import org.enso.semver.SemVer
|
||||
|
||||
|
@ -2,6 +2,9 @@ package org.enso.editions
|
||||
|
||||
import io.circe.syntax.EncoderOps
|
||||
import io.circe.{Decoder, DecodingFailure, Encoder}
|
||||
import org.enso.yaml.{YamlDecoder, YamlEncoder}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
import org.yaml.snakeyaml.nodes.{MappingNode, Node, ScalarNode}
|
||||
|
||||
/** Represents a library name that should uniquely identify the library.
|
||||
*
|
||||
@ -22,6 +25,47 @@ case class LibraryName(namespace: String, name: String) {
|
||||
|
||||
object LibraryName {
|
||||
|
||||
object Fields {
|
||||
val Namespace = "namespace"
|
||||
val Email = "email"
|
||||
}
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[LibraryName] =
|
||||
new YamlDecoder[LibraryName] {
|
||||
override def decode(node: Node): Either[Throwable, LibraryName] =
|
||||
node match {
|
||||
case mappingNode: MappingNode =>
|
||||
val stringDecoder = implicitly[YamlDecoder[String]]
|
||||
val clazzMap = mappingKV(mappingNode)
|
||||
for {
|
||||
namesapce <- clazzMap
|
||||
.get(Fields.Namespace)
|
||||
.toRight(
|
||||
new YAMLException(s"Missing '${Fields.Namespace}' field")
|
||||
)
|
||||
.flatMap(stringDecoder.decode)
|
||||
email <- clazzMap
|
||||
.get(Fields.Email)
|
||||
.toRight(
|
||||
new YAMLException(s"Missing '${Fields.Email}' field")
|
||||
)
|
||||
.flatMap(stringDecoder.decode)
|
||||
} yield LibraryName(namesapce, email)
|
||||
case scalarNode: ScalarNode =>
|
||||
val v = scalarNode.getValue
|
||||
fromModuleName(v).toRight(
|
||||
new YAMLException(s"'$v' is not a valid library name")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlEncoder: YamlEncoder[LibraryName] =
|
||||
new YamlEncoder[LibraryName] {
|
||||
override def encode(value: LibraryName) = {
|
||||
value.toString
|
||||
}
|
||||
}
|
||||
|
||||
/** A [[Decoder]] instance allowing to parse a [[LibraryName]]. */
|
||||
implicit val decoder: Decoder[LibraryName] = { json =>
|
||||
for {
|
||||
|
@ -1,8 +1,9 @@
|
||||
package org.enso.editions.repository
|
||||
|
||||
import io.circe._
|
||||
import io.circe.syntax.EncoderOps
|
||||
import org.enso.editions.EditionName
|
||||
import org.enso.yaml.{YamlDecoder, YamlEncoder}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
import org.yaml.snakeyaml.nodes.{MappingNode, Node, ScalarNode, SequenceNode}
|
||||
|
||||
/** The Edition Repository manifest, which lists all editions that the
|
||||
* repository provides.
|
||||
@ -14,17 +15,35 @@ object Manifest {
|
||||
val editions = "editions"
|
||||
}
|
||||
|
||||
/** A [[Decoder]] instance for parsing [[Manifest]]. */
|
||||
implicit val decoder: Decoder[Manifest] = { json =>
|
||||
for {
|
||||
editions <- json.get[Seq[EditionName]](Fields.editions)
|
||||
} yield Manifest(editions)
|
||||
}
|
||||
implicit val yamlDecoder: YamlDecoder[Manifest] =
|
||||
new YamlDecoder[Manifest] {
|
||||
override def decode(node: Node): Either[Throwable, Manifest] =
|
||||
node match {
|
||||
case seqNode: SequenceNode =>
|
||||
val decoder = implicitly[YamlDecoder[Seq[EditionName]]]
|
||||
decoder.decode(seqNode).map(Manifest(_))
|
||||
case mappingNode: MappingNode if mappingNode.getValue.size() == 1 =>
|
||||
val editionsNode = mappingNode.getValue.get(0)
|
||||
(editionsNode.getKeyNode, editionsNode.getValueNode) match {
|
||||
case (keyNode: ScalarNode, seqNode: SequenceNode)
|
||||
if keyNode.getValue == Fields.editions =>
|
||||
val decoder = implicitly[YamlDecoder[Seq[EditionName]]]
|
||||
decoder.decode(seqNode).map(Manifest(_))
|
||||
case _ =>
|
||||
Left(new YAMLException("Failed to decode editions"))
|
||||
}
|
||||
case _ =>
|
||||
Left(new YAMLException("Failed to decode editions"))
|
||||
}
|
||||
}
|
||||
|
||||
/** An [[Encoder]] instance for serializing [[Manifest]]. */
|
||||
implicit val encoder: Encoder[Manifest] = { manifest =>
|
||||
Json.obj(Fields.editions -> manifest.editions.asJson)
|
||||
}
|
||||
implicit val yamlEncoder: YamlEncoder[Manifest] =
|
||||
new YamlEncoder[Manifest] {
|
||||
override def encode(value: Manifest) = {
|
||||
val editionsEncoder = implicitly[YamlEncoder[Seq[EditionName]]]
|
||||
toMap(Fields.editions, editionsEncoder.encode(value.editions))
|
||||
}
|
||||
}
|
||||
|
||||
/** The name of the manifest file that should be present at the root of
|
||||
* editions repository.
|
||||
|
@ -1,19 +1,15 @@
|
||||
package org.enso.yaml
|
||||
|
||||
import cats.Show
|
||||
|
||||
/** Indicates a parse failure, usually meaning that the input data has
|
||||
* unexpected format (like missing fields or wrong field types).
|
||||
*/
|
||||
case class ParseError(message: String, cause: io.circe.Error)
|
||||
case class ParseError(message: String, cause: Throwable)
|
||||
extends RuntimeException(message, cause)
|
||||
|
||||
object ParseError {
|
||||
|
||||
/** Wraps a [[io.circe.Error]] into a more user-friendly [[ParseError]]. */
|
||||
def apply(error: io.circe.Error): ParseError = {
|
||||
val errorMessage =
|
||||
implicitly[Show[io.circe.Error]].show(error)
|
||||
ParseError(errorMessage, error)
|
||||
/** Wraps a parser exception into a more user-friendly [[ParseError]]. */
|
||||
def apply(error: Throwable): ParseError = {
|
||||
ParseError(error.getMessage, error)
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
package org.enso.yaml
|
||||
|
||||
import io.circe.yaml.{Parser, Printer}
|
||||
import io.circe.{yaml, Decoder, Encoder}
|
||||
import org.yaml.snakeyaml.nodes.Tag
|
||||
import org.yaml.snakeyaml.{DumperOptions, Yaml}
|
||||
|
||||
import java.io.FileReader
|
||||
import java.io.{FileReader, StringReader}
|
||||
import java.nio.file.Path
|
||||
import scala.util.{Try, Using}
|
||||
|
||||
@ -13,23 +13,29 @@ object YamlHelper {
|
||||
/** Parses a string representation of a YAML configuration of type `R`. */
|
||||
def parseString[R](
|
||||
yamlString: String
|
||||
)(implicit decoder: Decoder[R]): Either[ParseError, R] =
|
||||
yaml.parser
|
||||
.parse(yamlString)
|
||||
.flatMap(_.as[R])
|
||||
)(implicit decoder: YamlDecoder[R]): Either[ParseError, R] = {
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
Try(snakeYaml.compose(new StringReader(yamlString))).toEither
|
||||
.flatMap(decoder.decode(_))
|
||||
.left
|
||||
.map(ParseError(_))
|
||||
}
|
||||
|
||||
/** Tries to load and parse a YAML file at the provided path. */
|
||||
def load[R](path: Path)(implicit decoder: Decoder[R]): Try[R] =
|
||||
def load[R](path: Path)(implicit decoder: YamlDecoder[R]): Try[R] =
|
||||
Using(new FileReader(path.toFile)) { reader =>
|
||||
Parser.default
|
||||
.parse(reader)
|
||||
.flatMap(_.as[R])
|
||||
.toTry
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
Try(snakeYaml.compose(reader))
|
||||
.flatMap(decoder.decode(_).toTry)
|
||||
}.flatten
|
||||
|
||||
/** Saves a YAML representation of an object into a string. */
|
||||
def toYaml[A](obj: A)(implicit encoder: Encoder[A]): String =
|
||||
Printer.spaces2.copy(preserveOrder = true).pretty(encoder(obj))
|
||||
def toYaml[A](obj: A)(implicit encoder: YamlEncoder[A]): String = {
|
||||
val node = encoder.encode(obj)
|
||||
val dumperOptions = new DumperOptions()
|
||||
dumperOptions.setIndent(2)
|
||||
dumperOptions.setPrettyFlow(true)
|
||||
val yaml = new Yaml(dumperOptions)
|
||||
yaml.dumpAs(node, Tag.MAP, DumperOptions.FlowStyle.BLOCK)
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +81,23 @@ class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside {
|
||||
}
|
||||
}
|
||||
|
||||
"not allow non-unique libraries" in {
|
||||
val parsed = EditionSerialization.parseYamlString(
|
||||
"""engine-version: 1.2.3-SNAPSHOT
|
||||
|libraries:
|
||||
|- name: Foo.local
|
||||
| repository: local
|
||||
|- name: Foo.local
|
||||
| repository: local
|
||||
|""".stripMargin
|
||||
)
|
||||
inside(parsed) { case Failure(exception) =>
|
||||
exception.getMessage should include(
|
||||
"YAML definition contains duplicate entries"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"not allow invalid version combinations for libraries" in {
|
||||
val parsed = EditionSerialization.parseYamlString(
|
||||
"""extends: foo
|
||||
@ -91,7 +108,7 @@ class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside {
|
||||
|""".stripMargin
|
||||
)
|
||||
inside(parsed) { case Failure(exception) =>
|
||||
exception.getMessage should include("Version field must not be set")
|
||||
exception.getMessage should include("'version' field must not be set")
|
||||
}
|
||||
|
||||
val parsed2 = EditionSerialization.parseYamlString(
|
||||
@ -102,7 +119,9 @@ class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside {
|
||||
|""".stripMargin
|
||||
)
|
||||
inside(parsed2) { case Failure(exception) =>
|
||||
exception.getMessage should include("Version field is mandatory")
|
||||
exception.getMessage should include(
|
||||
"'version' field is mandatory for non-local libraries"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import org.enso.semver.SemVer
|
||||
import org.enso.cli.OS
|
||||
import org.enso.distribution.FileSystem
|
||||
import org.enso.downloader.archive.TarGzWriter
|
||||
import org.enso.editions.EditionSerialization.editionEncoder
|
||||
import org.enso.editions.Editions.RawEdition
|
||||
import org.enso.editions.{Editions, LibraryName}
|
||||
import org.enso.pkg.{Package, PackageManager}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package org.enso.librarymanager.published.repository
|
||||
|
||||
import io.circe.{Decoder, Encoder, Json}
|
||||
import io.circe.syntax.EncoderOps
|
||||
import io.circe.yaml
|
||||
import org.enso.editions.LibraryName
|
||||
import org.enso.yaml.{YamlDecoder, YamlEncoder}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
import org.yaml.snakeyaml.nodes.{MappingNode, Node}
|
||||
|
||||
import java.io.StringReader
|
||||
import java.util
|
||||
import scala.util.Try
|
||||
|
||||
/** The manifest file containing metadata related to a published library.
|
||||
@ -41,40 +43,73 @@ object LibraryManifest {
|
||||
val description = "description"
|
||||
}
|
||||
|
||||
/** A [[Decoder]] instance for parsing [[LibraryManifest]]. */
|
||||
implicit val decoder: Decoder[LibraryManifest] = { json =>
|
||||
for {
|
||||
archives <- json.get[Seq[String]](Fields.archives)
|
||||
dependencies <- json.getOrElse[Seq[LibraryName]](Fields.dependencies)(
|
||||
Seq()
|
||||
)
|
||||
tagLine <- json.get[Option[String]](Fields.tagLine)
|
||||
description <- json.get[Option[String]](Fields.description)
|
||||
} yield LibraryManifest(
|
||||
archives = archives,
|
||||
dependencies = dependencies,
|
||||
tagLine = tagLine,
|
||||
description = description
|
||||
)
|
||||
}
|
||||
implicit val yamlDecoder: YamlDecoder[LibraryManifest] =
|
||||
new YamlDecoder[LibraryManifest] {
|
||||
override def decode(node: Node): Either[Throwable, LibraryManifest] =
|
||||
node match {
|
||||
case mappingNode: MappingNode =>
|
||||
val archivesDecoder = implicitly[YamlDecoder[Seq[String]]]
|
||||
val dependenciesDecoder =
|
||||
implicitly[YamlDecoder[Seq[LibraryName]]]
|
||||
val optStringDecoder = implicitly[YamlDecoder[Option[String]]]
|
||||
val kv = mappingKV(mappingNode)
|
||||
for {
|
||||
archives <- kv
|
||||
.get(Fields.archives)
|
||||
.map(archivesDecoder.decode(_))
|
||||
.getOrElse(Right(Seq.empty))
|
||||
dependencies <- kv
|
||||
.get(Fields.dependencies)
|
||||
.map(dependenciesDecoder.decode(_))
|
||||
.getOrElse(Right(Seq.empty))
|
||||
tagLine <- kv
|
||||
.get(Fields.tagLine)
|
||||
.map(optStringDecoder.decode(_))
|
||||
.getOrElse(Right(None))
|
||||
description <- kv
|
||||
.get(Fields.description)
|
||||
.map(optStringDecoder.decode(_))
|
||||
.getOrElse(Right(None))
|
||||
} yield LibraryManifest(
|
||||
archives,
|
||||
dependencies,
|
||||
tagLine,
|
||||
description
|
||||
)
|
||||
case _ =>
|
||||
Left(new YAMLException("Unexpected library manifest definition"))
|
||||
}
|
||||
}
|
||||
|
||||
/** An [[Encoder]] instance for parsing [[LibraryManifest]]. */
|
||||
implicit val encoder: Encoder[LibraryManifest] = { manifest =>
|
||||
val baseFields = Seq(
|
||||
Fields.archives -> manifest.archives.asJson,
|
||||
Fields.dependencies -> manifest.dependencies.asJson
|
||||
)
|
||||
|
||||
val allFields = baseFields ++
|
||||
manifest.tagLine.map(Fields.tagLine -> _.asJson).toSeq ++
|
||||
manifest.description.map(Fields.description -> _.asJson).toSeq
|
||||
|
||||
Json.obj(allFields: _*)
|
||||
}
|
||||
implicit val yamlEncoder: YamlEncoder[LibraryManifest] =
|
||||
new YamlEncoder[LibraryManifest] {
|
||||
override def encode(value: LibraryManifest): AnyRef = {
|
||||
val archivesEncoder = implicitly[YamlEncoder[Seq[String]]]
|
||||
val dependenciesEncoder = implicitly[YamlEncoder[Seq[LibraryName]]]
|
||||
val elements = new util.ArrayList[(String, Object)]()
|
||||
if (value.archives.nonEmpty)
|
||||
elements.add(
|
||||
(Fields.archives, archivesEncoder.encode(value.archives))
|
||||
)
|
||||
if (value.dependencies.nonEmpty)
|
||||
elements.add(
|
||||
(
|
||||
Fields.dependencies,
|
||||
dependenciesEncoder.encode(value.dependencies)
|
||||
)
|
||||
)
|
||||
value.tagLine.foreach(v => elements.add((Fields.tagLine, v)))
|
||||
value.description.foreach(v => elements.add((Fields.description, v)))
|
||||
toMap(elements)
|
||||
}
|
||||
}
|
||||
|
||||
/** Parser the provided string and returns a LibraryManifest, if valid */
|
||||
def fromYaml(yamlString: String): Try[LibraryManifest] = {
|
||||
yaml.parser.parse(yamlString).flatMap(_.as[LibraryManifest]).toTry
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
Try(snakeYaml.compose(new StringReader(yamlString))).toEither
|
||||
.flatMap(implicitly[YamlDecoder[LibraryManifest]].decode(_))
|
||||
.toTry
|
||||
}
|
||||
|
||||
/** The name of the manifest file as included in the directory associated with
|
||||
|
@ -3,6 +3,11 @@ package org.enso.pkg
|
||||
import io.circe._
|
||||
import io.circe.syntax._
|
||||
import org.enso.editions.LibraryName
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
import org.yaml.snakeyaml.nodes.{MappingNode, Node, ScalarNode, SequenceNode}
|
||||
import org.enso.yaml.{YamlDecoder, YamlEncoder}
|
||||
|
||||
import java.util
|
||||
|
||||
/** The description of component groups provided by the package.
|
||||
*
|
||||
@ -45,6 +50,54 @@ object ComponentGroups {
|
||||
json.getOrElse[List[ExtendedComponentGroup]](Fields.Extends)(List())
|
||||
} yield ComponentGroups(newGroups, extendsGroups)
|
||||
}
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[ComponentGroups] =
|
||||
new YamlDecoder[ComponentGroups] {
|
||||
override def decode(node: Node): Either[Throwable, ComponentGroups] =
|
||||
node match {
|
||||
case mappingNode: MappingNode =>
|
||||
val clazzMap = mappingKV(mappingNode)
|
||||
val newGroupsDecoder =
|
||||
implicitly[YamlDecoder[List[ComponentGroup]]]
|
||||
val extendedGroupsDecoder =
|
||||
implicitly[YamlDecoder[List[ExtendedComponentGroup]]]
|
||||
for {
|
||||
newGroups <- clazzMap
|
||||
.get(Fields.New)
|
||||
.map(newGroupsDecoder.decode)
|
||||
.getOrElse(Right(Nil))
|
||||
extendedGroups <- clazzMap
|
||||
.get(Fields.Extends)
|
||||
.map(extendedGroupsDecoder.decode)
|
||||
.getOrElse(Right(Nil))
|
||||
} yield ComponentGroups(newGroups, extendedGroups)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlEncoder: YamlEncoder[ComponentGroups] =
|
||||
new YamlEncoder[ComponentGroups] {
|
||||
override def encode(value: ComponentGroups) = {
|
||||
val componentGroupEncoder =
|
||||
implicitly[YamlEncoder[List[ComponentGroup]]]
|
||||
val extendedComponentGoupEncoder =
|
||||
implicitly[YamlEncoder[List[ExtendedComponentGroup]]]
|
||||
val elements = new util.ArrayList[(String, Object)](0)
|
||||
if (value.newGroups.nonEmpty) {
|
||||
elements.add(
|
||||
(Fields.New, componentGroupEncoder.encode(value.newGroups))
|
||||
)
|
||||
}
|
||||
if (value.extendedGroups.nonEmpty) {
|
||||
elements.add(
|
||||
(
|
||||
Fields.Extends,
|
||||
extendedComponentGoupEncoder.encode(value.extendedGroups)
|
||||
)
|
||||
)
|
||||
}
|
||||
toMap(elements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The definition of a single component group.
|
||||
@ -64,11 +117,88 @@ object ComponentGroup {
|
||||
|
||||
/** Fields for use when serializing the [[ComponentGroup]]. */
|
||||
private object Fields {
|
||||
val Group = "group"
|
||||
val Color = "color"
|
||||
val Icon = "icon"
|
||||
val Exports = "exports"
|
||||
}
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[ComponentGroup] =
|
||||
new YamlDecoder[ComponentGroup] {
|
||||
override def decode(node: Node): Either[Throwable, ComponentGroup] =
|
||||
node match {
|
||||
case mappingNode: MappingNode =>
|
||||
if (mappingNode.getValue.size() == 1) {
|
||||
val groupNode = mappingNode.getValue.get(0)
|
||||
(groupNode.getKeyNode, groupNode.getValueNode) match {
|
||||
case (scalarNode: ScalarNode, mappingNode: MappingNode) =>
|
||||
val clazzMap = mappingKV(mappingNode)
|
||||
val groupDecoder = implicitly[YamlDecoder[GroupName]]
|
||||
val colorDecoder =
|
||||
implicitly[YamlDecoder[Option[String]]]
|
||||
val iconDecoder =
|
||||
implicitly[YamlDecoder[Option[String]]]
|
||||
val exportDecoder =
|
||||
implicitly[YamlDecoder[Seq[Component]]]
|
||||
|
||||
for {
|
||||
group <- groupDecoder.decode(scalarNode)
|
||||
color <- clazzMap
|
||||
.get(Fields.Color)
|
||||
.map(colorDecoder.decode)
|
||||
.getOrElse(Right(None))
|
||||
icon <- clazzMap
|
||||
.get(Fields.Icon)
|
||||
.map(iconDecoder.decode)
|
||||
.getOrElse(Right(None))
|
||||
exports <- clazzMap
|
||||
.get(Fields.Exports)
|
||||
.map(exportDecoder.decode)
|
||||
.getOrElse(Right(Seq.empty))
|
||||
} yield ComponentGroup(group, color, icon, exports)
|
||||
case (_: ScalarNode, value: ScalarNode) =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Failed to decode component group. Expected a map field, got a value:" + value.getValue
|
||||
)
|
||||
)
|
||||
case (_: ScalarNode, _: SequenceNode) =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Failed to decode component group. Expected a mapping, got a sequence"
|
||||
)
|
||||
)
|
||||
case _ =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Failed to decode component group"
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Left(
|
||||
new YAMLException("Failed to decode component group")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlEncoder: YamlEncoder[ComponentGroup] =
|
||||
new YamlEncoder[ComponentGroup] {
|
||||
override def encode(value: ComponentGroup) = {
|
||||
val fields = new util.ArrayList[(String, Object)](3)
|
||||
val seqEncoder = implicitly[YamlEncoder[Seq[Component]]]
|
||||
value.color.foreach(v => fields.add((Fields.Color, v)))
|
||||
value.icon.foreach(v => fields.add((Fields.Icon, v)))
|
||||
if (value.exports.nonEmpty) {
|
||||
val exportsNode = seqEncoder.encode(value.exports)
|
||||
fields.add((Fields.Exports, exportsNode))
|
||||
}
|
||||
val componentElementsGroupNode = toMap(fields)
|
||||
toMap(value.group.name, componentElementsGroupNode)
|
||||
}
|
||||
}
|
||||
|
||||
/** [[Encoder]] instance for the [[ComponentGroup]]. */
|
||||
implicit val encoder: Encoder[ComponentGroup] = { componentGroup =>
|
||||
val color = componentGroup.color.map(Fields.Color -> _.asJson)
|
||||
@ -131,9 +261,94 @@ object ExtendedComponentGroup {
|
||||
|
||||
/** Fields for use when serializing the [[ExtendedComponentGroup]]. */
|
||||
private object Fields {
|
||||
val Group = "group"
|
||||
val Exports = "exports"
|
||||
}
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[ExtendedComponentGroup] =
|
||||
new YamlDecoder[ExtendedComponentGroup] {
|
||||
override def decode(
|
||||
node: Node
|
||||
): Either[Throwable, ExtendedComponentGroup] = node match {
|
||||
case mappingNode: MappingNode =>
|
||||
if (mappingNode.getValue.size() == 1) {
|
||||
val groupDecoder = implicitly[YamlDecoder[GroupReference]]
|
||||
val exportsDecoder = implicitly[YamlDecoder[Seq[Component]]]
|
||||
val groupNode = mappingNode.getValue.get(0)
|
||||
(groupNode.getKeyNode, groupNode.getValueNode) match {
|
||||
case (scalarNode: ScalarNode, seqNode: SequenceNode) =>
|
||||
for {
|
||||
group <- groupDecoder.decode(scalarNode)
|
||||
exports <- exportsDecoder.decode(seqNode)
|
||||
} yield ExtendedComponentGroup(group, exports)
|
||||
case (groupNode: ScalarNode, componentExportsNode: MappingNode) =>
|
||||
val values = componentExportsNode.getValue
|
||||
val valuesCount = values.size()
|
||||
if (valuesCount == 0) {
|
||||
groupDecoder
|
||||
.decode(groupNode)
|
||||
.map(ExtendedComponentGroup(_, Seq.empty))
|
||||
} else if (valuesCount == 1) {
|
||||
val exportsNode = values.get(0)
|
||||
(exportsNode.getKeyNode, exportsNode.getValueNode) match {
|
||||
case (exportsKeyNode: ScalarNode, seqNode: SequenceNode)
|
||||
if exportsKeyNode.getValue == Fields.Exports =>
|
||||
for {
|
||||
group <- groupDecoder.decode(groupNode)
|
||||
exports <- exportsDecoder.decode(seqNode)
|
||||
} yield ExtendedComponentGroup(group, exports)
|
||||
case _ =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Failed to decode Extended ComponentGroup"
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Failed to decode Extended Component Group"
|
||||
)
|
||||
)
|
||||
}
|
||||
case _ =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Failed to decode Component Group's name in " + groupNode
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Left(
|
||||
new YAMLException("Failed to decode Component Group's name")
|
||||
)
|
||||
}
|
||||
case scalarNode: ScalarNode =>
|
||||
val groupDecoder = implicitly[YamlDecoder[GroupReference]]
|
||||
groupDecoder
|
||||
.decode(scalarNode)
|
||||
.map(ExtendedComponentGroup(_, Seq.empty))
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlEncoder: YamlEncoder[ExtendedComponentGroup] =
|
||||
new YamlEncoder[ExtendedComponentGroup] {
|
||||
override def encode(value: ExtendedComponentGroup): Object = {
|
||||
val groupReferenceEncoder = implicitly[YamlEncoder[GroupReference]]
|
||||
val componentsEncoder = implicitly[YamlEncoder[Seq[Component]]]
|
||||
|
||||
val groupReferenceNode =
|
||||
groupReferenceEncoder.encode(value.group).asInstanceOf[String]
|
||||
if (value.exports.nonEmpty)
|
||||
toMap(
|
||||
groupReferenceNode,
|
||||
toMap("exports", componentsEncoder.encode(value.exports))
|
||||
)
|
||||
else
|
||||
groupReferenceNode
|
||||
}
|
||||
}
|
||||
|
||||
/** [[Encoder]] instance for the [[ExtendedComponentGroup]]. */
|
||||
implicit val encoder: Encoder[ExtendedComponentGroup] = {
|
||||
extendedComponentGroup =>
|
||||
@ -200,9 +415,63 @@ case class Component(name: String, shortcut: Option[Shortcut])
|
||||
object Component {
|
||||
|
||||
object Fields {
|
||||
val Name = "name"
|
||||
val Shortcut = "shortcut"
|
||||
}
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[Component] =
|
||||
new YamlDecoder[Component] {
|
||||
override def decode(node: Node): Either[Throwable, Component] =
|
||||
node match {
|
||||
case mappingNode: MappingNode =>
|
||||
if (mappingNode.getValue.size() == 1) {
|
||||
val componentNode = mappingNode.getValue.get(0)
|
||||
(componentNode.getKeyNode, componentNode.getValueNode) match {
|
||||
case (scalarNode: ScalarNode, mappingNode: MappingNode) =>
|
||||
val stringDecoder = implicitly[YamlDecoder[String]]
|
||||
val shortcutDecoder =
|
||||
implicitly[YamlDecoder[Option[Shortcut]]]
|
||||
for {
|
||||
name <- stringDecoder.decode(scalarNode)
|
||||
shortcut <- shortcutDecoder
|
||||
.decode(mappingNode)
|
||||
.map(_.filter(_.key.nonEmpty))
|
||||
} yield Component(name, shortcut)
|
||||
case (keyNode: ScalarNode, _: ScalarNode) =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Failed to decode exported component '" + keyNode.getValue + "'"
|
||||
)
|
||||
)
|
||||
case _ =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Failed to decode Component"
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Left(new YAMLException("Failed to decode Component"))
|
||||
}
|
||||
case scalarNode: ScalarNode =>
|
||||
val stringDecoder = implicitly[YamlDecoder[String]]
|
||||
stringDecoder.decode(scalarNode).map(Component(_, None))
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlEncoder: YamlEncoder[Component] =
|
||||
new YamlEncoder[Component] {
|
||||
override def encode(value: Component) = {
|
||||
if (value.shortcut.isEmpty) {
|
||||
value.name
|
||||
} else {
|
||||
val shortcutEncoder = implicitly[YamlEncoder[Shortcut]]
|
||||
val shortcutNode = value.shortcut.map(shortcutEncoder.encode(_)).get
|
||||
toMap(value.name, shortcutNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** [[Encoder]] instance for the [[Component]]. */
|
||||
implicit val encoder: Encoder[Component] = { component =>
|
||||
component.shortcut match {
|
||||
@ -256,6 +525,44 @@ object Component {
|
||||
case class Shortcut(key: String)
|
||||
object Shortcut {
|
||||
|
||||
object Fields {
|
||||
val Key = "shortcut"
|
||||
}
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[Shortcut] =
|
||||
new YamlDecoder[Shortcut] {
|
||||
override def decode(node: Node): Either[Throwable, Shortcut] =
|
||||
node match {
|
||||
case mappingNode: MappingNode =>
|
||||
val stringDecoder = implicitly[YamlDecoder[String]]
|
||||
val shortcutNode = mappingNode.getValue.get(0)
|
||||
(shortcutNode.getKeyNode, shortcutNode.getValueNode) match {
|
||||
case (key: ScalarNode, valueNode) if key.getValue == Fields.Key =>
|
||||
valueNode match {
|
||||
case valueNode: ScalarNode =>
|
||||
stringDecoder.decode(valueNode).map(Shortcut(_))
|
||||
case _: SequenceNode =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Failed to decode shortcut. Expected a string value, got a sequence"
|
||||
)
|
||||
)
|
||||
case _ =>
|
||||
Left(new YAMLException("Failed to decode Shortcut"))
|
||||
}
|
||||
case _ =>
|
||||
Left(new YAMLException("Failed to decode Shortcut"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlEncoder: YamlEncoder[Shortcut] =
|
||||
new YamlEncoder[Shortcut] {
|
||||
override def encode(value: Shortcut) = {
|
||||
toMap(Fields.Key, value.key)
|
||||
}
|
||||
}
|
||||
|
||||
/** [[Encoder]] instance for the [[Shortcut]]. */
|
||||
implicit val encoder: Encoder[Shortcut] = { shortcut =>
|
||||
shortcut.key.asJson
|
||||
@ -307,6 +614,31 @@ object GroupReference {
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
|
||||
object Fields {
|
||||
val LibraryName = "library-name"
|
||||
val GroupName = "group-name"
|
||||
}
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[GroupReference] =
|
||||
new YamlDecoder[GroupReference] {
|
||||
override def decode(node: Node): Either[Throwable, GroupReference] =
|
||||
node match {
|
||||
case scalarNode: ScalarNode =>
|
||||
fromModuleName(scalarNode.getValue).toRight(
|
||||
new YAMLException(
|
||||
s"Failed to decode '${scalarNode.getValue}' as a module reference"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlEncoder: YamlEncoder[GroupReference] =
|
||||
new YamlEncoder[GroupReference] {
|
||||
override def encode(value: GroupReference) = {
|
||||
value.libraryName.qualifiedName + LibraryName.separator + value.groupName.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The module name.
|
||||
@ -316,6 +648,20 @@ object GroupReference {
|
||||
case class GroupName(name: String)
|
||||
object GroupName {
|
||||
|
||||
object Fields {
|
||||
val Name = "name"
|
||||
}
|
||||
|
||||
implicit val yamlDecoder: YamlDecoder[GroupName] =
|
||||
new YamlDecoder[GroupName] {
|
||||
override def decode(node: Node): Either[Throwable, GroupName] =
|
||||
node match {
|
||||
case scalarNode: ScalarNode =>
|
||||
val stringDecoder = implicitly[YamlDecoder[String]]
|
||||
stringDecoder.decode(scalarNode).map(GroupName(_))
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a [[GroupName]] from its components. */
|
||||
def fromComponents(item: String, items: List[String]): GroupName =
|
||||
GroupName((item :: items).mkString(LibraryName.separator.toString))
|
||||
|
@ -1,20 +1,16 @@
|
||||
package org.enso.pkg
|
||||
|
||||
import io.circe._
|
||||
import io.circe.syntax._
|
||||
import io.circe.yaml.{Parser, Printer}
|
||||
import org.yaml.snakeyaml.nodes.Tag
|
||||
import org.enso.semver.SemVer
|
||||
import org.enso.editions.EditionSerialization._
|
||||
import org.enso.editions.{
|
||||
DefaultEnsoVersion,
|
||||
EditionName,
|
||||
Editions,
|
||||
EnsoVersion,
|
||||
SemVerEnsoVersion
|
||||
}
|
||||
import org.enso.editions.{EditionName, Editions}
|
||||
import org.enso.pkg.validation.NameValidation
|
||||
import org.enso.yaml.{YamlDecoder, YamlEncoder}
|
||||
import org.yaml.snakeyaml.{DumperOptions, Yaml}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
import org.yaml.snakeyaml.nodes.{MappingNode, Node}
|
||||
|
||||
import java.io.Reader
|
||||
import java.io.{Reader, StringReader}
|
||||
import java.util
|
||||
import scala.util.Try
|
||||
|
||||
/** Contact information to a user.
|
||||
@ -46,35 +42,38 @@ object Contact {
|
||||
val Email = "email"
|
||||
}
|
||||
|
||||
/** [[Encoder]] instance for the [[Contact]]. */
|
||||
implicit val encoder: Encoder[Contact] = { contact =>
|
||||
val name = contact.name.map(Fields.Name -> _.asJson)
|
||||
val email = contact.email.map(Fields.Email -> _.asJson)
|
||||
Json.obj((name.toSeq ++ email.toSeq): _*)
|
||||
}
|
||||
implicit val decoderSnake: YamlDecoder[Contact] =
|
||||
new YamlDecoder[Contact] {
|
||||
override def decode(node: Node): Either[Throwable, Contact] = node match {
|
||||
case mappingNode: MappingNode =>
|
||||
val optString = implicitly[YamlDecoder[Option[String]]]
|
||||
val bindings = mappingKV(mappingNode)
|
||||
for {
|
||||
name <- bindings
|
||||
.get(Fields.Name)
|
||||
.map(optString.decode)
|
||||
.getOrElse(Right(None))
|
||||
email <- bindings
|
||||
.get(Fields.Email)
|
||||
.map(optString.decode)
|
||||
.getOrElse(Right(None))
|
||||
} yield Contact(name, email)
|
||||
}
|
||||
}
|
||||
|
||||
/** [[Decoder]] instance for the [[Contact]].
|
||||
*/
|
||||
implicit val decoder: Decoder[Contact] = { json =>
|
||||
def verifyAtLeastOneDefined(
|
||||
name: Option[String],
|
||||
email: Option[String]
|
||||
): Either[DecodingFailure, Unit] =
|
||||
if (name.isEmpty && email.isEmpty)
|
||||
Left(
|
||||
DecodingFailure(
|
||||
"At least one of the fields `name`, `email` must be defined.",
|
||||
json.history
|
||||
)
|
||||
)
|
||||
else Right(())
|
||||
|
||||
for {
|
||||
name <- json.getOrElse[Option[String]](Fields.Name)(None)
|
||||
email <- json.getOrElse[Option[String]](Fields.Email)(None)
|
||||
_ <- verifyAtLeastOneDefined(name, email)
|
||||
} yield Contact(name, email)
|
||||
}
|
||||
implicit val encoderSnake: YamlEncoder[Contact] =
|
||||
new YamlEncoder[Contact] {
|
||||
override def encode(value: Contact) = {
|
||||
val elements = new util.ArrayList[(String, Object)]()
|
||||
value.name
|
||||
.map((Fields.Name, _))
|
||||
.foreach(elements.add)
|
||||
value.email
|
||||
.map((Fields.Email, _))
|
||||
.foreach(elements.add)
|
||||
toMap(elements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents a package configuration stored in the `package.yaml` file.
|
||||
@ -114,8 +113,14 @@ case class Config(
|
||||
) {
|
||||
|
||||
/** Converts the configuration into a YAML representation. */
|
||||
def toYaml: String =
|
||||
Printer.spaces2.copy(preserveOrder = true).pretty(Config.encoder(this))
|
||||
def toYaml: String = {
|
||||
val node = implicitly[YamlEncoder[Config]].encode(this)
|
||||
val dumperOptions = new DumperOptions()
|
||||
dumperOptions.setIndent(2)
|
||||
dumperOptions.setPrettyFlow(true)
|
||||
val yaml = new Yaml(dumperOptions)
|
||||
yaml.dumpAs(node, Tag.MAP, DumperOptions.FlowStyle.BLOCK)
|
||||
}
|
||||
|
||||
/** @return the module of name. */
|
||||
def moduleName: String =
|
||||
@ -125,121 +130,182 @@ case class Config(
|
||||
|
||||
object Config {
|
||||
|
||||
val defaultNamespace: String = "local"
|
||||
val DefaultNamespace: String = "local"
|
||||
val DefaultVersion: String = "dev"
|
||||
val DefaultLicense: String = ""
|
||||
val DefaultPreferLocalLibraries = false
|
||||
|
||||
private object JsonFields {
|
||||
val name: String = "name"
|
||||
val normalizedName: String = "normalized-name"
|
||||
val version: String = "version"
|
||||
val ensoVersion: String = "enso-version"
|
||||
val license: String = "license"
|
||||
val author: String = "authors"
|
||||
val namespace: String = "namespace"
|
||||
val maintainer: String = "maintainers"
|
||||
val edition: String = "edition"
|
||||
val preferLocalLibraries = "prefer-local-libraries"
|
||||
val componentGroups = "component-groups"
|
||||
val Name: String = "name"
|
||||
val NormalizedName: String = "normalized-name"
|
||||
val Version: String = "version"
|
||||
val EnsoVersion: String = "enso-version"
|
||||
val License: String = "license"
|
||||
val Author: String = "authors"
|
||||
val Namespace: String = "namespace"
|
||||
val Maintainer: String = "maintainers"
|
||||
val Edition: String = "edition"
|
||||
val PreferLocalLibraries = "prefer-local-libraries"
|
||||
val ComponentGroups = "component-groups"
|
||||
}
|
||||
|
||||
implicit val decoder: Decoder[Config] = { json =>
|
||||
for {
|
||||
name <- json.get[String](JsonFields.name)
|
||||
normalizedName <- json.get[Option[String]](JsonFields.normalizedName)
|
||||
namespace <- json.getOrElse[String](JsonFields.namespace)(
|
||||
defaultNamespace
|
||||
)
|
||||
version <- json.getOrElse[String](JsonFields.version)("dev")
|
||||
ensoVersion <- json.get[Option[EnsoVersion]](JsonFields.ensoVersion)
|
||||
rawEdition <- json
|
||||
.get[EditionName](JsonFields.edition)
|
||||
.map(x => Left(x.name))
|
||||
.orElse(
|
||||
json
|
||||
.get[Option[Editions.RawEdition]](JsonFields.edition)
|
||||
.map(Right(_))
|
||||
)
|
||||
edition = rawEdition.fold(
|
||||
editionName => Some(Editions.Raw.Edition(parent = Some(editionName))),
|
||||
identity
|
||||
)
|
||||
license <- json.getOrElse(JsonFields.license)("")
|
||||
author <- json.getOrElse[List[Contact]](JsonFields.author)(List())
|
||||
maintainer <- json.getOrElse[List[Contact]](JsonFields.maintainer)(List())
|
||||
preferLocal <-
|
||||
json.getOrElse[Boolean](JsonFields.preferLocalLibraries)(false)
|
||||
finalEdition <-
|
||||
editionOrVersionBackwardsCompatibility(edition, ensoVersion).left.map {
|
||||
error => DecodingFailure(error, json.history)
|
||||
}
|
||||
componentGroups <- json.getOrElse[Option[ComponentGroups]](
|
||||
JsonFields.componentGroups
|
||||
)(None)
|
||||
} yield {
|
||||
|
||||
Config(
|
||||
name = name,
|
||||
normalizedName = normalizedName,
|
||||
namespace = namespace,
|
||||
version = version,
|
||||
license = license,
|
||||
authors = author,
|
||||
maintainers = maintainer,
|
||||
edition = finalEdition,
|
||||
preferLocalLibraries = preferLocal,
|
||||
componentGroups = componentGroups
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val encoder: Encoder[Config] = { config =>
|
||||
val edition = config.edition
|
||||
.map { edition =>
|
||||
if (edition.isDerivingWithoutOverrides) edition.parent.get.asJson
|
||||
else edition.asJson
|
||||
implicit val yamlDecoder: YamlDecoder[Config] =
|
||||
new YamlDecoder[Config] {
|
||||
override def decode(node: Node): Either[Throwable, Config] = node match {
|
||||
case mappingNode: MappingNode =>
|
||||
val clazzMap = mappingKV(mappingNode)
|
||||
val stringDecoder = implicitly[YamlDecoder[String]]
|
||||
val normalizedNameDecoder =
|
||||
implicitly[YamlDecoder[Option[String]]]
|
||||
val contactDecoder = implicitly[YamlDecoder[List[Contact]]]
|
||||
val editionNameDecoder = implicitly[YamlDecoder[EditionName]]
|
||||
val editionDecoder =
|
||||
implicitly[YamlDecoder[Option[Editions.RawEdition]]]
|
||||
val booleanDecoder = implicitly[YamlDecoder[Boolean]]
|
||||
val componentGroups =
|
||||
implicitly[YamlDecoder[Option[ComponentGroups]]]
|
||||
for {
|
||||
name <- clazzMap
|
||||
.get(JsonFields.Name)
|
||||
.toRight(
|
||||
new YAMLException(s"Missing '${JsonFields.Name}' field")
|
||||
)
|
||||
.flatMap(stringDecoder.decode)
|
||||
normalizedName <- clazzMap
|
||||
.get(JsonFields.NormalizedName)
|
||||
.map(normalizedNameDecoder.decode)
|
||||
.getOrElse(Right(None))
|
||||
namespace <- clazzMap
|
||||
.get(JsonFields.Namespace)
|
||||
.map(stringDecoder.decode)
|
||||
.getOrElse(Right(DefaultNamespace))
|
||||
version <- clazzMap
|
||||
.get(JsonFields.Version)
|
||||
.map(stringDecoder.decode)
|
||||
.getOrElse(Right(DefaultVersion))
|
||||
license <- clazzMap
|
||||
.get(JsonFields.License)
|
||||
.map(stringDecoder.decode)
|
||||
.getOrElse(Right(DefaultLicense))
|
||||
authors <- clazzMap
|
||||
.get(JsonFields.Author)
|
||||
.map(contactDecoder.decode)
|
||||
.getOrElse(Right(Nil))
|
||||
maintainers <- clazzMap
|
||||
.get(JsonFields.Maintainer)
|
||||
.map(contactDecoder.decode)
|
||||
.getOrElse(Right(Nil))
|
||||
rawEdition = clazzMap
|
||||
.get(JsonFields.Edition)
|
||||
.flatMap(x => editionNameDecoder.decode(x).toOption.map(Left(_)))
|
||||
.getOrElse(
|
||||
clazzMap
|
||||
.get(JsonFields.Edition)
|
||||
.map(editionDecoder.decode)
|
||||
.getOrElse(Right(None))
|
||||
)
|
||||
.asInstanceOf[Either[EditionName, Option[Editions.RawEdition]]]
|
||||
edition <- rawEdition.fold(
|
||||
editionName =>
|
||||
Right(
|
||||
Some(Editions.Raw.Edition(parent = Some(editionName.name)))
|
||||
),
|
||||
r => Right(r)
|
||||
)
|
||||
preferLocalLibraries <- clazzMap
|
||||
.get(JsonFields.PreferLocalLibraries)
|
||||
.map(booleanDecoder.decode)
|
||||
.getOrElse(Right(DefaultPreferLocalLibraries))
|
||||
componentGroups <- clazzMap
|
||||
.get(JsonFields.ComponentGroups)
|
||||
.map(componentGroups.decode)
|
||||
.getOrElse(Right(None))
|
||||
} yield Config(
|
||||
name,
|
||||
normalizedName,
|
||||
namespace,
|
||||
version,
|
||||
license,
|
||||
authors,
|
||||
maintainers,
|
||||
edition,
|
||||
preferLocalLibraries,
|
||||
componentGroups
|
||||
)
|
||||
}
|
||||
.map(JsonFields.edition -> _)
|
||||
}
|
||||
|
||||
val componentGroups =
|
||||
Option.unless(
|
||||
config.componentGroups.isEmpty
|
||||
)(
|
||||
JsonFields.componentGroups -> config.componentGroups.asJson
|
||||
)
|
||||
implicit val encoderSnake: YamlEncoder[Config] =
|
||||
new YamlEncoder[Config] {
|
||||
override def encode(value: Config) = {
|
||||
val contactsEncoder = implicitly[YamlEncoder[List[Contact]]]
|
||||
val editionEncoder = implicitly[YamlEncoder[Editions.RawEdition]]
|
||||
val booleanEncoder = implicitly[YamlEncoder[Boolean]]
|
||||
val componentGroupsEncoder =
|
||||
implicitly[YamlEncoder[ComponentGroups]]
|
||||
|
||||
val normalizedName = config.normalizedName.map(value =>
|
||||
JsonFields.normalizedName -> value.asJson
|
||||
)
|
||||
val elements = new util.ArrayList[(String, Object)]()
|
||||
elements.add((JsonFields.Name, value.name))
|
||||
value.normalizedName.foreach(v =>
|
||||
elements.add((JsonFields.NormalizedName, v))
|
||||
)
|
||||
if (value.namespace != DefaultNamespace)
|
||||
elements.add((JsonFields.Namespace, value.namespace))
|
||||
if (value.version != DefaultVersion)
|
||||
elements.add(
|
||||
(JsonFields.Version, value.version)
|
||||
)
|
||||
if (value.license != DefaultLicense)
|
||||
elements.add(
|
||||
(JsonFields.License, value.license)
|
||||
)
|
||||
if (value.authors.nonEmpty) {
|
||||
elements.add(
|
||||
(JsonFields.Author, contactsEncoder.encode(value.authors))
|
||||
)
|
||||
}
|
||||
if (value.maintainers.nonEmpty) {
|
||||
elements.add(
|
||||
(JsonFields.Maintainer, contactsEncoder.encode(value.maintainers))
|
||||
)
|
||||
}
|
||||
|
||||
val overrides =
|
||||
Seq(JsonFields.name -> config.name.asJson) ++
|
||||
normalizedName.toSeq ++
|
||||
Seq(
|
||||
JsonFields.namespace -> config.namespace.asJson,
|
||||
JsonFields.version -> config.version.asJson,
|
||||
JsonFields.license -> config.license.asJson,
|
||||
JsonFields.author -> config.authors.asJson,
|
||||
JsonFields.maintainer -> config.maintainers.asJson
|
||||
) ++ edition.toSeq ++ componentGroups.toSeq
|
||||
value.edition.foreach { edition =>
|
||||
if (edition.isDerivingWithoutOverrides)
|
||||
elements.add((JsonFields.Edition, edition.parent.get))
|
||||
else
|
||||
elements.add((JsonFields.Edition, editionEncoder.encode(edition)))
|
||||
}
|
||||
if (value.preferLocalLibraries != DefaultPreferLocalLibraries)
|
||||
elements.add(
|
||||
(
|
||||
JsonFields.PreferLocalLibraries,
|
||||
booleanEncoder.encode(value.preferLocalLibraries)
|
||||
)
|
||||
)
|
||||
value.componentGroups.foreach(v =>
|
||||
elements.add(
|
||||
(JsonFields.ComponentGroups, componentGroupsEncoder.encode(v))
|
||||
)
|
||||
)
|
||||
|
||||
val preferLocalOverride =
|
||||
if (config.preferLocalLibraries)
|
||||
Seq(JsonFields.preferLocalLibraries -> true.asJson)
|
||||
else Seq()
|
||||
val overridesObject = JsonObject(
|
||||
overrides ++ preferLocalOverride: _*
|
||||
)
|
||||
|
||||
overridesObject.asJson
|
||||
}
|
||||
|
||||
/** Tries to parse the [[Config]] from a YAML string. */
|
||||
def fromYaml(yamlString: String): Try[Config] = {
|
||||
yaml.parser.parse(yamlString).flatMap(_.as[Config]).toTry
|
||||
}
|
||||
toMap(elements)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tries to parse the [[Config]] directly from the Reader */
|
||||
def fromYaml(reader: Reader): Try[Config] = {
|
||||
Parser.default.parse(reader).flatMap(_.as[Config]).toTry
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
Try(snakeYaml.compose(reader)).toEither
|
||||
.flatMap(implicitly[YamlDecoder[Config]].decode(_))
|
||||
.toTry
|
||||
}
|
||||
|
||||
def fromYaml(yamlString: String): Try[Config] = {
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
Try(snakeYaml.compose(new StringReader(yamlString))).toEither
|
||||
.flatMap(implicitly[YamlDecoder[Config]].decode(_))
|
||||
.toTry
|
||||
}
|
||||
|
||||
/** Creates a simple edition that just defines the provided engine version.
|
||||
@ -262,37 +328,4 @@ object Config {
|
||||
repositories = Map(),
|
||||
libraries = Map()
|
||||
)
|
||||
|
||||
/** A helper method that reconciles the old and new fields of the config
|
||||
* related to the edition.
|
||||
*
|
||||
* If an edition is present, it is just returned as-is. If the engine version
|
||||
* is specified, a special edition is created that specifies this particular
|
||||
* engine version and nothing else.
|
||||
*
|
||||
* If both fields are defined, an error is raised as the configuration may be
|
||||
* inconsistent - the `engine-version` field should only be present in old
|
||||
* configs and after migration to the edition format it should be removed.
|
||||
*/
|
||||
private def editionOrVersionBackwardsCompatibility(
|
||||
edition: Option[Editions.RawEdition],
|
||||
ensoVersion: Option[EnsoVersion]
|
||||
): Either[String, Option[Editions.RawEdition]] =
|
||||
(edition, ensoVersion) match {
|
||||
case (Some(_), Some(_)) =>
|
||||
Left(
|
||||
s"The deprecated `${JsonFields.ensoVersion}` should not be defined " +
|
||||
s"if the `${JsonFields.edition}` that replaces it is already defined."
|
||||
)
|
||||
case (Some(edition), _) =>
|
||||
Right(Some(edition))
|
||||
case (_, Some(SemVerEnsoVersion(version))) =>
|
||||
Right(Some(makeCompatibilityEditionFromVersion(version)))
|
||||
case (_, Some(DefaultEnsoVersion)) =>
|
||||
// If the `default` version is specified, we return None, so that later
|
||||
// on, it will fallback to the default edition.
|
||||
Right(None)
|
||||
case (None, None) =>
|
||||
Right(None)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.enso.pkg
|
||||
|
||||
import cats.Show
|
||||
import org.enso.editions.{Editions, LibraryName}
|
||||
import org.enso.filesystem.FileSystem
|
||||
import org.enso.pkg.validation.NameValidation
|
||||
@ -350,15 +349,6 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
|
||||
result.recoverWith {
|
||||
case packageLoadingException: PackageManager.PackageLoadingException =>
|
||||
Failure(packageLoadingException)
|
||||
case decodingError: io.circe.Error =>
|
||||
val errorMessage =
|
||||
implicitly[Show[io.circe.Error]].show(decodingError)
|
||||
Failure(
|
||||
PackageManager.PackageLoadingFailure(
|
||||
s"Cannot decode the package config: $errorMessage",
|
||||
decodingError
|
||||
)
|
||||
)
|
||||
case otherError =>
|
||||
Failure(
|
||||
PackageManager.PackageLoadingFailure(
|
||||
|
@ -1,12 +1,11 @@
|
||||
package org.enso.pkg
|
||||
|
||||
import cats.Show
|
||||
import io.circe.{DecodingFailure, Json}
|
||||
import org.enso.semver.SemVer
|
||||
import org.enso.editions.LibraryName
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import org.scalatest.{Inside, OptionValues}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
|
||||
import scala.util.Failure
|
||||
|
||||
@ -17,22 +16,6 @@ class ConfigSpec
|
||||
with OptionValues {
|
||||
|
||||
"Config" should {
|
||||
"preserve unknown keys when deserialized and serialized again" ignore {
|
||||
val original = Json.obj(
|
||||
"name" -> Json.fromString("name"),
|
||||
"unknown-key" -> Json.fromString("value")
|
||||
)
|
||||
|
||||
inside(original.as[Config]) { case Right(config) =>
|
||||
val serialized = Config.encoder(config)
|
||||
serialized.asObject
|
||||
.value("unknown-key")
|
||||
.value
|
||||
.asString
|
||||
.value shouldEqual "value"
|
||||
}
|
||||
}
|
||||
|
||||
"deserialize the serialized representation to the original value" in {
|
||||
val config = Config(
|
||||
name = "placeholder",
|
||||
@ -194,8 +177,8 @@ class ConfigSpec
|
||||
val parsed = Config.fromYaml(config)
|
||||
|
||||
parsed match {
|
||||
case Failure(f: DecodingFailure) =>
|
||||
Show[DecodingFailure].show(f) should include(
|
||||
case Failure(failure: YAMLException) =>
|
||||
failure.getMessage should include(
|
||||
"Failed to decode 'Group 1' as a module reference"
|
||||
)
|
||||
case unexpected =>
|
||||
@ -254,9 +237,9 @@ class ConfigSpec
|
||||
|""".stripMargin
|
||||
val parsed = Config.fromYaml(config)
|
||||
parsed match {
|
||||
case Failure(f: DecodingFailure) =>
|
||||
Show[DecodingFailure].show(f) should include(
|
||||
"Failed to decode shortcut"
|
||||
case Failure(failure: YAMLException) =>
|
||||
failure.getMessage should equal(
|
||||
"Failed to decode shortcut. Expected a string value, got a sequence"
|
||||
)
|
||||
case unexpected =>
|
||||
fail(s"Unexpected result: $unexpected")
|
||||
@ -273,9 +256,9 @@ class ConfigSpec
|
||||
|""".stripMargin
|
||||
val parsed = Config.fromYaml(config)
|
||||
parsed match {
|
||||
case Failure(f: DecodingFailure) =>
|
||||
Show[DecodingFailure].show(f) should include(
|
||||
"Failed to decode component group"
|
||||
case Failure(failure: YAMLException) =>
|
||||
failure.getMessage should equal(
|
||||
"Failed to decode component group. Expected a mapping, got a sequence"
|
||||
)
|
||||
case unexpected =>
|
||||
fail(s"Unexpected result: $unexpected")
|
||||
@ -293,9 +276,9 @@ class ConfigSpec
|
||||
|""".stripMargin
|
||||
val parsed = Config.fromYaml(config)
|
||||
parsed match {
|
||||
case Failure(f: DecodingFailure) =>
|
||||
Show[DecodingFailure].show(f) should include(
|
||||
"Failed to decode exported component"
|
||||
case Failure(failure: YAMLException) =>
|
||||
failure.getMessage should equal(
|
||||
"Failed to decode exported component 'one'"
|
||||
)
|
||||
case unexpected =>
|
||||
fail(s"Unexpected result: $unexpected")
|
||||
|
@ -108,7 +108,7 @@ class ProjectService[
|
||||
id = projectId,
|
||||
name = name,
|
||||
module = moduleName,
|
||||
namespace = Config.defaultNamespace,
|
||||
namespace = Config.DefaultNamespace,
|
||||
kind = UserProject,
|
||||
created = creationTime,
|
||||
edition = None,
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.enso.projectmanager.service.config
|
||||
|
||||
import io.circe.Json
|
||||
import org.enso.semver.SemVer
|
||||
import org.enso.editions.{DefaultEnsoVersion, EnsoVersion, SemVerEnsoVersion}
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
@ -36,7 +35,7 @@ class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
|
||||
key: String,
|
||||
value: String
|
||||
): F[GlobalConfigServiceFailure, Unit] = Sync[F].blockingOp {
|
||||
configurationManager.updateConfigRaw(key, Json.fromString(value))
|
||||
configurationManager.updateConfigRaw(key, value)
|
||||
}.recoverAccessErrors
|
||||
|
||||
/** @inheritdoc */
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.enso.runtimeversionmanager.config
|
||||
|
||||
import io.circe.Json
|
||||
import org.enso.semver.SemVer
|
||||
import org.enso.distribution.DistributionManager
|
||||
import org.enso.distribution.config.InvalidConfigError
|
||||
@ -27,16 +26,16 @@ class GlobalConfigurationManagerSpec
|
||||
"GlobalConfigurationManager" should {
|
||||
"allow to edit and remove known keys" in {
|
||||
val configurationManager = makeConfigManager()
|
||||
val value = Json.fromInt(42)
|
||||
val value = 42.toString
|
||||
configurationManager.updateConfigRaw("unknown-key", value)
|
||||
configurationManager.getConfig
|
||||
.findByKey("unknown-key") should not be defined
|
||||
val newEmail = Json.fromString("foo@bar.com")
|
||||
val newEmail = "foo@bar.com"
|
||||
configurationManager.getConfig
|
||||
.findByKey("author.email") should not be defined
|
||||
configurationManager.updateConfigRaw("author.email", newEmail)
|
||||
configurationManager.getConfig
|
||||
.findByKey("author.email") shouldEqual newEmail.asString
|
||||
.findByKey("author.email") shouldEqual Some(newEmail)
|
||||
}
|
||||
|
||||
"not allow saving an invalid config" in {
|
||||
@ -44,7 +43,7 @@ class GlobalConfigurationManagerSpec
|
||||
intercept[InvalidConfigError] {
|
||||
configurationManager.updateConfigRaw(
|
||||
"default.enso-version",
|
||||
Json.fromString("invalid-version")
|
||||
"invalid-version"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,19 @@
|
||||
package org.enso.runtimeversionmanager.components
|
||||
|
||||
import java.io.FileReader
|
||||
import java.io.{FileReader, StringReader}
|
||||
import java.nio.file.Path
|
||||
import cats.Show
|
||||
import io.circe.yaml.Parser
|
||||
import io.circe.{yaml, Decoder}
|
||||
import org.enso
|
||||
import org.enso.semver.SemVer
|
||||
import org.enso.cli.OS
|
||||
import org.enso.semver.SemVerJson._
|
||||
import org.enso.semver.SemVerYaml._
|
||||
import org.enso.runtimeversionmanager.components.Manifest.{
|
||||
JVMOption,
|
||||
RequiredInstallerVersions
|
||||
}
|
||||
import org.enso.runtimeversionmanager.components
|
||||
import org.enso.yaml.{ParseError, YamlDecoder}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
import org.yaml.snakeyaml.nodes.{MappingNode, Node}
|
||||
|
||||
import scala.util.{Failure, Try, Using}
|
||||
|
||||
@ -70,6 +71,39 @@ object Manifest {
|
||||
*/
|
||||
case class RequiredInstallerVersions(launcher: SemVer, projectManager: SemVer)
|
||||
|
||||
object RequiredInstallerVersions {
|
||||
implicit val yamlDecoder: YamlDecoder[RequiredInstallerVersions] =
|
||||
new YamlDecoder[RequiredInstallerVersions] {
|
||||
override def decode(
|
||||
node: Node
|
||||
): Either[Throwable, RequiredInstallerVersions] = {
|
||||
node match {
|
||||
case mappingNode: MappingNode =>
|
||||
val semverDecoder = implicitly[YamlDecoder[SemVer]]
|
||||
val bindings = mappingKV(mappingNode)
|
||||
for {
|
||||
launcher <- bindings
|
||||
.get(Manifest.Fields.MinimumLauncherVersion)
|
||||
.toRight(
|
||||
new YAMLException(
|
||||
s"Missing `${Manifest.Fields.MinimumLauncherVersion}` field"
|
||||
)
|
||||
)
|
||||
.flatMap(semverDecoder.decode)
|
||||
projectManager <- bindings
|
||||
.get(Manifest.Fields.MinimumProjectManagerVersion)
|
||||
.toRight(
|
||||
new YAMLException(
|
||||
s"Missing `${Manifest.Fields.MinimumProjectManagerVersion}` field"
|
||||
)
|
||||
)
|
||||
.flatMap(semverDecoder.decode)
|
||||
} yield RequiredInstallerVersions(launcher, projectManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Defines the name under which the manifest is included in the releases.
|
||||
*/
|
||||
val DEFAULT_MANIFEST_NAME = "manifest.yaml"
|
||||
@ -109,24 +143,31 @@ object Manifest {
|
||||
|
||||
object JVMOption {
|
||||
private object Fields {
|
||||
val os = "os"
|
||||
val value = "value"
|
||||
val Os = "os"
|
||||
val Value = "value"
|
||||
}
|
||||
|
||||
/** [[Decoder]] instance that allows to parse the [[JVMOption]] from the
|
||||
* YAML manifest.
|
||||
*/
|
||||
implicit val decoder: Decoder[JVMOption] = { json =>
|
||||
val hasOSKey = json.keys.exists { keyList: Iterable[String] =>
|
||||
keyList.toSeq.contains(Fields.os)
|
||||
implicit val yamlDecoder: YamlDecoder[JVMOption] =
|
||||
new YamlDecoder[JVMOption] {
|
||||
override def decode(node: Node): Either[Throwable, JVMOption] = {
|
||||
node match {
|
||||
case node: MappingNode =>
|
||||
val bindings = mappingKV(node)
|
||||
val stringDecoder = implicitly[YamlDecoder[String]]
|
||||
val OSdecoder = implicitly[YamlDecoder[OS]]
|
||||
for {
|
||||
value <- bindings
|
||||
.get(Fields.Value)
|
||||
.toRight(new YAMLException(s"missing `${Fields.Value} field"))
|
||||
.flatMap(stringDecoder.decode)
|
||||
osRestriction <- bindings
|
||||
.get(Fields.Os)
|
||||
.map(OSdecoder.decode(_).map(Some(_)))
|
||||
.getOrElse(Right(None))
|
||||
} yield JVMOption(value, osRestriction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
value <- json.get[String](Fields.value)
|
||||
osRestriction <-
|
||||
if (hasOSKey) json.get[OS](Fields.os).map(Some(_)) else Right(None)
|
||||
} yield JVMOption(value, osRestriction)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tries to load the manifest at the given path.
|
||||
@ -135,10 +176,11 @@ object Manifest {
|
||||
*/
|
||||
def load(path: Path): Try[Manifest] =
|
||||
Using(new FileReader(path.toFile)) { reader =>
|
||||
Parser.default
|
||||
.parse(reader)
|
||||
.flatMap(_.as[Manifest])
|
||||
.toTry
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
Try(snakeYaml.compose(reader))
|
||||
.flatMap(
|
||||
implicitly[enso.yaml.YamlDecoder[Manifest]].decode(_).toTry
|
||||
)
|
||||
}.flatten.recoverWith { error =>
|
||||
Failure(ManifestLoadingError.fromThrowable(error))
|
||||
}
|
||||
@ -148,9 +190,11 @@ object Manifest {
|
||||
* Returns None if the definition cannot be parsed.
|
||||
*/
|
||||
def fromYaml(yamlString: String): Try[Manifest] = {
|
||||
yaml.parser
|
||||
.parse(yamlString)
|
||||
.flatMap(_.as[Manifest])
|
||||
val snakeYaml = new org.yaml.snakeyaml.Yaml()
|
||||
Try(snakeYaml.compose(new StringReader(yamlString))).toEither
|
||||
.flatMap(implicitly[enso.yaml.YamlDecoder[Manifest]].decode(_))
|
||||
.left
|
||||
.map(ParseError(_))
|
||||
.toTry
|
||||
.recoverWith { error =>
|
||||
Failure(ManifestLoadingError.fromThrowable(error))
|
||||
@ -176,49 +220,68 @@ object Manifest {
|
||||
*/
|
||||
def fromThrowable(throwable: Throwable): ManifestLoadingError =
|
||||
throwable match {
|
||||
case decodingError: io.circe.Error =>
|
||||
val errorMessage =
|
||||
implicitly[Show[io.circe.Error]].show(decodingError)
|
||||
ManifestLoadingError(
|
||||
s"Could not parse the manifest: $errorMessage",
|
||||
decodingError
|
||||
)
|
||||
case other =>
|
||||
ManifestLoadingError(s"Could not load the manifest: $other", other)
|
||||
}
|
||||
}
|
||||
|
||||
object Fields {
|
||||
val minimumLauncherVersion = "minimum-launcher-version"
|
||||
val minimumProjectManagerVersion = "minimum-project-manager-version"
|
||||
val jvmOptions = "jvm-options"
|
||||
val graalVMVersion = "graal-vm-version"
|
||||
val graalJavaVersion = "graal-java-version"
|
||||
val brokenMark = "broken"
|
||||
val MinimumLauncherVersion = "minimum-launcher-version"
|
||||
val MinimumProjectManagerVersion = "minimum-project-manager-version"
|
||||
val JvmOptions = "jvm-options"
|
||||
val GraalVMVersion = "graal-vm-version"
|
||||
val GraalJavaVersion = "graal-java-version"
|
||||
val NrokenMark = "broken"
|
||||
}
|
||||
|
||||
implicit private val decoder: Decoder[Manifest] = { json =>
|
||||
for {
|
||||
minimumLauncherVersion <- json.get[SemVer](Fields.minimumLauncherVersion)
|
||||
minimumProjectManagerVersion <- json.get[SemVer](
|
||||
Fields.minimumProjectManagerVersion
|
||||
)
|
||||
graalVMVersion <- json.get[String](Fields.graalVMVersion)
|
||||
graalJavaVersion <-
|
||||
json
|
||||
.get[String](Fields.graalJavaVersion)
|
||||
.orElse(json.get[Int](Fields.graalJavaVersion).map(_.toString))
|
||||
jvmOptions <- json.getOrElse[Seq[JVMOption]](Fields.jvmOptions)(Seq())
|
||||
broken <- json.getOrElse[Boolean](Fields.brokenMark)(false)
|
||||
} yield Manifest(
|
||||
requiredInstallerVersions = RequiredInstallerVersions(
|
||||
launcher = minimumLauncherVersion,
|
||||
projectManager = minimumProjectManagerVersion
|
||||
),
|
||||
graalVMVersion = graalVMVersion,
|
||||
graalJavaVersion = graalJavaVersion,
|
||||
jvmOptions = jvmOptions,
|
||||
brokenMark = broken
|
||||
)
|
||||
}
|
||||
implicit val yamlDecoder: YamlDecoder[Manifest] =
|
||||
new YamlDecoder[Manifest] {
|
||||
override def decode(node: Node): Either[Throwable, Manifest] = {
|
||||
node match {
|
||||
case node: MappingNode =>
|
||||
val bindings = mappingKV(node)
|
||||
val requiredInstallerVersionsDecoder =
|
||||
implicitly[YamlDecoder[RequiredInstallerVersions]]
|
||||
val stringDecoder = implicitly[YamlDecoder[String]]
|
||||
val seqJVMOptionsDecoder =
|
||||
implicitly[YamlDecoder[Seq[JVMOption]]]
|
||||
val booleanDecoder = implicitly[YamlDecoder[Boolean]]
|
||||
|
||||
for {
|
||||
requiredInstallerVersions <- requiredInstallerVersionsDecoder
|
||||
.decode(node)
|
||||
graalVMVersion <- bindings
|
||||
.get(Fields.GraalVMVersion)
|
||||
.toRight(
|
||||
new YAMLException(
|
||||
s"Required `${Fields.GraalVMVersion}`field is missing"
|
||||
)
|
||||
)
|
||||
.flatMap(stringDecoder.decode)
|
||||
graalJavaVersion <- bindings
|
||||
.get(Fields.GraalJavaVersion)
|
||||
.toRight(
|
||||
new YAMLException(
|
||||
s"Required `${Fields.GraalJavaVersion}` field is missing"
|
||||
)
|
||||
)
|
||||
.flatMap(stringDecoder.decode)
|
||||
jvmOptions <- bindings
|
||||
.get(Fields.JvmOptions)
|
||||
.map(seqJVMOptionsDecoder.decode)
|
||||
.getOrElse(Right(Seq.empty))
|
||||
brokenMark <- bindings
|
||||
.get(Fields.NrokenMark)
|
||||
.map(booleanDecoder.decode)
|
||||
.getOrElse(Right(false))
|
||||
} yield Manifest(
|
||||
requiredInstallerVersions,
|
||||
graalVMVersion,
|
||||
graalJavaVersion,
|
||||
jvmOptions,
|
||||
brokenMark
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -524,7 +524,7 @@ class RuntimeVersionManager(
|
||||
)
|
||||
) { writer =>
|
||||
writer.newLine()
|
||||
writer.write(s"${Manifest.Fields.brokenMark}: true\n")
|
||||
writer.write(s"${Manifest.Fields.NrokenMark}: true\n")
|
||||
}.get
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
@ -733,7 +733,8 @@ class RuntimeVersionManager(
|
||||
private def loadAndCheckEngineManifest(
|
||||
path: Path
|
||||
): Try[Manifest] = {
|
||||
Manifest.load(path / Manifest.DEFAULT_MANIFEST_NAME).flatMap { manifest =>
|
||||
val manifestPath = path / Manifest.DEFAULT_MANIFEST_NAME
|
||||
Manifest.load(manifestPath).flatMap { manifest =>
|
||||
if (!isEngineVersionCompatibleWithThisInstaller(manifest)) {
|
||||
Failure(
|
||||
UpgradeRequiredError(
|
||||
|
@ -26,7 +26,7 @@ object SemVerJson {
|
||||
Left(
|
||||
DecodingFailure(
|
||||
s"`$version` is not a valid semantic versioning string.",
|
||||
json.history
|
||||
if (json != null) json.history else Nil
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -35,4 +35,5 @@ object SemVerJson {
|
||||
/** [[Encoder]] instance allowing to serialize semantic versioning strings.
|
||||
*/
|
||||
implicit val semverEncoder: Encoder[SemVer] = _.toString.asJson
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
package org.enso.semver
|
||||
|
||||
import org.enso.yaml.{YamlDecoder, YamlEncoder}
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
import org.yaml.snakeyaml.nodes.{Node, ScalarNode}
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object SemVerYaml {
|
||||
|
||||
private def safeParse(version: String): Either[YAMLException, SemVer] = {
|
||||
SemVer.parse(version) match {
|
||||
case Success(parsed) => Right(parsed)
|
||||
case Failure(throwable) =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
s"`$version` is not a valid semantic versioning string.",
|
||||
throwable
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
implicit val yamlSemverDecoder: YamlDecoder[SemVer] =
|
||||
new YamlDecoder[SemVer] {
|
||||
override def decode(node: Node): Either[Throwable, SemVer] = node match {
|
||||
case node: ScalarNode =>
|
||||
safeParse(node.getValue)
|
||||
case _ =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Expected a simple value node for SemVer, instead got " + node.getClass
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val yamlSemverEncoder: YamlEncoder[SemVer] =
|
||||
new YamlEncoder[SemVer] {
|
||||
override def encode(value: SemVer): Object = {
|
||||
value.toString
|
||||
}
|
||||
}
|
||||
}
|
218
lib/scala/yaml/src/main/scala/org/enso/yaml/YamlDecoder.scala
Normal file
218
lib/scala/yaml/src/main/scala/org/enso/yaml/YamlDecoder.scala
Normal file
@ -0,0 +1,218 @@
|
||||
package org.enso.yaml
|
||||
|
||||
import org.yaml.snakeyaml.nodes.Node
|
||||
import org.yaml.snakeyaml.nodes.ScalarNode
|
||||
import org.yaml.snakeyaml.nodes.MappingNode
|
||||
import org.yaml.snakeyaml.nodes.SequenceNode
|
||||
import org.yaml.snakeyaml.nodes.Tag
|
||||
import org.yaml.snakeyaml.error.YAMLException
|
||||
|
||||
import scala.jdk.CollectionConverters.CollectionHasAsScala
|
||||
import scala.collection.{mutable, BuildFrom}
|
||||
|
||||
abstract class YamlDecoder[T] {
|
||||
def decode(node: Node): Either[Throwable, T]
|
||||
|
||||
final protected def mappingKV(mappingNode: MappingNode): Map[String, Node] = {
|
||||
val mutableMap = mutable.HashMap[String, Node]()
|
||||
val values = mappingNode.getValue
|
||||
val len = values.size()
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
val value = values.get(i)
|
||||
value.getKeyNode match {
|
||||
case n: ScalarNode =>
|
||||
mutableMap.put(n.getValue, value.getValueNode)
|
||||
case _: SequenceNode =>
|
||||
throw new YAMLException(
|
||||
"Expected a plain value as a map's key, got a sequence instead"
|
||||
)
|
||||
case _: MappingNode =>
|
||||
throw new YAMLException(
|
||||
"Expected a plain value as a map's key, got a map instead"
|
||||
)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
mutableMap.toMap
|
||||
}
|
||||
}
|
||||
|
||||
object YamlDecoder {
|
||||
implicit def optionDecoderYaml[T](implicit
|
||||
valueDecoder: YamlDecoder[T]
|
||||
): YamlDecoder[Option[T]] = new YamlDecoder[Option[T]] {
|
||||
override def decode(node: Node): Either[Throwable, Option[T]] = node match {
|
||||
case node: ScalarNode =>
|
||||
node.getTag match {
|
||||
case Tag.NULL => Right(None)
|
||||
case _ =>
|
||||
val v = node.getValue
|
||||
if (v == null || v.isEmpty) Right(None)
|
||||
else valueDecoder.decode(node).map(Some(_))
|
||||
}
|
||||
case mappingNode: MappingNode =>
|
||||
valueDecoder
|
||||
.decode(mappingNode)
|
||||
.map(Option(_))
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper class used for automatic decoding of sequences of fields to a map.
|
||||
*/
|
||||
trait MapKeyField {
|
||||
|
||||
/** Determines the name of the field to be used as a key in the map.
|
||||
*/
|
||||
def key: String
|
||||
|
||||
/** Determines if duplicate map entries are allowed in the source YAML.
|
||||
*/
|
||||
def duplicatesAllowed: Boolean
|
||||
}
|
||||
|
||||
object MapKeyField {
|
||||
private case class PlainMapKeyField(key: String, duplicatesAllowed: Boolean)
|
||||
extends MapKeyField
|
||||
|
||||
def plainField(
|
||||
key: String,
|
||||
duplicatesAllowed: Boolean = false
|
||||
): MapKeyField = PlainMapKeyField(key, duplicatesAllowed)
|
||||
}
|
||||
|
||||
implicit def mapDecoderYaml[K, V](implicit
|
||||
keyDecoder: YamlDecoder[K],
|
||||
valueDecoder: YamlDecoder[V],
|
||||
keyMapper: MapKeyField
|
||||
): YamlDecoder[Map[K, V]] = new YamlDecoder[Map[K, V]] {
|
||||
override def decode(node: Node): Either[Throwable, Map[K, V]] = node match {
|
||||
case mapping: MappingNode =>
|
||||
val kv = mapping.getValue.asScala.map { node =>
|
||||
for {
|
||||
k <- keyDecoder.decode(node.getKeyNode)
|
||||
v <- valueDecoder.decode(node.getValueNode)
|
||||
} yield (k, v)
|
||||
}
|
||||
liftEither(kv.toSeq).map(_.toMap)
|
||||
case sequence: SequenceNode =>
|
||||
val result = sequence
|
||||
.getValue()
|
||||
.asScala
|
||||
.toList
|
||||
.map(node =>
|
||||
node match {
|
||||
case mappingNode: MappingNode =>
|
||||
val kv = mappingKV(mappingNode)
|
||||
if (kv.contains(keyMapper.key)) {
|
||||
for {
|
||||
k <- keyDecoder
|
||||
.decode(kv(keyMapper.key).asInstanceOf[ScalarNode])
|
||||
v <- valueDecoder.decode(mappingNode)
|
||||
} yield (k, v)
|
||||
} else {
|
||||
Left(
|
||||
new YAMLException(
|
||||
s"Cannot find '${keyMapper.key}' in the list of fields "
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
val lifted = liftEither(result).map(_.toMap)
|
||||
if (
|
||||
lifted.isRight && lifted
|
||||
.map(_.size)
|
||||
.getOrElse(-1) != result.size && !keyMapper.duplicatesAllowed
|
||||
) Left(new YAMLException("YAML definition contains duplicate entries"))
|
||||
else lifted
|
||||
case _ =>
|
||||
Left(new YAMLException("Expected `MappingNode` for a map value"))
|
||||
}
|
||||
|
||||
def liftEither[A, B](xs: Seq[Either[A, B]]): Either[A, Seq[B]] = {
|
||||
xs.foldLeft[Either[A, Seq[B]]](Right(Seq.empty)) {
|
||||
case (acc @ Left(_), _) => acc
|
||||
case (_, elem @ Left(_)) => elem.asInstanceOf[Either[A, Seq[B]]]
|
||||
case (Right(acc), Right(elem)) => Right(acc :+ elem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicit def stringDecoderYaml: YamlDecoder[String] =
|
||||
new YamlDecoder[String] {
|
||||
override def decode(node: Node): Either[Throwable, String] = {
|
||||
node match {
|
||||
case node: ScalarNode =>
|
||||
Right(node.getValue)
|
||||
case _: MappingNode =>
|
||||
Left(new YAMLException("Expected a plain value, got a map instead"))
|
||||
case _: SequenceNode =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Expected a plain value, got a sequence instead"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicit def booleanDecoderYaml: YamlDecoder[Boolean] =
|
||||
new YamlDecoder[Boolean] {
|
||||
override def decode(node: Node): Either[Throwable, Boolean] = {
|
||||
node match {
|
||||
case node: ScalarNode =>
|
||||
node.getValue match {
|
||||
case "true" => Right(true)
|
||||
case "false" => Right(false)
|
||||
case v => Left(new YAMLException("Unknown boolean value: " + v))
|
||||
}
|
||||
case _: MappingNode =>
|
||||
Left(new YAMLException("Expected a plain value, got a map instead"))
|
||||
case _: SequenceNode =>
|
||||
Left(
|
||||
new YAMLException(
|
||||
"Expected a plain value, got a sequence instead"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicit def iterableDecoderYaml[CC[X] <: IterableOnce[X], T](implicit
|
||||
valueDecoder: YamlDecoder[T],
|
||||
cbf: BuildFrom[List[Either[Throwable, T]], T, CC[T]]
|
||||
): YamlDecoder[CC[T]] = new YamlDecoder[CC[T]] {
|
||||
|
||||
override def decode(node: Node): Either[Throwable, CC[T]] = node match {
|
||||
case seqNode: SequenceNode =>
|
||||
val elements = seqNode.getValue.asScala.map(valueDecoder.decode).toList
|
||||
liftEither(elements)(cbf)
|
||||
case _: ScalarNode =>
|
||||
Left(
|
||||
new YAMLException("Expected a sequence, got a plain value instead")
|
||||
)
|
||||
case _: MappingNode =>
|
||||
Left(new YAMLException("Expected a sequence, got a map instead"))
|
||||
}
|
||||
|
||||
def liftEither[A, B](xs: List[Either[A, B]])(implicit
|
||||
cbf: BuildFrom[List[Either[A, B]], B, CC[B]]
|
||||
): Either[A, CC[B]] = {
|
||||
val builder =
|
||||
xs.foldLeft[Either[A, scala.collection.mutable.Builder[B, CC[B]]]](
|
||||
Right(cbf.newBuilder(xs))
|
||||
) {
|
||||
case (acc @ Left(_), _) => acc
|
||||
case (_, elem @ Left(_)) =>
|
||||
elem.asInstanceOf[
|
||||
Either[A, scala.collection.mutable.Builder[B, CC[B]]]
|
||||
]
|
||||
case (Right(builder), Right(elem)) => Right(builder.addOne(elem))
|
||||
}
|
||||
|
||||
builder.map(_.result())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package org.enso.yaml
|
||||
|
||||
import java.util
|
||||
|
||||
trait YamlEncoder[T] {
|
||||
|
||||
def encode(value: T): Object
|
||||
|
||||
/** Creates a single-element map from the provided values.
|
||||
*/
|
||||
protected def toMap(
|
||||
key: String,
|
||||
value: Object
|
||||
): java.util.Map[String, Object] = {
|
||||
val map = new util.LinkedHashMap[String, Object]()
|
||||
map.put(key, value)
|
||||
map
|
||||
}
|
||||
|
||||
/** Creates a java.util.Map from the provided list of tuples, while preserving the order.
|
||||
* @param elements list of key-value pairs
|
||||
* @return a map
|
||||
*/
|
||||
protected def toMap(
|
||||
elements: java.util.List[(String, Object)]
|
||||
): java.util.Map[String, Object] = {
|
||||
val map: util.Map[String, Object] = new util.LinkedHashMap()
|
||||
elements.forEach { case (k, v) => map.put(k, v) }
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
object YamlEncoder {
|
||||
|
||||
implicit def stringEncoderYaml: YamlEncoder[String] =
|
||||
new YamlEncoder[String] {
|
||||
override def encode(value: String): Object = {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
implicit def booleanEncoderYaml: YamlEncoder[Boolean] =
|
||||
new YamlEncoder[Boolean] {
|
||||
override def encode(value: Boolean): Object = {
|
||||
value.toString
|
||||
}
|
||||
}
|
||||
|
||||
implicit def iterableEncoderYaml[CC[X] <: IterableOnce[X], T](implicit
|
||||
elemEncoder: YamlEncoder[T]
|
||||
): YamlEncoder[CC[T]] = new YamlEncoder[CC[T]] {
|
||||
override def encode(value: CC[T]): Object = {
|
||||
val elements = new util.ArrayList[Object](value.iterator.size)
|
||||
value.iterator.foreach(e => elements.add(elemEncoder.encode(e)))
|
||||
elements
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
Copyright 2016 circe
|
@ -1 +0,0 @@
|
||||
Copyright 2016 circe
|
@ -1,3 +1,3 @@
|
||||
EDA5F924FB902EA37C9FD0A8C8657C4B55E804E33F8E4F2A59802D657D448F43
|
||||
69A14ED5CFDD2E721976A524D46407779F7851C84B8ED9481CC6177B9E3C99A1
|
||||
8DE3C509911C2AB8D430D76BDB6FD401A8262BC700DA927E97A6CC9055B331C9
|
||||
1CCB55F023131497A0E6A16BB5B2D63E5D842572D8638017816EF1D5474B0169
|
||||
0
|
||||
|
@ -1 +0,0 @@
|
||||
Copyright 2016 circe
|
@ -1 +0,0 @@
|
||||
Copyright 2016 circe
|
@ -1,3 +1,3 @@
|
||||
FD4B2B697E262E316088253505039F532B194D2DA6849BC7D4A6C7911DD6E404
|
||||
2DAD16605A78EDE6A4592684F8634387B4906BCA6932D33620CB2E23C7832215
|
||||
61FA814CA4FC0688FB059CC530561D4B5E329B33919A6DBEAD9CBD9C19D49337
|
||||
F356D9CC4CE4F118B02747EBA189642FCFB2EA96121262374402C3BA3B6B5ECD
|
||||
0
|
||||
|
@ -1 +0,0 @@
|
||||
Copyright 2016 circe
|
@ -1 +0,0 @@
|
||||
Copyright 2016 circe
|
@ -1,3 +1,3 @@
|
||||
6A91D4302F22E9404B2BF4D85EFEDF4D7F363633A12499A5A3C7C244E9CB4E4E
|
||||
D4E2379AA0DB83E264E6E580E39ABA46F3D0322CC79234EECAE2A79D39E46E74
|
||||
69E9EEFDB627E4C31E3C7184A7CF645047519F5D162D8BBA2715CB494C26E4FF
|
||||
FC47A03D984C60193E6785C0EC0B681C0F8903F5AF4106AEB319F26F9B3A9CBB
|
||||
0
|
||||
|
Loading…
Reference in New Issue
Block a user