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:
Hubert Plociniczak 2024-07-05 09:32:45 +02:00 committed by GitHub
parent 0661f17d1c
commit c54c3b7e9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 1980 additions and 852 deletions

View File

@ -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)

View File

@ -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`.

View File

@ -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.
*/

View File

@ -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.
*/

View File

@ -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`.

View File

@ -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.
*/

View File

@ -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.
*/

View File

@ -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`.

View File

@ -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.
*/

View File

@ -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.
*/

View File

@ -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})."

View File

@ -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
}

View File

@ -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 =>
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 {
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
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 {
)
)
}
}
}

View File

@ -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"))
}
}
}

View File

@ -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,23 +33,33 @@ 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]].

View File

@ -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 =>
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]]]
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 <- 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
)
defaultVersion <- defaultVersionOpt
name <- nameOpt
email <- emailOpt
editionProvider <- editionProviderOpt
} yield GlobalConfig(defaultVersion, name, email, editionProvider)
}
}
/** [[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
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)
)
overrides.dropNullValues.asJson
)
)
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)
}
}
}

View File

@ -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)
def updateConfigRaw(key: String, value: String): Unit = {
stringToYamlNode(value)
.flatMap(newValueNode =>
updateYamlNode(key.split("\\.").toList, getConfig, newValueNode)
.flatMap { updatedNode =>
GlobalConfigurationManager
.writeConfigRaw(configLocation, updated.asJson)
.writeConfigRaw(configLocation, updatedNode)
.recoverWith { case e: InvalidConfigError =>
Failure(
InvalidConfigError(
s"Invalid value for key `$key`. Config changes were not saved.",
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)
updateYamlNode(key.split("\\.").toList, getConfig, null).map(updatedNode =>
GlobalConfigurationManager.writeConfigRaw(
configLocation,
updated.asJson
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()
}

View File

@ -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,31 +24,28 @@ 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

View File

@ -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
}
}

View File

@ -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. */

View File

@ -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

View File

@ -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 {

View File

@ -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,16 +15,34 @@ 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

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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"
)
}
}
}

View File

@ -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}

View File

@ -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 =>
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 <- 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)
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 = archives,
dependencies = dependencies,
tagLine = tagLine,
description = description
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
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))
)
val allFields = baseFields ++
manifest.tagLine.map(Fields.tagLine -> _.asJson).toSeq ++
manifest.description.map(Fields.description -> _.asJson).toSeq
Json.obj(allFields: _*)
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

View File

@ -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))

View File

@ -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,34 +42,37 @@ 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)
}
}
}
@ -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
}
.map(JsonFields.edition -> _)
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 =
Option.unless(
config.componentGroups.isEmpty
)(
JsonFields.componentGroups -> config.componentGroups.asJson
implicitly[YamlDecoder[Option[ComponentGroups]]]
for {
name <- clazzMap
.get(JsonFields.Name)
.toRight(
new YAMLException(s"Missing '${JsonFields.Name}' field")
)
val normalizedName = config.normalizedName.map(value =>
JsonFields.normalizedName -> value.asJson
.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))
)
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
val preferLocalOverride =
if (config.preferLocalLibraries)
Seq(JsonFields.preferLocalLibraries -> true.asJson)
else Seq()
val overridesObject = JsonObject(
overrides ++ preferLocalOverride: _*
.asInstanceOf[Either[EditionName, Option[Editions.RawEdition]]]
edition <- rawEdition.fold(
editionName =>
Right(
Some(Editions.Raw.Edition(parent = Some(editionName.name)))
),
r => Right(r)
)
overridesObject.asJson
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
)
}
}
/** Tries to parse the [[Config]] from a YAML string. */
def fromYaml(yamlString: String): Try[Config] = {
yaml.parser.parse(yamlString).flatMap(_.as[Config]).toTry
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 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))
)
}
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))
)
)
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)
}
}

View File

@ -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(

View File

@ -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")

View File

@ -108,7 +108,7 @@ class ProjectService[
id = projectId,
name = name,
module = moduleName,
namespace = Config.defaultNamespace,
namespace = Config.DefaultNamespace,
kind = UserProject,
created = creationTime,
edition = None,

View File

@ -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 */

View File

@ -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"
)
}
}

View File

@ -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,25 +143,32 @@ object Manifest {
object JVMOption {
private object Fields {
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)
val Os = "os"
val Value = "value"
}
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 <- json.get[String](Fields.value)
osRestriction <-
if (hasOSKey) json.get[OS](Fields.os).map(Some(_)) else Right(None)
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)
}
}
}
}
/** 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 =>
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 {
minimumLauncherVersion <- json.get[SemVer](Fields.minimumLauncherVersion)
minimumProjectManagerVersion <- json.get[SemVer](
Fields.minimumProjectManagerVersion
requiredInstallerVersions <- requiredInstallerVersionsDecoder
.decode(node)
graalVMVersion <- bindings
.get(Fields.GraalVMVersion)
.toRight(
new YAMLException(
s"Required `${Fields.GraalVMVersion}`field is missing"
)
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)
)
.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 = RequiredInstallerVersions(
launcher = minimumLauncherVersion,
projectManager = minimumProjectManagerVersion
),
graalVMVersion = graalVMVersion,
graalJavaVersion = graalJavaVersion,
jvmOptions = jvmOptions,
brokenMark = broken
requiredInstallerVersions,
graalVMVersion,
graalJavaVersion,
jvmOptions,
brokenMark
)
}
}
}
}

View File

@ -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(

View File

@ -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
}

View File

@ -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
}
}
}

View 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())
}
}
}

View File

@ -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
}
}
}

View File

@ -1,3 +1,3 @@
EDA5F924FB902EA37C9FD0A8C8657C4B55E804E33F8E4F2A59802D657D448F43
69A14ED5CFDD2E721976A524D46407779F7851C84B8ED9481CC6177B9E3C99A1
8DE3C509911C2AB8D430D76BDB6FD401A8262BC700DA927E97A6CC9055B331C9
1CCB55F023131497A0E6A16BB5B2D63E5D842572D8638017816EF1D5474B0169
0

View File

@ -1,3 +1,3 @@
FD4B2B697E262E316088253505039F532B194D2DA6849BC7D4A6C7911DD6E404
2DAD16605A78EDE6A4592684F8634387B4906BCA6932D33620CB2E23C7832215
61FA814CA4FC0688FB059CC530561D4B5E329B33919A6DBEAD9CBD9C19D49337
F356D9CC4CE4F118B02747EBA189642FCFB2EA96121262374402C3BA3B6B5ECD
0

View File

@ -1,3 +1,3 @@
6A91D4302F22E9404B2BF4D85EFEDF4D7F363633A12499A5A3C7C244E9CB4E4E
D4E2379AA0DB83E264E6E580E39ABA46F3D0322CC79234EECAE2A79D39E46E74
69E9EEFDB627E4C31E3C7184A7CF645047519F5D162D8BBA2715CB494C26E4FF
FC47A03D984C60193E6785C0EC0B681C0F8903F5AF4106AEB319F26F9B3A9CBB
0