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`, `syntax-rust-definition`,
`text-buffer`, `text-buffer`,
yaml, yaml,
`scala-yaml`,
pkg, pkg,
cli, cli,
`task-progress-notifications`, `task-progress-notifications`,
@ -417,10 +418,10 @@ val catsVersion = "2.9.0"
// === Circe ================================================================== // === Circe ==================================================================
val circeVersion = "0.14.7" val circeVersion = "0.14.7"
val circeYamlVersion = "0.15.1"
val circeGenericExtrasVersion = "0.14.3" val circeGenericExtrasVersion = "0.14.3"
val circe = Seq("circe-core", "circe-generic", "circe-parser") val circe = Seq("circe-core", "circe-generic", "circe-parser")
.map("io.circe" %% _ % circeVersion) .map("io.circe" %% _ % circeVersion)
val snakeyamlVersion = "2.2"
// === Commons ================================================================ // === Commons ================================================================
@ -751,7 +752,16 @@ lazy val yaml = (project in file("lib/java/yaml"))
frgaalJavaCompilerSetting, frgaalJavaCompilerSetting,
version := "0.1", version := "0.1",
libraryDependencies ++= Seq( 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", version := "0.1",
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided", "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.scalatest" %% "scalatest" % scalatestVersion % Test,
"org.apache.commons" % "commons-compress" % commonsCompressVersion "org.apache.commons" % "commons-compress" % commonsCompressVersion
) )
@ -930,10 +941,12 @@ lazy val cli = project
version := "0.1", version := "0.1",
libraryDependencies ++= circe ++ Seq( libraryDependencies ++= circe ++ Seq(
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion, "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
"org.yaml" % "snakeyaml" % snakeyamlVersion % "provided",
"org.scalatest" %% "scalatest" % scalatestVersion % Test "org.scalatest" %% "scalatest" % scalatestVersion % Test
), ),
Test / parallelExecution := false Test / parallelExecution := false
) )
.dependsOn(`scala-yaml`)
lazy val `task-progress-notifications` = project lazy val `task-progress-notifications` = project
.in(file("lib/scala/task-progress-notifications")) .in(file("lib/scala/task-progress-notifications"))
@ -1461,11 +1474,11 @@ lazy val `polyglot-api` = project
"runtime-fat-jar" "runtime-fat-jar"
) / Compile / fullClasspath).value, ) / Compile / fullClasspath).value,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"io.circe" %% "circe-core" % circeVersion % "provided",
"org.graalvm.sdk" % "polyglot-tck" % graalMavenPackagesVersion % "provided", "org.graalvm.sdk" % "polyglot-tck" % graalMavenPackagesVersion % "provided",
"org.graalvm.truffle" % "truffle-api" % 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-macros" % jsoniterVersion,
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % 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, "com.google.flatbuffers" % "flatbuffers-java" % flatbuffersVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test, "org.scalatest" %% "scalatest" % scalatestVersion % Test,
"org.scalacheck" %% "scalacheck" % scalacheckVersion % Test "org.scalacheck" %% "scalacheck" % scalacheckVersion % Test
@ -2764,7 +2777,7 @@ lazy val `distribution-manager` = project
resolvers += Resolver.bintrayRepo("gn0s1s", "releases"), resolvers += Resolver.bintrayRepo("gn0s1s", "releases"),
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion, "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
"io.circe" %% "circe-yaml" % circeYamlVersion, "org.yaml" % "snakeyaml" % snakeyamlVersion,
"commons-io" % "commons-io" % commonsIoVersion, "commons-io" % "commons-io" % commonsIoVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test "org.scalatest" %% "scalatest" % scalatestVersion % Test
) )
@ -2944,7 +2957,8 @@ lazy val editions = project
.settings( .settings(
frgaalJavaCompilerSetting, frgaalJavaCompilerSetting,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"io.circe" %% "circe-yaml" % circeYamlVersion % "provided", "io.circe" %% "circe-core" % circeVersion % "provided",
"org.yaml" % "snakeyaml" % snakeyamlVersion % "provided",
"org.scalatest" %% "scalatest" % scalatestVersion % Test "org.scalatest" %% "scalatest" % scalatestVersion % Test
) )
) )
@ -2973,7 +2987,8 @@ lazy val semver = project
.settings( .settings(
frgaalJavaCompilerSetting, frgaalJavaCompilerSetting,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"io.circe" %% "circe-yaml" % circeYamlVersion % "provided", "io.circe" %% "circe-core" % circeVersion % "provided",
"org.yaml" % "snakeyaml" % snakeyamlVersion % "provided",
"org.scalatest" %% "scalatest" % scalatestVersion % Test, "org.scalatest" %% "scalatest" % scalatestVersion % Test,
"junit" % "junit" % junitVersion % Test, "junit" % "junit" % junitVersion % Test,
"com.github.sbt" % "junit-interface" % junitIfVersion % Test "com.github.sbt" % "junit-interface" % junitIfVersion % Test
@ -2996,6 +3011,7 @@ lazy val semver = project
cleanFiles += baseDirectory.value / ".." / ".." / "distribution" / "editions" cleanFiles += baseDirectory.value / ".." / ".." / "distribution" / "editions"
) )
.dependsOn(testkit % Test) .dependsOn(testkit % Test)
.dependsOn(`scala-yaml`)
lazy val downloader = (project in file("lib/scala/downloader")) lazy val downloader = (project in file("lib/scala/downloader"))
.settings( .settings(
@ -3040,7 +3056,7 @@ lazy val `edition-uploader` = project
.settings( .settings(
frgaalJavaCompilerSetting, frgaalJavaCompilerSetting,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"io.circe" %% "circe-yaml" % circeYamlVersion % "provided" "io.circe" %% "circe-core" % circeVersion % "provided"
) )
) )
.dependsOn(editions) .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`. 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. 'helidon-builder-api', licensed under the Apache 2.0, is distributed with the engine.
The license file can be found at `licenses/APACHE2.0`. 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`. 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`. 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. 'commons-compress', licensed under the Apache-2.0, is distributed with the launcher.
The license file can be found at `licenses/APACHE2.0`. 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`. 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`. 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. '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`. 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`. 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 java.nio.file.Path
import com.typesafe.scalalogging.Logger import com.typesafe.scalalogging.Logger
import io.circe.Json
import org.enso.semver.SemVer import org.enso.semver.SemVer
import org.enso.distribution.config.DefaultVersion import org.enso.distribution.config.DefaultVersion
import org.enso.editions.updater.EditionManager import org.enso.editions.updater.EditionManager
@ -368,7 +367,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
s"(${configurationManager.configLocation.toAbsolutePath})." s"(${configurationManager.configLocation.toAbsolutePath})."
) )
} else { } else {
configurationManager.updateConfigRaw(key, Json.fromString(value)) configurationManager.updateConfigRaw(key, value)
InfoLogger.info( InfoLogger.info(
s"""Key `$key` set to "$value" in the global configuration file """ + s"""Key `$key` set to "$value" in the global configuration file """ +
s"(${configurationManager.configLocation.toAbsolutePath})." s"(${configurationManager.configLocation.toAbsolutePath})."

View File

@ -1,7 +1,9 @@
package org.enso.launcher.releases.fallback.staticwebsite 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 import scala.util.Try
/** Manifest of the fallback mechanism. /** Manifest of the fallback mechanism.
@ -13,6 +15,23 @@ case class FallbackManifest(enabled: Boolean)
object FallbackManifest { 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 /** Defines a part of the URL scheme of the fallback mechanism - the name of
* manifest file. * manifest file.
* *
@ -25,17 +44,10 @@ object FallbackManifest {
val enabled = "enabled" val enabled = "enabled"
} }
/** [[Decoder]] instance for [[FallbackManifest]]. def parseString(yamlString: String): Try[FallbackManifest] = {
* val snakeYaml = new org.yaml.snakeyaml.Yaml()
* It should always remain backwards compatible, since the fallback mechanism Try(snakeYaml.compose(new StringReader(yamlString))).toEither
* must work for all released launcher versions. .flatMap(implicitly[YamlDecoder[FallbackManifest]].decode(_))
*/ .toTry
implicit val decoder: Decoder[FallbackManifest] = { json =>
for {
enabled <- json.get[Boolean](Fields.enabled)
} yield FallbackManifest(enabled)
} }
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 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.semver.SemVer
import org.enso.runtimeversionmanager.releases.ReleaseProviderException 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} import scala.util.{Failure, Try}
/** Contains release metadata associated with a launcher release. /** Contains release metadata associated with a launcher release.
@ -37,28 +41,52 @@ object LauncherManifest {
val directoriesToCopy = "directories-to-copy" val directoriesToCopy = "directories-to-copy"
} }
/** [[Decoder]] instance for [[LauncherManifest]]. implicit val yamlDecoder: YamlDecoder[LauncherManifest] =
*/ new YamlDecoder[LauncherManifest] {
implicit val decoder: Decoder[LauncherManifest] = { json => 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 { for {
minimumVersionToUpgrade <- minimumVersionForUpgrade <- bindings
json.get[SemVer](Fields.minimumVersionForUpgrade) .get(Fields.minimumVersionForUpgrade)
files <- json.getOrElse[Seq[String]](Fields.filesToCopy)(Seq()) .map(semverDecoder.decode)
directories <- .getOrElse(
json.getOrElse[Seq[String]](Fields.directoriesToCopy)(Seq()) Left(
} yield LauncherManifest( new YAMLException(
minimumVersionForUpgrade = minimumVersionToUpgrade, s"Required `${Fields.minimumVersionForUpgrade}` field is missing"
filesToCopy = files,
directoriesToCopy = directories
) )
)
)
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]]. /** Tries to parse the [[LauncherManifest]] from a [[String]].
*/ */
def fromYAML(string: String): Try[LauncherManifest] = def fromYAML(string: String): Try[LauncherManifest] = {
yaml.parser val snakeYaml = new org.yaml.snakeyaml.Yaml()
.parse(string) Try(snakeYaml.compose(new StringReader(string))).toEither
.flatMap(_.as[LauncherManifest]) .flatMap(
implicitly[YamlDecoder[launcher.LauncherManifest]].decode(_)
)
.left
.map(ParseError(_))
.toTry .toTry
.recoverWith { error => .recoverWith { error =>
// TODO [RW] more readable errors in #1111 // TODO [RW] more readable errors in #1111
@ -70,3 +98,4 @@ object LauncherManifest {
) )
} }
} }
}

View File

@ -2,6 +2,9 @@ package org.enso.cli
import com.typesafe.scalalogging.Logger import com.typesafe.scalalogging.Logger
import io.circe.{Decoder, DecodingFailure} 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). /** Represents one of the supported platforms (operating systems).
*/ */
@ -29,7 +32,7 @@ object OS {
/** @inheritdoc /** @inheritdoc
*/ */
def configName: String = "linux" val configName: String = "linux"
} }
/** Represents the macOS operating system. /** Represents the macOS operating system.
@ -38,7 +41,7 @@ object OS {
/** @inheritdoc /** @inheritdoc
*/ */
def configName: String = "macos" val configName: String = "macos"
/** @inheritdoc /** @inheritdoc
*/ */
@ -52,7 +55,7 @@ object OS {
/** @inheritdoc /** @inheritdoc
*/ */
def configName: String = "windows" val configName: String = "windows"
} }
/** Checks if the application is being run on 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 package org.enso.distribution.config
import io.circe.syntax._
import io.circe.{Decoder, Encoder, Json}
import org.enso.semver.SemVer import org.enso.semver.SemVer
import org.enso.cli.arguments.{Argument, OptsParseError} 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 /** Default version that is used when launching Enso outside of projects and
* when creating new projects. * when creating new projects.
@ -17,9 +17,11 @@ object DefaultVersion {
*/ */
case object LatestInstalled extends DefaultVersion { case object LatestInstalled extends DefaultVersion {
val name = "latest-installed"
/** @inheritdoc /** @inheritdoc
*/ */
override def toString: String = "latest-installed" override def toString: String = name
} }
/** Defaults to a specified version. /** Defaults to a specified version.
@ -31,23 +33,33 @@ object DefaultVersion {
override def toString: String = version.toString override def toString: String = version.toString
} }
/** [[Encoder]] instance for [[DefaultVersion]]. implicit val yamlDecoder: YamlDecoder[DefaultVersion] =
*/ new YamlDecoder[DefaultVersion] {
implicit val encoder: Encoder[DefaultVersion] = { override def decode(node: Node) = {
case LatestInstalled => node match {
Json.Null case node if node == null =>
case Exact(version) => Right(LatestInstalled)
version.asJson 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 yamlEncoder: YamlEncoder[DefaultVersion] =
*/ new YamlEncoder[DefaultVersion] {
implicit val decoder: Decoder[DefaultVersion] = { json => override def encode(value: DefaultVersion): AnyRef = {
if (json.value.isNull) Right(LatestInstalled) value match {
else case latest @ LatestInstalled => latest.toString
for { case Exact(version) => version.toString
version <- json.as[SemVer] }
} yield Exact(version) }
} }
/** [[Argument]] instance for [[DefaultVersion]]. /** [[Argument]] instance for [[DefaultVersion]].

View File

@ -1,8 +1,10 @@
package org.enso.distribution.config package org.enso.distribution.config
import io.circe.syntax._ import org.enso.yaml.{YamlDecoder, YamlEncoder}
import io.circe.{Decoder, Encoder, Json} import org.yaml.snakeyaml.error.YAMLException
import org.enso.distribution.config import org.yaml.snakeyaml.nodes.{MappingNode, Node}
import java.util
/** Global user configuration. /** Global user configuration.
* *
@ -25,20 +27,18 @@ case class GlobalConfig(
editionProviders: Seq[String] editionProviders: Seq[String]
) { ) {
def findByKey(key: String): Option[String] = { def findByKey(key: String): Option[String] = {
val jsonValue: Option[Json] = key match { key match {
case GlobalConfig.Fields.DefaultVersion => case GlobalConfig.Fields.DefaultVersion =>
Option(defaultVersion).map(_.asJson) Option(defaultVersion).map(_.toString)
case GlobalConfig.Fields.AuthorName => case GlobalConfig.Fields.AuthorName =>
authorName.map(_.asJson) authorName
case GlobalConfig.Fields.AuthorEmail => case GlobalConfig.Fields.AuthorEmail =>
authorEmail.map(_.asJson) authorEmail
case GlobalConfig.Fields.EditionProviders => case GlobalConfig.Fields.EditionProviders =>
Option(editionProviders).map(_.asJson) Option(editionProviders).map(_.toString())
case _ => case _ =>
None None
} }
jsonValue.map(j => j.asString.getOrElse(j.toString()))
} }
} }
@ -66,36 +66,103 @@ object GlobalConfig {
val EditionProviders = "edition-providers" val EditionProviders = "edition-providers"
} }
/** [[Decoder]] instance for [[GlobalConfig]]. implicit val yamlDecoder: YamlDecoder[GlobalConfig] =
*/ new YamlDecoder[GlobalConfig] {
implicit val decoder: Decoder[GlobalConfig] = { json => 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 { for {
defaultVersion <- json.getOrElse[DefaultVersion](Fields.DefaultVersion)( defaultVersion <- defaultVersionOpt
DefaultVersion.LatestInstalled name <- nameOpt
) email <- emailOpt
authorName <- json.getOrElse[Option[String]](Fields.AuthorName)(None) editionProvider <- editionProviderOpt
authorEmail <- json.getOrElse[Option[String]](Fields.AuthorEmail)(None) } yield GlobalConfig(defaultVersion, name, email, editionProvider)
editionProviders <- json.getOrElse[Seq[String]](Fields.EditionProviders)( }
defaultEditionProviders
)
} yield config.GlobalConfig(
defaultVersion = defaultVersion,
authorName = authorName,
authorEmail = authorEmail,
editionProviders = editionProviders
)
} }
/** [[Encoder]] instance for [[GlobalConfig]]. implicit val yamlEncoder: YamlEncoder[GlobalConfig] =
*/ new YamlEncoder[GlobalConfig] {
implicit val encoder: Encoder[GlobalConfig] = { config => override def encode(value: GlobalConfig): AnyRef = {
val overrides = val defaultVersionEncoder = implicitly[YamlEncoder[DefaultVersion]]
Json.obj( val editionProviders = implicitly[YamlEncoder[Seq[String]]]
Fields.DefaultVersion -> config.defaultVersion.asJson, val elements = new util.ArrayList[(String, AnyRef)]()
Fields.AuthorName -> config.authorName.asJson, elements.add(
Fields.AuthorEmail -> config.authorEmail.asJson, (
Fields.EditionProviders -> config.editionProviders.asJson "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 package org.enso.distribution.config
import com.typesafe.scalalogging.Logger 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.DistributionManager
import org.enso.distribution.FileSystem.PathSyntax 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 java.nio.file.{Files, NoSuchFileException, Path}
import scala.jdk.CollectionConverters.{CollectionHasAsScala, SeqHasAsJava}
import scala.util.{Failure, Success, Try, Using} import scala.util.{Failure, Success, Try, Using}
/** Manages the global configuration of the distribution. */ /** 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 * (because an invalid value has been set for a known field), the config is
* not saved and an exception is thrown. * not saved and an exception is thrown.
*/ */
def updateConfigRaw(key: String, value: Json): Unit = { def updateConfigRaw(key: String, value: String): Unit = {
val updated = GlobalConfig.encoder(getConfig).asObject.get.add(key, value) stringToYamlNode(value)
.flatMap(newValueNode =>
updateYamlNode(key.split("\\.").toList, getConfig, newValueNode)
.flatMap { updatedNode =>
GlobalConfigurationManager GlobalConfigurationManager
.writeConfigRaw(configLocation, updated.asJson) .writeConfigRaw(configLocation, updatedNode)
.recoverWith { case e: InvalidConfigError => .recoverWith { case e: InvalidConfigError =>
Failure( Failure(
InvalidConfigError( InvalidConfigError(
s"Invalid value for key `$key`. Config changes were not saved.", s"Invalid value for key `$key`. Config changes were not saved",
e e
) )
) )
} }
}
)
.get .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. /** Removes the `key` from the config.
* *
* If removing that setting would result in the config becoming unreadable, * If removing that setting would result in the config becoming unreadable,
* the config is not saved and an exception is thrown. * the config is not saved and an exception is thrown.
*/ */
def removeFromConfig(key: String): Unit = { def removeFromConfig(key: String): Unit = {
val updated = GlobalConfig.encoder(getConfig).asObject.get.remove(key) updateYamlNode(key.split("\\.").toList, getConfig, null).map(updatedNode =>
GlobalConfigurationManager.writeConfigRaw( GlobalConfigurationManager.writeConfigRaw(
configLocation, configLocation,
updated.asJson updatedNode
)
) )
} }
} }
@ -85,24 +215,31 @@ object GlobalConfigurationManager {
/** Tries to read the global config from the given `path`. */ /** Tries to read the global config from the given `path`. */
private def readConfig(path: Path): Try[GlobalConfig] = private def readConfig(path: Path): Try[GlobalConfig] =
Using(Files.newBufferedReader(path)) { reader => Using(Files.newBufferedReader(path)) { reader =>
for { val snakeYaml = new Yaml()
json <- Parser.default.parse(reader) Try(snakeYaml.compose(reader)).toEither
config <- json.as[GlobalConfig] .flatMap(implicitly[YamlDecoder[GlobalConfig]].decode(_))
} yield config .toTry
}.flatMap(_.toTry) }.flatten
/** Tries to write the provided `config` to the given `path`. */ /** Tries to write the provided `config` to the given `path`. */
private def writeConfig(path: Path, config: GlobalConfig): Try[Unit] = private def writeConfig(path: Path, config: GlobalConfig): Try[Unit] = {
writeConfigRaw(path, GlobalConfig.encoder(config)) 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`. /** 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 * The config will not be saved if it is invalid, instead an exception is
* thrown. * thrown.
*/ */
private def writeConfigRaw(path: Path, rawConfig: Json): Try[Unit] = { private def writeConfigRaw(path: Path, rawNode: Node): Try[Unit] = {
def verifyConfig: Try[Unit] = def verifyConfig: Try[Unit] = {
rawConfig.as[GlobalConfig] match { implicitly[YamlDecoder[GlobalConfig]].decode(rawNode) match {
case Left(failure) => case Left(failure) =>
Failure( Failure(
InvalidConfigError( InvalidConfigError(
@ -112,16 +249,19 @@ object GlobalConfigurationManager {
) )
case Right(_) => Success(()) case Right(_) => Success(())
} }
}
def bufferedWriter: BufferedWriter = { def bufferedWriter: BufferedWriter = {
Files.createDirectories(path.getParent) Files.createDirectories(path.getParent)
Files.newBufferedWriter(path) Files.newBufferedWriter(path)
} }
def writeConfig: Try[Unit] = def writeConfig: Try[Unit] =
Using(bufferedWriter) { writer => Using(bufferedWriter) { writer =>
val string = yaml.Printer.spaces2 val dumperOptions = new DumperOptions()
.copy(preserveOrder = true) dumperOptions.setIndent(2)
.pretty(rawConfig) dumperOptions.setPrettyFlow(true)
writer.write(string) val yaml = new Yaml(dumperOptions)
yaml.serialize(rawNode, writer)
writer.newLine() writer.newLine()
} }

View File

@ -1,7 +1,8 @@
package org.enso.editions package org.enso.editions
import io.circe.syntax.EncoderOps import org.yaml.snakeyaml.error.YAMLException
import io.circe.{Decoder, DecodingFailure, Encoder, Json} 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. /** A helper type to handle special parsing logic of edition names.
* *
@ -23,31 +24,28 @@ object EditionName {
/** A helper method for constructing an [[EditionName]]. */ /** A helper method for constructing an [[EditionName]]. */
def apply(name: String): EditionName = new EditionName(name) def apply(name: String): EditionName = new EditionName(name)
/** A [[Decoder]] instance for [[EditionName]] that accepts not only strings implicit val yamlDecoder: YamlDecoder[EditionName] =
* but also numbers as valid edition names. new YamlDecoder[EditionName] {
*/ override def decode(node: Node): Either[Throwable, EditionName] =
implicit val editionNameDecoder: Decoder[EditionName] = { json => node match {
json case scalarNode: ScalarNode =>
.as[String] scalarNode.getTag match {
.fold[Either[DecodingFailure, Any]]( case Tag.NULL =>
_ => Left(new YAMLException("edition cannot be empty"))
if (json.value == Json.Null) case _ =>
Left(DecodingFailure("edition cannot be empty", Nil)) val stringDecoder = implicitly[YamlDecoder[String]]
else stringDecoder.decode(scalarNode).map(EditionName(_))
json.as[Int].orElse(json.as[Float]), }
Right(_) case _ =>
) Left(new YAMLException("unexpected edition name"))
.map(v => EditionName(v.toString)) }
} }
/** An [[Encoder]] instance for serializing [[EditionName]]. implicit val yamlEncoder: YamlEncoder[EditionName] =
* new YamlEncoder[EditionName] {
* Regardless of the original representation, the edition name is always override def encode(value: EditionName): Object = {
* serialized as string as this is the most portable and precise format for value.name
* this datatype. }
*/
implicit val encoder: Encoder[EditionName] = { case EditionName(name) =>
name.asJson
} }
/** The filename suffix that is used to create a filename corresponding to a /** The filename suffix that is used to create a filename corresponding to a

View File

@ -1,17 +1,10 @@
package org.enso.editions package org.enso.editions
import cats.Show import org.enso.editions.Editions.Raw
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.yaml.YamlHelper import org.enso.yaml.YamlHelper
import java.io.FileReader
import java.nio.file.Path import java.nio.file.Path
import scala.util.{Failure, Try, Using} import scala.util.{Failure, Try}
import org.enso.semver.SemVer
/** Gathers methods for decoding and encoding of Raw editions. */ /** Gathers methods for decoding and encoding of Raw editions. */
object EditionSerialization { object EditionSerialization {
@ -26,12 +19,8 @@ object EditionSerialization {
/** Tries to load an edition definition from a YAML file. */ /** Tries to load an edition definition from a YAML file. */
def loadEdition(path: Path): Try[Raw.Edition] = def loadEdition(path: Path): Try[Raw.Edition] =
Using(new FileReader(path.toFile)) { reader => YamlHelper
Parser.default .load[Raw.Edition](path)
.parse(reader)
.flatMap(_.as[Raw.Edition])
.toTry
}.flatten
.recoverWith { error => .recoverWith { error =>
Failure( Failure(
EditionResolutionError.wrapLoadingError( 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 { object Fields {
val name = "name" val name = "name"
val version = "version" val version = "version"
@ -136,99 +49,4 @@ object EditionSerialization {
override def toString: String = message 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 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. /** Defines the general edition structure.
* *
@ -43,6 +49,136 @@ trait Editions {
def name: LibraryName 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. */ /** Represents a local library. */
case class LocalLibrary(override val name: LibraryName) extends Library case class LocalLibrary(override val name: LibraryName) extends Library
@ -111,6 +247,99 @@ trait Editions {
libraries.map(l => (l.name, l)).toMap 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 { object Editions {
@ -120,11 +349,53 @@ object Editions {
object Repository { object Repository {
object Fields {
val Name = "name"
val Url = "url"
}
/** An alternative constructor for unnamed repositories. /** An alternative constructor for unnamed repositories.
* *
* The URL is used as the repository name. * The URL is used as the repository name.
*/ */
def apply(url: String): Repository = Repository(url, url) 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 /** Implements the Raw editions that can be directly parsed from a YAML
@ -136,6 +407,18 @@ object Editions {
object Raw extends Editions { object Raw extends Editions {
override type NestedEditionType = String override type NestedEditionType = String
override type LibraryRepositoryType = 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 /** Implements the Resolved editions which are obtained by analyzing the Raw
@ -144,6 +427,17 @@ object Editions {
object Resolved extends Editions { object Resolved extends Editions {
override type NestedEditionType = this.Edition override type NestedEditionType = this.Edition
override type LibraryRepositoryType = Repository 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. */ /** An alias for Raw editions. */

View File

@ -1,6 +1,6 @@
package org.enso.editions package org.enso.editions
import io.circe.syntax._ import io.circe.syntax.EncoderOps
import io.circe.{Decoder, DecodingFailure, Encoder} import io.circe.{Decoder, DecodingFailure, Encoder}
import org.enso.semver.SemVer import org.enso.semver.SemVer

View File

@ -2,6 +2,9 @@ package org.enso.editions
import io.circe.syntax.EncoderOps import io.circe.syntax.EncoderOps
import io.circe.{Decoder, DecodingFailure, Encoder} 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. /** Represents a library name that should uniquely identify the library.
* *
@ -22,6 +25,47 @@ case class LibraryName(namespace: String, name: String) {
object LibraryName { 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]]. */ /** A [[Decoder]] instance allowing to parse a [[LibraryName]]. */
implicit val decoder: Decoder[LibraryName] = { json => implicit val decoder: Decoder[LibraryName] = { json =>
for { for {

View File

@ -1,8 +1,9 @@
package org.enso.editions.repository package org.enso.editions.repository
import io.circe._
import io.circe.syntax.EncoderOps
import org.enso.editions.EditionName 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 /** The Edition Repository manifest, which lists all editions that the
* repository provides. * repository provides.
@ -14,16 +15,34 @@ object Manifest {
val editions = "editions" val editions = "editions"
} }
/** A [[Decoder]] instance for parsing [[Manifest]]. */ implicit val yamlDecoder: YamlDecoder[Manifest] =
implicit val decoder: Decoder[Manifest] = { json => new YamlDecoder[Manifest] {
for { override def decode(node: Node): Either[Throwable, Manifest] =
editions <- json.get[Seq[EditionName]](Fields.editions) node match {
} yield Manifest(editions) 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 yamlEncoder: YamlEncoder[Manifest] =
implicit val encoder: Encoder[Manifest] = { manifest => new YamlEncoder[Manifest] {
Json.obj(Fields.editions -> manifest.editions.asJson) 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 /** The name of the manifest file that should be present at the root of

View File

@ -1,19 +1,15 @@
package org.enso.yaml package org.enso.yaml
import cats.Show
/** Indicates a parse failure, usually meaning that the input data has /** Indicates a parse failure, usually meaning that the input data has
* unexpected format (like missing fields or wrong field types). * 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) extends RuntimeException(message, cause)
object ParseError { object ParseError {
/** Wraps a [[io.circe.Error]] into a more user-friendly [[ParseError]]. */ /** Wraps a parser exception into a more user-friendly [[ParseError]]. */
def apply(error: io.circe.Error): ParseError = { def apply(error: Throwable): ParseError = {
val errorMessage = ParseError(error.getMessage, error)
implicitly[Show[io.circe.Error]].show(error)
ParseError(errorMessage, error)
} }
} }

View File

@ -1,9 +1,9 @@
package org.enso.yaml package org.enso.yaml
import io.circe.yaml.{Parser, Printer} import org.yaml.snakeyaml.nodes.Tag
import io.circe.{yaml, Decoder, Encoder} import org.yaml.snakeyaml.{DumperOptions, Yaml}
import java.io.FileReader import java.io.{FileReader, StringReader}
import java.nio.file.Path import java.nio.file.Path
import scala.util.{Try, Using} import scala.util.{Try, Using}
@ -13,23 +13,29 @@ object YamlHelper {
/** Parses a string representation of a YAML configuration of type `R`. */ /** Parses a string representation of a YAML configuration of type `R`. */
def parseString[R]( def parseString[R](
yamlString: String yamlString: String
)(implicit decoder: Decoder[R]): Either[ParseError, R] = )(implicit decoder: YamlDecoder[R]): Either[ParseError, R] = {
yaml.parser val snakeYaml = new org.yaml.snakeyaml.Yaml()
.parse(yamlString) Try(snakeYaml.compose(new StringReader(yamlString))).toEither
.flatMap(_.as[R]) .flatMap(decoder.decode(_))
.left .left
.map(ParseError(_)) .map(ParseError(_))
}
/** Tries to load and parse a YAML file at the provided path. */ /** 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 => Using(new FileReader(path.toFile)) { reader =>
Parser.default val snakeYaml = new org.yaml.snakeyaml.Yaml()
.parse(reader) Try(snakeYaml.compose(reader))
.flatMap(_.as[R]) .flatMap(decoder.decode(_).toTry)
.toTry
}.flatten }.flatten
/** Saves a YAML representation of an object into a string. */ /** Saves a YAML representation of an object into a string. */
def toYaml[A](obj: A)(implicit encoder: Encoder[A]): String = def toYaml[A](obj: A)(implicit encoder: YamlEncoder[A]): String = {
Printer.spaces2.copy(preserveOrder = true).pretty(encoder(obj)) 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 { "not allow invalid version combinations for libraries" in {
val parsed = EditionSerialization.parseYamlString( val parsed = EditionSerialization.parseYamlString(
"""extends: foo """extends: foo
@ -91,7 +108,7 @@ class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside {
|""".stripMargin |""".stripMargin
) )
inside(parsed) { case Failure(exception) => 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( val parsed2 = EditionSerialization.parseYamlString(
@ -102,7 +119,9 @@ class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside {
|""".stripMargin |""".stripMargin
) )
inside(parsed2) { case Failure(exception) => 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.cli.OS
import org.enso.distribution.FileSystem import org.enso.distribution.FileSystem
import org.enso.downloader.archive.TarGzWriter import org.enso.downloader.archive.TarGzWriter
import org.enso.editions.EditionSerialization.editionEncoder
import org.enso.editions.Editions.RawEdition import org.enso.editions.Editions.RawEdition
import org.enso.editions.{Editions, LibraryName} import org.enso.editions.{Editions, LibraryName}
import org.enso.pkg.{Package, PackageManager} import org.enso.pkg.{Package, PackageManager}

View File

@ -1,10 +1,12 @@
package org.enso.librarymanager.published.repository 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.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 import scala.util.Try
/** The manifest file containing metadata related to a published library. /** The manifest file containing metadata related to a published library.
@ -41,40 +43,73 @@ object LibraryManifest {
val description = "description" val description = "description"
} }
/** A [[Decoder]] instance for parsing [[LibraryManifest]]. */ implicit val yamlDecoder: YamlDecoder[LibraryManifest] =
implicit val decoder: Decoder[LibraryManifest] = { json => 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 { for {
archives <- json.get[Seq[String]](Fields.archives) archives <- kv
dependencies <- json.getOrElse[Seq[LibraryName]](Fields.dependencies)( .get(Fields.archives)
Seq() .map(archivesDecoder.decode(_))
) .getOrElse(Right(Seq.empty))
tagLine <- json.get[Option[String]](Fields.tagLine) dependencies <- kv
description <- json.get[Option[String]](Fields.description) .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( } yield LibraryManifest(
archives = archives, archives,
dependencies = dependencies, dependencies,
tagLine = tagLine, tagLine,
description = description description
) )
case _ =>
Left(new YAMLException("Unexpected library manifest definition"))
}
} }
/** An [[Encoder]] instance for parsing [[LibraryManifest]]. */ implicit val yamlEncoder: YamlEncoder[LibraryManifest] =
implicit val encoder: Encoder[LibraryManifest] = { manifest => new YamlEncoder[LibraryManifest] {
val baseFields = Seq( override def encode(value: LibraryManifest): AnyRef = {
Fields.archives -> manifest.archives.asJson, val archivesEncoder = implicitly[YamlEncoder[Seq[String]]]
Fields.dependencies -> manifest.dependencies.asJson val dependenciesEncoder = implicitly[YamlEncoder[Seq[LibraryName]]]
val elements = new util.ArrayList[(String, Object)]()
if (value.archives.nonEmpty)
elements.add(
(Fields.archives, archivesEncoder.encode(value.archives))
) )
if (value.dependencies.nonEmpty)
val allFields = baseFields ++ elements.add(
manifest.tagLine.map(Fields.tagLine -> _.asJson).toSeq ++ (
manifest.description.map(Fields.description -> _.asJson).toSeq Fields.dependencies,
dependenciesEncoder.encode(value.dependencies)
Json.obj(allFields: _*) )
)
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 */ /** Parser the provided string and returns a LibraryManifest, if valid */
def fromYaml(yamlString: String): Try[LibraryManifest] = { 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 /** 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._
import io.circe.syntax._ import io.circe.syntax._
import org.enso.editions.LibraryName 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. /** The description of component groups provided by the package.
* *
@ -45,6 +50,54 @@ object ComponentGroups {
json.getOrElse[List[ExtendedComponentGroup]](Fields.Extends)(List()) json.getOrElse[List[ExtendedComponentGroup]](Fields.Extends)(List())
} yield ComponentGroups(newGroups, extendsGroups) } 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. /** The definition of a single component group.
@ -64,11 +117,88 @@ object ComponentGroup {
/** Fields for use when serializing the [[ComponentGroup]]. */ /** Fields for use when serializing the [[ComponentGroup]]. */
private object Fields { private object Fields {
val Group = "group"
val Color = "color" val Color = "color"
val Icon = "icon" val Icon = "icon"
val Exports = "exports" 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]]. */ /** [[Encoder]] instance for the [[ComponentGroup]]. */
implicit val encoder: Encoder[ComponentGroup] = { componentGroup => implicit val encoder: Encoder[ComponentGroup] = { componentGroup =>
val color = componentGroup.color.map(Fields.Color -> _.asJson) val color = componentGroup.color.map(Fields.Color -> _.asJson)
@ -131,9 +261,94 @@ object ExtendedComponentGroup {
/** Fields for use when serializing the [[ExtendedComponentGroup]]. */ /** Fields for use when serializing the [[ExtendedComponentGroup]]. */
private object Fields { private object Fields {
val Group = "group"
val Exports = "exports" 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]]. */ /** [[Encoder]] instance for the [[ExtendedComponentGroup]]. */
implicit val encoder: Encoder[ExtendedComponentGroup] = { implicit val encoder: Encoder[ExtendedComponentGroup] = {
extendedComponentGroup => extendedComponentGroup =>
@ -200,9 +415,63 @@ case class Component(name: String, shortcut: Option[Shortcut])
object Component { object Component {
object Fields { object Fields {
val Name = "name"
val Shortcut = "shortcut" 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]]. */ /** [[Encoder]] instance for the [[Component]]. */
implicit val encoder: Encoder[Component] = { component => implicit val encoder: Encoder[Component] = { component =>
component.shortcut match { component.shortcut match {
@ -256,6 +525,44 @@ object Component {
case class Shortcut(key: String) case class Shortcut(key: String)
object Shortcut { 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]]. */ /** [[Encoder]] instance for the [[Shortcut]]. */
implicit val encoder: Encoder[Shortcut] = { shortcut => implicit val encoder: Encoder[Shortcut] = { shortcut =>
shortcut.key.asJson shortcut.key.asJson
@ -307,6 +614,31 @@ object GroupReference {
case _ => case _ =>
None 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. /** The module name.
@ -316,6 +648,20 @@ object GroupReference {
case class GroupName(name: String) case class GroupName(name: String)
object GroupName { 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. */ /** Create a [[GroupName]] from its components. */
def fromComponents(item: String, items: List[String]): GroupName = def fromComponents(item: String, items: List[String]): GroupName =
GroupName((item :: items).mkString(LibraryName.separator.toString)) GroupName((item :: items).mkString(LibraryName.separator.toString))

View File

@ -1,20 +1,16 @@
package org.enso.pkg package org.enso.pkg
import io.circe._ import org.yaml.snakeyaml.nodes.Tag
import io.circe.syntax._
import io.circe.yaml.{Parser, Printer}
import org.enso.semver.SemVer import org.enso.semver.SemVer
import org.enso.editions.EditionSerialization._ import org.enso.editions.{EditionName, Editions}
import org.enso.editions.{
DefaultEnsoVersion,
EditionName,
Editions,
EnsoVersion,
SemVerEnsoVersion
}
import org.enso.pkg.validation.NameValidation 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 import scala.util.Try
/** Contact information to a user. /** Contact information to a user.
@ -46,34 +42,37 @@ object Contact {
val Email = "email" val Email = "email"
} }
/** [[Encoder]] instance for the [[Contact]]. */ implicit val decoderSnake: YamlDecoder[Contact] =
implicit val encoder: Encoder[Contact] = { contact => new YamlDecoder[Contact] {
val name = contact.name.map(Fields.Name -> _.asJson) override def decode(node: Node): Either[Throwable, Contact] = node match {
val email = contact.email.map(Fields.Email -> _.asJson) case mappingNode: MappingNode =>
Json.obj((name.toSeq ++ email.toSeq): _*) 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 encoderSnake: YamlEncoder[Contact] =
*/ new YamlEncoder[Contact] {
implicit val decoder: Decoder[Contact] = { json => override def encode(value: Contact) = {
def verifyAtLeastOneDefined( val elements = new util.ArrayList[(String, Object)]()
name: Option[String], value.name
email: Option[String] .map((Fields.Name, _))
): Either[DecodingFailure, Unit] = .foreach(elements.add)
if (name.isEmpty && email.isEmpty) value.email
Left( .map((Fields.Email, _))
DecodingFailure( .foreach(elements.add)
"At least one of the fields `name`, `email` must be defined.", toMap(elements)
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)
} }
} }
@ -114,8 +113,14 @@ case class Config(
) { ) {
/** Converts the configuration into a YAML representation. */ /** Converts the configuration into a YAML representation. */
def toYaml: String = def toYaml: String = {
Printer.spaces2.copy(preserveOrder = true).pretty(Config.encoder(this)) 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. */ /** @return the module of name. */
def moduleName: String = def moduleName: String =
@ -125,121 +130,182 @@ case class Config(
object Config { object Config {
val defaultNamespace: String = "local" val DefaultNamespace: String = "local"
val DefaultVersion: String = "dev"
val DefaultLicense: String = ""
val DefaultPreferLocalLibraries = false
private object JsonFields { private object JsonFields {
val name: String = "name" val Name: String = "name"
val normalizedName: String = "normalized-name" val NormalizedName: String = "normalized-name"
val version: String = "version" val Version: String = "version"
val ensoVersion: String = "enso-version" val EnsoVersion: String = "enso-version"
val license: String = "license" val License: String = "license"
val author: String = "authors" val Author: String = "authors"
val namespace: String = "namespace" val Namespace: String = "namespace"
val maintainer: String = "maintainers" val Maintainer: String = "maintainers"
val edition: String = "edition" val Edition: String = "edition"
val preferLocalLibraries = "prefer-local-libraries" val PreferLocalLibraries = "prefer-local-libraries"
val componentGroups = "component-groups" val ComponentGroups = "component-groups"
} }
implicit val decoder: Decoder[Config] = { json => implicit val yamlDecoder: YamlDecoder[Config] =
for { new YamlDecoder[Config] {
name <- json.get[String](JsonFields.name) override def decode(node: Node): Either[Throwable, Config] = node match {
normalizedName <- json.get[Option[String]](JsonFields.normalizedName) case mappingNode: MappingNode =>
namespace <- json.getOrElse[String](JsonFields.namespace)( val clazzMap = mappingKV(mappingNode)
defaultNamespace val stringDecoder = implicitly[YamlDecoder[String]]
) val normalizedNameDecoder =
version <- json.getOrElse[String](JsonFields.version)("dev") implicitly[YamlDecoder[Option[String]]]
ensoVersion <- json.get[Option[EnsoVersion]](JsonFields.ensoVersion) val contactDecoder = implicitly[YamlDecoder[List[Contact]]]
rawEdition <- json val editionNameDecoder = implicitly[YamlDecoder[EditionName]]
.get[EditionName](JsonFields.edition) val editionDecoder =
.map(x => Left(x.name)) implicitly[YamlDecoder[Option[Editions.RawEdition]]]
.orElse( val booleanDecoder = implicitly[YamlDecoder[Boolean]]
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 -> _)
val componentGroups = val componentGroups =
Option.unless( implicitly[YamlDecoder[Option[ComponentGroups]]]
config.componentGroups.isEmpty for {
)( name <- clazzMap
JsonFields.componentGroups -> config.componentGroups.asJson .get(JsonFields.Name)
.toRight(
new YAMLException(s"Missing '${JsonFields.Name}' field")
) )
.flatMap(stringDecoder.decode)
val normalizedName = config.normalizedName.map(value => normalizedName <- clazzMap
JsonFields.normalizedName -> value.asJson .get(JsonFields.NormalizedName)
.map(normalizedNameDecoder.decode)
.getOrElse(Right(None))
namespace <- clazzMap
.get(JsonFields.Namespace)
.map(stringDecoder.decode)
.getOrElse(Right(DefaultNamespace))
version <- clazzMap
.get(JsonFields.Version)
.map(stringDecoder.decode)
.getOrElse(Right(DefaultVersion))
license <- clazzMap
.get(JsonFields.License)
.map(stringDecoder.decode)
.getOrElse(Right(DefaultLicense))
authors <- clazzMap
.get(JsonFields.Author)
.map(contactDecoder.decode)
.getOrElse(Right(Nil))
maintainers <- clazzMap
.get(JsonFields.Maintainer)
.map(contactDecoder.decode)
.getOrElse(Right(Nil))
rawEdition = clazzMap
.get(JsonFields.Edition)
.flatMap(x => editionNameDecoder.decode(x).toOption.map(Left(_)))
.getOrElse(
clazzMap
.get(JsonFields.Edition)
.map(editionDecoder.decode)
.getOrElse(Right(None))
) )
.asInstanceOf[Either[EditionName, Option[Editions.RawEdition]]]
val overrides = edition <- rawEdition.fold(
Seq(JsonFields.name -> config.name.asJson) ++ editionName =>
normalizedName.toSeq ++ Right(
Seq( Some(Editions.Raw.Edition(parent = Some(editionName.name)))
JsonFields.namespace -> config.namespace.asJson, ),
JsonFields.version -> config.version.asJson, r => Right(r)
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: _*
) )
preferLocalLibraries <- clazzMap
overridesObject.asJson .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. */ implicit val encoderSnake: YamlEncoder[Config] =
def fromYaml(yamlString: String): Try[Config] = { new YamlEncoder[Config] {
yaml.parser.parse(yamlString).flatMap(_.as[Config]).toTry 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 */ /** Tries to parse the [[Config]] directly from the Reader */
def fromYaml(reader: Reader): Try[Config] = { 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. /** Creates a simple edition that just defines the provided engine version.
@ -262,37 +328,4 @@ object Config {
repositories = Map(), repositories = Map(),
libraries = 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 package org.enso.pkg
import cats.Show
import org.enso.editions.{Editions, LibraryName} import org.enso.editions.{Editions, LibraryName}
import org.enso.filesystem.FileSystem import org.enso.filesystem.FileSystem
import org.enso.pkg.validation.NameValidation import org.enso.pkg.validation.NameValidation
@ -350,15 +349,6 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
result.recoverWith { result.recoverWith {
case packageLoadingException: PackageManager.PackageLoadingException => case packageLoadingException: PackageManager.PackageLoadingException =>
Failure(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 => case otherError =>
Failure( Failure(
PackageManager.PackageLoadingFailure( PackageManager.PackageLoadingFailure(

View File

@ -1,12 +1,11 @@
package org.enso.pkg package org.enso.pkg
import cats.Show
import io.circe.{DecodingFailure, Json}
import org.enso.semver.SemVer import org.enso.semver.SemVer
import org.enso.editions.LibraryName import org.enso.editions.LibraryName
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.{Inside, OptionValues} import org.scalatest.{Inside, OptionValues}
import org.yaml.snakeyaml.error.YAMLException
import scala.util.Failure import scala.util.Failure
@ -17,22 +16,6 @@ class ConfigSpec
with OptionValues { with OptionValues {
"Config" should { "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 { "deserialize the serialized representation to the original value" in {
val config = Config( val config = Config(
name = "placeholder", name = "placeholder",
@ -194,8 +177,8 @@ class ConfigSpec
val parsed = Config.fromYaml(config) val parsed = Config.fromYaml(config)
parsed match { parsed match {
case Failure(f: DecodingFailure) => case Failure(failure: YAMLException) =>
Show[DecodingFailure].show(f) should include( failure.getMessage should include(
"Failed to decode 'Group 1' as a module reference" "Failed to decode 'Group 1' as a module reference"
) )
case unexpected => case unexpected =>
@ -254,9 +237,9 @@ class ConfigSpec
|""".stripMargin |""".stripMargin
val parsed = Config.fromYaml(config) val parsed = Config.fromYaml(config)
parsed match { parsed match {
case Failure(f: DecodingFailure) => case Failure(failure: YAMLException) =>
Show[DecodingFailure].show(f) should include( failure.getMessage should equal(
"Failed to decode shortcut" "Failed to decode shortcut. Expected a string value, got a sequence"
) )
case unexpected => case unexpected =>
fail(s"Unexpected result: $unexpected") fail(s"Unexpected result: $unexpected")
@ -273,9 +256,9 @@ class ConfigSpec
|""".stripMargin |""".stripMargin
val parsed = Config.fromYaml(config) val parsed = Config.fromYaml(config)
parsed match { parsed match {
case Failure(f: DecodingFailure) => case Failure(failure: YAMLException) =>
Show[DecodingFailure].show(f) should include( failure.getMessage should equal(
"Failed to decode component group" "Failed to decode component group. Expected a mapping, got a sequence"
) )
case unexpected => case unexpected =>
fail(s"Unexpected result: $unexpected") fail(s"Unexpected result: $unexpected")
@ -293,9 +276,9 @@ class ConfigSpec
|""".stripMargin |""".stripMargin
val parsed = Config.fromYaml(config) val parsed = Config.fromYaml(config)
parsed match { parsed match {
case Failure(f: DecodingFailure) => case Failure(failure: YAMLException) =>
Show[DecodingFailure].show(f) should include( failure.getMessage should equal(
"Failed to decode exported component" "Failed to decode exported component 'one'"
) )
case unexpected => case unexpected =>
fail(s"Unexpected result: $unexpected") fail(s"Unexpected result: $unexpected")

View File

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

View File

@ -1,6 +1,5 @@
package org.enso.projectmanager.service.config package org.enso.projectmanager.service.config
import io.circe.Json
import org.enso.semver.SemVer import org.enso.semver.SemVer
import org.enso.editions.{DefaultEnsoVersion, EnsoVersion, SemVerEnsoVersion} import org.enso.editions.{DefaultEnsoVersion, EnsoVersion, SemVerEnsoVersion}
import org.enso.projectmanager.control.core.CovariantFlatMap import org.enso.projectmanager.control.core.CovariantFlatMap
@ -36,7 +35,7 @@ class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
key: String, key: String,
value: String value: String
): F[GlobalConfigServiceFailure, Unit] = Sync[F].blockingOp { ): F[GlobalConfigServiceFailure, Unit] = Sync[F].blockingOp {
configurationManager.updateConfigRaw(key, Json.fromString(value)) configurationManager.updateConfigRaw(key, value)
}.recoverAccessErrors }.recoverAccessErrors
/** @inheritdoc */ /** @inheritdoc */

View File

@ -1,6 +1,5 @@
package org.enso.runtimeversionmanager.config package org.enso.runtimeversionmanager.config
import io.circe.Json
import org.enso.semver.SemVer import org.enso.semver.SemVer
import org.enso.distribution.DistributionManager import org.enso.distribution.DistributionManager
import org.enso.distribution.config.InvalidConfigError import org.enso.distribution.config.InvalidConfigError
@ -27,16 +26,16 @@ class GlobalConfigurationManagerSpec
"GlobalConfigurationManager" should { "GlobalConfigurationManager" should {
"allow to edit and remove known keys" in { "allow to edit and remove known keys" in {
val configurationManager = makeConfigManager() val configurationManager = makeConfigManager()
val value = Json.fromInt(42) val value = 42.toString
configurationManager.updateConfigRaw("unknown-key", value) configurationManager.updateConfigRaw("unknown-key", value)
configurationManager.getConfig configurationManager.getConfig
.findByKey("unknown-key") should not be defined .findByKey("unknown-key") should not be defined
val newEmail = Json.fromString("foo@bar.com") val newEmail = "foo@bar.com"
configurationManager.getConfig configurationManager.getConfig
.findByKey("author.email") should not be defined .findByKey("author.email") should not be defined
configurationManager.updateConfigRaw("author.email", newEmail) configurationManager.updateConfigRaw("author.email", newEmail)
configurationManager.getConfig configurationManager.getConfig
.findByKey("author.email") shouldEqual newEmail.asString .findByKey("author.email") shouldEqual Some(newEmail)
} }
"not allow saving an invalid config" in { "not allow saving an invalid config" in {
@ -44,7 +43,7 @@ class GlobalConfigurationManagerSpec
intercept[InvalidConfigError] { intercept[InvalidConfigError] {
configurationManager.updateConfigRaw( configurationManager.updateConfigRaw(
"default.enso-version", "default.enso-version",
Json.fromString("invalid-version") "invalid-version"
) )
} }
} }

View File

@ -1,18 +1,19 @@
package org.enso.runtimeversionmanager.components package org.enso.runtimeversionmanager.components
import java.io.FileReader import java.io.{FileReader, StringReader}
import java.nio.file.Path import java.nio.file.Path
import cats.Show import org.enso
import io.circe.yaml.Parser
import io.circe.{yaml, Decoder}
import org.enso.semver.SemVer import org.enso.semver.SemVer
import org.enso.cli.OS import org.enso.cli.OS
import org.enso.semver.SemVerJson._ import org.enso.semver.SemVerYaml._
import org.enso.runtimeversionmanager.components.Manifest.{ import org.enso.runtimeversionmanager.components.Manifest.{
JVMOption, JVMOption,
RequiredInstallerVersions RequiredInstallerVersions
} }
import org.enso.runtimeversionmanager.components 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} import scala.util.{Failure, Try, Using}
@ -70,6 +71,39 @@ object Manifest {
*/ */
case class RequiredInstallerVersions(launcher: SemVer, projectManager: SemVer) 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. /** Defines the name under which the manifest is included in the releases.
*/ */
val DEFAULT_MANIFEST_NAME = "manifest.yaml" val DEFAULT_MANIFEST_NAME = "manifest.yaml"
@ -109,25 +143,32 @@ object Manifest {
object JVMOption { object JVMOption {
private object Fields { private object Fields {
val os = "os" val Os = "os"
val value = "value" val Value = "value"
}
/** [[Decoder]] instance that allows to parse the [[JVMOption]] from the
* YAML manifest.
*/
implicit val decoder: Decoder[JVMOption] = { json =>
val hasOSKey = json.keys.exists { keyList: Iterable[String] =>
keyList.toSeq.contains(Fields.os)
} }
implicit val yamlDecoder: YamlDecoder[JVMOption] =
new YamlDecoder[JVMOption] {
override def decode(node: Node): Either[Throwable, JVMOption] = {
node match {
case node: MappingNode =>
val bindings = mappingKV(node)
val stringDecoder = implicitly[YamlDecoder[String]]
val OSdecoder = implicitly[YamlDecoder[OS]]
for { for {
value <- json.get[String](Fields.value) value <- bindings
osRestriction <- .get(Fields.Value)
if (hasOSKey) json.get[OS](Fields.os).map(Some(_)) else Right(None) .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) } yield JVMOption(value, osRestriction)
} }
} }
}
}
/** Tries to load the manifest at the given path. /** Tries to load the manifest at the given path.
* *
@ -135,10 +176,11 @@ object Manifest {
*/ */
def load(path: Path): Try[Manifest] = def load(path: Path): Try[Manifest] =
Using(new FileReader(path.toFile)) { reader => Using(new FileReader(path.toFile)) { reader =>
Parser.default val snakeYaml = new org.yaml.snakeyaml.Yaml()
.parse(reader) Try(snakeYaml.compose(reader))
.flatMap(_.as[Manifest]) .flatMap(
.toTry implicitly[enso.yaml.YamlDecoder[Manifest]].decode(_).toTry
)
}.flatten.recoverWith { error => }.flatten.recoverWith { error =>
Failure(ManifestLoadingError.fromThrowable(error)) Failure(ManifestLoadingError.fromThrowable(error))
} }
@ -148,9 +190,11 @@ object Manifest {
* Returns None if the definition cannot be parsed. * Returns None if the definition cannot be parsed.
*/ */
def fromYaml(yamlString: String): Try[Manifest] = { def fromYaml(yamlString: String): Try[Manifest] = {
yaml.parser val snakeYaml = new org.yaml.snakeyaml.Yaml()
.parse(yamlString) Try(snakeYaml.compose(new StringReader(yamlString))).toEither
.flatMap(_.as[Manifest]) .flatMap(implicitly[enso.yaml.YamlDecoder[Manifest]].decode(_))
.left
.map(ParseError(_))
.toTry .toTry
.recoverWith { error => .recoverWith { error =>
Failure(ManifestLoadingError.fromThrowable(error)) Failure(ManifestLoadingError.fromThrowable(error))
@ -176,49 +220,68 @@ object Manifest {
*/ */
def fromThrowable(throwable: Throwable): ManifestLoadingError = def fromThrowable(throwable: Throwable): ManifestLoadingError =
throwable match { 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 => case other =>
ManifestLoadingError(s"Could not load the manifest: $other", other) ManifestLoadingError(s"Could not load the manifest: $other", other)
} }
} }
object Fields { object Fields {
val minimumLauncherVersion = "minimum-launcher-version" val MinimumLauncherVersion = "minimum-launcher-version"
val minimumProjectManagerVersion = "minimum-project-manager-version" val MinimumProjectManagerVersion = "minimum-project-manager-version"
val jvmOptions = "jvm-options" val JvmOptions = "jvm-options"
val graalVMVersion = "graal-vm-version" val GraalVMVersion = "graal-vm-version"
val graalJavaVersion = "graal-java-version" val GraalJavaVersion = "graal-java-version"
val brokenMark = "broken" 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 { for {
minimumLauncherVersion <- json.get[SemVer](Fields.minimumLauncherVersion) requiredInstallerVersions <- requiredInstallerVersionsDecoder
minimumProjectManagerVersion <- json.get[SemVer]( .decode(node)
Fields.minimumProjectManagerVersion graalVMVersion <- bindings
.get(Fields.GraalVMVersion)
.toRight(
new YAMLException(
s"Required `${Fields.GraalVMVersion}`field is missing"
) )
graalVMVersion <- json.get[String](Fields.graalVMVersion) )
graalJavaVersion <- .flatMap(stringDecoder.decode)
json graalJavaVersion <- bindings
.get[String](Fields.graalJavaVersion) .get(Fields.GraalJavaVersion)
.orElse(json.get[Int](Fields.graalJavaVersion).map(_.toString)) .toRight(
jvmOptions <- json.getOrElse[Seq[JVMOption]](Fields.jvmOptions)(Seq()) new YAMLException(
broken <- json.getOrElse[Boolean](Fields.brokenMark)(false) 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( } yield Manifest(
requiredInstallerVersions = RequiredInstallerVersions( requiredInstallerVersions,
launcher = minimumLauncherVersion, graalVMVersion,
projectManager = minimumProjectManagerVersion graalJavaVersion,
), jvmOptions,
graalVMVersion = graalVMVersion, brokenMark
graalJavaVersion = graalJavaVersion,
jvmOptions = jvmOptions,
brokenMark = broken
) )
} }
} }
}
}

View File

@ -524,7 +524,7 @@ class RuntimeVersionManager(
) )
) { writer => ) { writer =>
writer.newLine() writer.newLine()
writer.write(s"${Manifest.Fields.brokenMark}: true\n") writer.write(s"${Manifest.Fields.NrokenMark}: true\n")
}.get }.get
} catch { } catch {
case ex: Exception => case ex: Exception =>
@ -733,7 +733,8 @@ class RuntimeVersionManager(
private def loadAndCheckEngineManifest( private def loadAndCheckEngineManifest(
path: Path path: Path
): Try[Manifest] = { ): 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)) { if (!isEngineVersionCompatibleWithThisInstaller(manifest)) {
Failure( Failure(
UpgradeRequiredError( UpgradeRequiredError(

View File

@ -26,7 +26,7 @@ object SemVerJson {
Left( Left(
DecodingFailure( DecodingFailure(
s"`$version` is not a valid semantic versioning string.", 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. /** [[Encoder]] instance allowing to serialize semantic versioning strings.
*/ */
implicit val semverEncoder: Encoder[SemVer] = _.toString.asJson 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 8DE3C509911C2AB8D430D76BDB6FD401A8262BC700DA927E97A6CC9055B331C9
69A14ED5CFDD2E721976A524D46407779F7851C84B8ED9481CC6177B9E3C99A1 1CCB55F023131497A0E6A16BB5B2D63E5D842572D8638017816EF1D5474B0169
0 0

View File

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

View File

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