diff --git a/build.sbt b/build.sbt index f563a3455610..ad1ad91929be 100644 --- a/build.sbt +++ b/build.sbt @@ -290,6 +290,7 @@ lazy val enso = (project in file(".")) `syntax-rust-definition`, `text-buffer`, yaml, + `scala-yaml`, pkg, cli, `task-progress-notifications`, @@ -417,10 +418,10 @@ val catsVersion = "2.9.0" // === Circe ================================================================== val circeVersion = "0.14.7" -val circeYamlVersion = "0.15.1" val circeGenericExtrasVersion = "0.14.3" val circe = Seq("circe-core", "circe-generic", "circe-parser") .map("io.circe" %% _ % circeVersion) +val snakeyamlVersion = "2.2" // === Commons ================================================================ @@ -751,7 +752,16 @@ lazy val yaml = (project in file("lib/java/yaml")) frgaalJavaCompilerSetting, version := "0.1", libraryDependencies ++= Seq( - "io.circe" %% "circe-yaml" % circeYamlVersion % "provided" + "org.yaml" % "snakeyaml" % snakeyamlVersion % "provided" + ) + ) + +lazy val `scala-yaml` = (project in file("lib/scala/yaml")) + .configs(Test) + .settings( + frgaalJavaCompilerSetting, + libraryDependencies ++= Seq( + "org.yaml" % "snakeyaml" % snakeyamlVersion % "provided" ) ) @@ -762,7 +772,8 @@ lazy val pkg = (project in file("lib/scala/pkg")) version := "0.1", libraryDependencies ++= Seq( "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided", - "io.circe" %% "circe-yaml" % circeYamlVersion % "provided", + "io.circe" %% "circe-core" % circeVersion % "provided", + "org.yaml" % "snakeyaml" % snakeyamlVersion % "provided", "org.scalatest" %% "scalatest" % scalatestVersion % Test, "org.apache.commons" % "commons-compress" % commonsCompressVersion ) @@ -930,10 +941,12 @@ lazy val cli = project version := "0.1", libraryDependencies ++= circe ++ Seq( "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion, + "org.yaml" % "snakeyaml" % snakeyamlVersion % "provided", "org.scalatest" %% "scalatest" % scalatestVersion % Test ), Test / parallelExecution := false ) + .dependsOn(`scala-yaml`) lazy val `task-progress-notifications` = project .in(file("lib/scala/task-progress-notifications")) @@ -1461,11 +1474,11 @@ lazy val `polyglot-api` = project "runtime-fat-jar" ) / Compile / fullClasspath).value, libraryDependencies ++= Seq( + "io.circe" %% "circe-core" % circeVersion % "provided", "org.graalvm.sdk" % "polyglot-tck" % graalMavenPackagesVersion % "provided", "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided", "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion, "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % jsoniterVersion, - "io.circe" %% "circe-yaml" % circeYamlVersion % "provided", // as required by `pkg` and `editions` "com.google.flatbuffers" % "flatbuffers-java" % flatbuffersVersion, "org.scalatest" %% "scalatest" % scalatestVersion % Test, "org.scalacheck" %% "scalacheck" % scalacheckVersion % Test @@ -2764,7 +2777,7 @@ lazy val `distribution-manager` = project resolvers += Resolver.bintrayRepo("gn0s1s", "releases"), libraryDependencies ++= Seq( "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion, - "io.circe" %% "circe-yaml" % circeYamlVersion, + "org.yaml" % "snakeyaml" % snakeyamlVersion, "commons-io" % "commons-io" % commonsIoVersion, "org.scalatest" %% "scalatest" % scalatestVersion % Test ) @@ -2944,7 +2957,8 @@ lazy val editions = project .settings( frgaalJavaCompilerSetting, libraryDependencies ++= Seq( - "io.circe" %% "circe-yaml" % circeYamlVersion % "provided", + "io.circe" %% "circe-core" % circeVersion % "provided", + "org.yaml" % "snakeyaml" % snakeyamlVersion % "provided", "org.scalatest" %% "scalatest" % scalatestVersion % Test ) ) @@ -2973,7 +2987,8 @@ lazy val semver = project .settings( frgaalJavaCompilerSetting, libraryDependencies ++= Seq( - "io.circe" %% "circe-yaml" % circeYamlVersion % "provided", + "io.circe" %% "circe-core" % circeVersion % "provided", + "org.yaml" % "snakeyaml" % snakeyamlVersion % "provided", "org.scalatest" %% "scalatest" % scalatestVersion % Test, "junit" % "junit" % junitVersion % Test, "com.github.sbt" % "junit-interface" % junitIfVersion % Test @@ -2996,6 +3011,7 @@ lazy val semver = project cleanFiles += baseDirectory.value / ".." / ".." / "distribution" / "editions" ) .dependsOn(testkit % Test) + .dependsOn(`scala-yaml`) lazy val downloader = (project in file("lib/scala/downloader")) .settings( @@ -3040,7 +3056,7 @@ lazy val `edition-uploader` = project .settings( frgaalJavaCompilerSetting, libraryDependencies ++= Seq( - "io.circe" %% "circe-yaml" % circeYamlVersion % "provided" + "io.circe" %% "circe-core" % circeVersion % "provided" ) ) .dependsOn(editions) diff --git a/distribution/engine/THIRD-PARTY/NOTICE b/distribution/engine/THIRD-PARTY/NOTICE index ffc957c8ae93..ba4a663793a4 100644 --- a/distribution/engine/THIRD-PARTY/NOTICE +++ b/distribution/engine/THIRD-PARTY/NOTICE @@ -211,16 +211,6 @@ The license file can be found at `licenses/APACHE2.0`. Copyright notices related to this dependency can be found in the directory `io.circe.circe-parser_2.13-0.14.7`. -'circe-yaml-common_2.13', licensed under the Apache-2.0, is distributed with the engine. -The license file can be found at `licenses/APACHE2.0`. -Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml-common_2.13-0.15.1`. - - -'circe-yaml_2.13', licensed under the Apache-2.0, is distributed with the engine. -The license file can be found at `licenses/APACHE2.0`. -Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml_2.13-0.15.1`. - - 'helidon-builder-api', licensed under the Apache 2.0, is distributed with the engine. The license file can be found at `licenses/APACHE2.0`. Copyright notices related to this dependency can be found in the directory `io.helidon.builder.helidon-builder-api-4.0.8`. diff --git a/distribution/engine/THIRD-PARTY/io.circe.circe-yaml-common_2.13-0.15.1/NOTICES b/distribution/engine/THIRD-PARTY/io.circe.circe-yaml-common_2.13-0.15.1/NOTICES deleted file mode 100644 index d390f204f1b3..000000000000 --- a/distribution/engine/THIRD-PARTY/io.circe.circe-yaml-common_2.13-0.15.1/NOTICES +++ /dev/null @@ -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. - */ diff --git a/distribution/engine/THIRD-PARTY/io.circe.circe-yaml_2.13-0.15.1/NOTICES b/distribution/engine/THIRD-PARTY/io.circe.circe-yaml_2.13-0.15.1/NOTICES deleted file mode 100644 index d390f204f1b3..000000000000 --- a/distribution/engine/THIRD-PARTY/io.circe.circe-yaml_2.13-0.15.1/NOTICES +++ /dev/null @@ -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. - */ diff --git a/distribution/launcher/THIRD-PARTY/NOTICE b/distribution/launcher/THIRD-PARTY/NOTICE index 08844d297c43..cb590dcc28a5 100644 --- a/distribution/launcher/THIRD-PARTY/NOTICE +++ b/distribution/launcher/THIRD-PARTY/NOTICE @@ -86,16 +86,6 @@ The license file can be found at `licenses/APACHE2.0`. Copyright notices related to this dependency can be found in the directory `io.circe.circe-parser_2.13-0.14.7`. -'circe-yaml-common_2.13', licensed under the Apache-2.0, is distributed with the launcher. -The license file can be found at `licenses/APACHE2.0`. -Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml-common_2.13-0.15.1`. - - -'circe-yaml_2.13', licensed under the Apache-2.0, is distributed with the launcher. -The license file can be found at `licenses/APACHE2.0`. -Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml_2.13-0.15.1`. - - 'commons-compress', licensed under the Apache-2.0, is distributed with the launcher. The license file can be found at `licenses/APACHE2.0`. Copyright notices related to this dependency can be found in the directory `org.apache.commons.commons-compress-1.23.0`. diff --git a/distribution/launcher/THIRD-PARTY/io.circe.circe-yaml-common_2.13-0.15.1/NOTICES b/distribution/launcher/THIRD-PARTY/io.circe.circe-yaml-common_2.13-0.15.1/NOTICES deleted file mode 100644 index d390f204f1b3..000000000000 --- a/distribution/launcher/THIRD-PARTY/io.circe.circe-yaml-common_2.13-0.15.1/NOTICES +++ /dev/null @@ -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. - */ diff --git a/distribution/launcher/THIRD-PARTY/io.circe.circe-yaml_2.13-0.15.1/NOTICES b/distribution/launcher/THIRD-PARTY/io.circe.circe-yaml_2.13-0.15.1/NOTICES deleted file mode 100644 index d390f204f1b3..000000000000 --- a/distribution/launcher/THIRD-PARTY/io.circe.circe-yaml_2.13-0.15.1/NOTICES +++ /dev/null @@ -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. - */ diff --git a/distribution/project-manager/THIRD-PARTY/NOTICE b/distribution/project-manager/THIRD-PARTY/NOTICE index 9f4467d6a222..9249355d5928 100644 --- a/distribution/project-manager/THIRD-PARTY/NOTICE +++ b/distribution/project-manager/THIRD-PARTY/NOTICE @@ -186,16 +186,6 @@ The license file can be found at `licenses/APACHE2.0`. Copyright notices related to this dependency can be found in the directory `io.circe.circe-parser_2.13-0.14.7`. -'circe-yaml-common_2.13', licensed under the Apache-2.0, is distributed with the project-manager. -The license file can be found at `licenses/APACHE2.0`. -Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml-common_2.13-0.15.1`. - - -'circe-yaml_2.13', licensed under the Apache-2.0, is distributed with the project-manager. -The license file can be found at `licenses/APACHE2.0`. -Copyright notices related to this dependency can be found in the directory `io.circe.circe-yaml_2.13-0.15.1`. - - 'helidon-builder-api', licensed under the Apache 2.0, is distributed with the project-manager. The license file can be found at `licenses/APACHE2.0`. Copyright notices related to this dependency can be found in the directory `io.helidon.builder.helidon-builder-api-4.0.8`. diff --git a/distribution/project-manager/THIRD-PARTY/io.circe.circe-yaml-common_2.13-0.15.1/NOTICES b/distribution/project-manager/THIRD-PARTY/io.circe.circe-yaml-common_2.13-0.15.1/NOTICES deleted file mode 100644 index d390f204f1b3..000000000000 --- a/distribution/project-manager/THIRD-PARTY/io.circe.circe-yaml-common_2.13-0.15.1/NOTICES +++ /dev/null @@ -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. - */ diff --git a/distribution/project-manager/THIRD-PARTY/io.circe.circe-yaml_2.13-0.15.1/NOTICES b/distribution/project-manager/THIRD-PARTY/io.circe.circe-yaml_2.13-0.15.1/NOTICES deleted file mode 100644 index d390f204f1b3..000000000000 --- a/distribution/project-manager/THIRD-PARTY/io.circe.circe-yaml_2.13-0.15.1/NOTICES +++ /dev/null @@ -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. - */ diff --git a/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala b/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala index 6716477dcd53..3339c9683571 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala @@ -2,7 +2,6 @@ package org.enso.launcher import java.nio.file.Path import com.typesafe.scalalogging.Logger -import io.circe.Json import org.enso.semver.SemVer import org.enso.distribution.config.DefaultVersion import org.enso.editions.updater.EditionManager @@ -368,7 +367,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) { s"(${configurationManager.configLocation.toAbsolutePath})." ) } else { - configurationManager.updateConfigRaw(key, Json.fromString(value)) + configurationManager.updateConfigRaw(key, value) InfoLogger.info( s"""Key `$key` set to "$value" in the global configuration file """ + s"(${configurationManager.configLocation.toAbsolutePath})." diff --git a/engine/launcher/src/main/scala/org/enso/launcher/releases/fallback/staticwebsite/FallbackManifest.scala b/engine/launcher/src/main/scala/org/enso/launcher/releases/fallback/staticwebsite/FallbackManifest.scala index 151c36ebcf5c..c9fadf82b9c3 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/releases/fallback/staticwebsite/FallbackManifest.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/releases/fallback/staticwebsite/FallbackManifest.scala @@ -1,7 +1,9 @@ package org.enso.launcher.releases.fallback.staticwebsite -import io.circe.Decoder +import org.enso.yaml.YamlDecoder +import org.yaml.snakeyaml.nodes.{MappingNode, Node} +import java.io.StringReader import scala.util.Try /** Manifest of the fallback mechanism. @@ -13,6 +15,23 @@ case class FallbackManifest(enabled: Boolean) object FallbackManifest { + implicit val yamlDecoder: YamlDecoder[FallbackManifest] = + new YamlDecoder[FallbackManifest] { + override def decode(node: Node) = { + node match { + case node: MappingNode => + val booleanDecoder = implicitly[YamlDecoder[Boolean]] + val bindings = mappingKV(node) + for { + enabled <- bindings + .get(Fields.enabled) + .map(booleanDecoder.decode(_)) + .getOrElse(Right(false)) + } yield FallbackManifest(enabled) + } + } + } + /** Defines a part of the URL scheme of the fallback mechanism - the name of * manifest file. * @@ -25,17 +44,10 @@ object FallbackManifest { val enabled = "enabled" } - /** [[Decoder]] instance for [[FallbackManifest]]. - * - * It should always remain backwards compatible, since the fallback mechanism - * must work for all released launcher versions. - */ - implicit val decoder: Decoder[FallbackManifest] = { json => - for { - enabled <- json.get[Boolean](Fields.enabled) - } yield FallbackManifest(enabled) + def parseString(yamlString: String): Try[FallbackManifest] = { + val snakeYaml = new org.yaml.snakeyaml.Yaml() + Try(snakeYaml.compose(new StringReader(yamlString))).toEither + .flatMap(implicitly[YamlDecoder[FallbackManifest]].decode(_)) + .toTry } - - def parseString(string: String): Try[FallbackManifest] = - io.circe.yaml.parser.parse(string).flatMap(_.as[FallbackManifest]).toTry } diff --git a/engine/launcher/src/main/scala/org/enso/launcher/releases/launcher/LauncherManifest.scala b/engine/launcher/src/main/scala/org/enso/launcher/releases/launcher/LauncherManifest.scala index 5238a90c11e2..4654b57755f3 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/releases/launcher/LauncherManifest.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/releases/launcher/LauncherManifest.scala @@ -1,10 +1,14 @@ package org.enso.launcher.releases.launcher -import io.circe.{yaml, Decoder} +import org.enso.launcher.releases.launcher import org.enso.semver.SemVer import org.enso.runtimeversionmanager.releases.ReleaseProviderException -import org.enso.semver.SemVerJson._ +import org.enso.semver.SemVerYaml._ +import org.enso.yaml.{ParseError, YamlDecoder} +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.nodes.{MappingNode, Node} +import java.io.StringReader import scala.util.{Failure, Try} /** Contains release metadata associated with a launcher release. @@ -37,28 +41,52 @@ object LauncherManifest { val directoriesToCopy = "directories-to-copy" } - /** [[Decoder]] instance for [[LauncherManifest]]. - */ - implicit val decoder: Decoder[LauncherManifest] = { json => - for { - minimumVersionToUpgrade <- - json.get[SemVer](Fields.minimumVersionForUpgrade) - files <- json.getOrElse[Seq[String]](Fields.filesToCopy)(Seq()) - directories <- - json.getOrElse[Seq[String]](Fields.directoriesToCopy)(Seq()) - } yield LauncherManifest( - minimumVersionForUpgrade = minimumVersionToUpgrade, - filesToCopy = files, - directoriesToCopy = directories - ) - } + implicit val yamlDecoder: YamlDecoder[LauncherManifest] = + new YamlDecoder[LauncherManifest] { + override def decode(node: Node): Either[Throwable, LauncherManifest] = { + node match { + case node: MappingNode => + val bindings = mappingKV(node) + val semverDecoder = implicitly[YamlDecoder[SemVer]] + val seqStringDecoder = implicitly[YamlDecoder[Seq[String]]] + for { + minimumVersionForUpgrade <- bindings + .get(Fields.minimumVersionForUpgrade) + .map(semverDecoder.decode) + .getOrElse( + Left( + new YAMLException( + s"Required `${Fields.minimumVersionForUpgrade}` field is missing" + ) + ) + ) + filesToCopy <- bindings + .get(Fields.filesToCopy) + .map(seqStringDecoder.decode) + .getOrElse(Right(Seq.empty)) + directoriesToCopy <- bindings + .get(Fields.directoriesToCopy) + .map(seqStringDecoder.decode) + .getOrElse(Right(Seq.empty)) + } yield LauncherManifest( + minimumVersionForUpgrade, + filesToCopy, + directoriesToCopy + ) + } + } + } /** Tries to parse the [[LauncherManifest]] from a [[String]]. */ - def fromYAML(string: String): Try[LauncherManifest] = - yaml.parser - .parse(string) - .flatMap(_.as[LauncherManifest]) + def fromYAML(string: String): Try[LauncherManifest] = { + val snakeYaml = new org.yaml.snakeyaml.Yaml() + Try(snakeYaml.compose(new StringReader(string))).toEither + .flatMap( + implicitly[YamlDecoder[launcher.LauncherManifest]].decode(_) + ) + .left + .map(ParseError(_)) .toTry .recoverWith { error => // TODO [RW] more readable errors in #1111 @@ -69,4 +97,5 @@ object LauncherManifest { ) ) } + } } diff --git a/lib/scala/cli/src/main/scala/org/enso/cli/OS.scala b/lib/scala/cli/src/main/scala/org/enso/cli/OS.scala index 34161eebc2ce..dc3aa15b9d46 100644 --- a/lib/scala/cli/src/main/scala/org/enso/cli/OS.scala +++ b/lib/scala/cli/src/main/scala/org/enso/cli/OS.scala @@ -2,6 +2,9 @@ package org.enso.cli import com.typesafe.scalalogging.Logger import io.circe.{Decoder, DecodingFailure} +import org.enso.yaml.YamlDecoder +import org.yaml.snakeyaml.nodes.{Node, ScalarNode} +import org.yaml.snakeyaml.error.YAMLException /** Represents one of the supported platforms (operating systems). */ @@ -29,7 +32,7 @@ object OS { /** @inheritdoc */ - def configName: String = "linux" + val configName: String = "linux" } /** Represents the macOS operating system. @@ -38,7 +41,7 @@ object OS { /** @inheritdoc */ - def configName: String = "macos" + val configName: String = "macos" /** @inheritdoc */ @@ -52,7 +55,7 @@ object OS { /** @inheritdoc */ - def configName: String = "windows" + val configName: String = "windows" } /** Checks if the application is being run on Windows. @@ -143,4 +146,18 @@ object OS { } } } + + implicit val yamlDecoder: YamlDecoder[OS] = (node: Node) => { + node match { + case s: ScalarNode => + s.getValue match { + case Linux.configName => Right(Linux) + case Windows.configName => Right(Windows) + case MacOS.configName => Right(MacOS) + case os => Left(new YAMLException(s"Unsupported os `$os`")) + } + case _ => + Left(new YAMLException("Expected a plain string value")) + } + } } diff --git a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/DefaultVersion.scala b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/DefaultVersion.scala index d9fa5fe41b28..4a2672a82738 100644 --- a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/DefaultVersion.scala +++ b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/DefaultVersion.scala @@ -1,10 +1,10 @@ package org.enso.distribution.config -import io.circe.syntax._ -import io.circe.{Decoder, Encoder, Json} import org.enso.semver.SemVer import org.enso.cli.arguments.{Argument, OptsParseError} -import org.enso.semver.SemVerJson._ +import org.enso.semver.SemVerYaml._ +import org.enso.yaml.{YamlDecoder, YamlEncoder} +import org.yaml.snakeyaml.nodes.{Node, ScalarNode} /** Default version that is used when launching Enso outside of projects and * when creating new projects. @@ -17,9 +17,11 @@ object DefaultVersion { */ case object LatestInstalled extends DefaultVersion { + val name = "latest-installed" + /** @inheritdoc */ - override def toString: String = "latest-installed" + override def toString: String = name } /** Defaults to a specified version. @@ -31,24 +33,34 @@ object DefaultVersion { override def toString: String = version.toString } - /** [[Encoder]] instance for [[DefaultVersion]]. - */ - implicit val encoder: Encoder[DefaultVersion] = { - case LatestInstalled => - Json.Null - case Exact(version) => - version.asJson - } + implicit val yamlDecoder: YamlDecoder[DefaultVersion] = + new YamlDecoder[DefaultVersion] { + override def decode(node: Node) = { + node match { + case node if node == null => + Right(LatestInstalled) + case scalarNode: ScalarNode => + scalarNode.getValue match { + case LatestInstalled.name => + Right(LatestInstalled) + case _ => + implicitly[YamlDecoder[SemVer]] + .decode(scalarNode) + .map(Exact(_)) + } + } + } + } - /** [[Decoder]] instance for [[DefaultVersion]]. - */ - implicit val decoder: Decoder[DefaultVersion] = { json => - if (json.value.isNull) Right(LatestInstalled) - else - for { - version <- json.as[SemVer] - } yield Exact(version) - } + implicit val yamlEncoder: YamlEncoder[DefaultVersion] = + new YamlEncoder[DefaultVersion] { + override def encode(value: DefaultVersion): AnyRef = { + value match { + case latest @ LatestInstalled => latest.toString + case Exact(version) => version.toString + } + } + } /** [[Argument]] instance for [[DefaultVersion]]. */ diff --git a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfig.scala b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfig.scala index 4d7c43d125fa..af0f04abfc53 100644 --- a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfig.scala +++ b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfig.scala @@ -1,8 +1,10 @@ package org.enso.distribution.config -import io.circe.syntax._ -import io.circe.{Decoder, Encoder, Json} -import org.enso.distribution.config +import org.enso.yaml.{YamlDecoder, YamlEncoder} +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.nodes.{MappingNode, Node} + +import java.util /** Global user configuration. * @@ -25,20 +27,18 @@ case class GlobalConfig( editionProviders: Seq[String] ) { def findByKey(key: String): Option[String] = { - val jsonValue: Option[Json] = key match { + key match { case GlobalConfig.Fields.DefaultVersion => - Option(defaultVersion).map(_.asJson) + Option(defaultVersion).map(_.toString) case GlobalConfig.Fields.AuthorName => - authorName.map(_.asJson) + authorName case GlobalConfig.Fields.AuthorEmail => - authorEmail.map(_.asJson) + authorEmail case GlobalConfig.Fields.EditionProviders => - Option(editionProviders).map(_.asJson) + Option(editionProviders).map(_.toString()) case _ => None } - - jsonValue.map(j => j.asString.getOrElse(j.toString())) } } @@ -66,36 +66,103 @@ object GlobalConfig { val EditionProviders = "edition-providers" } - /** [[Decoder]] instance for [[GlobalConfig]]. - */ - implicit val decoder: Decoder[GlobalConfig] = { json => - for { - defaultVersion <- json.getOrElse[DefaultVersion](Fields.DefaultVersion)( - DefaultVersion.LatestInstalled - ) - authorName <- json.getOrElse[Option[String]](Fields.AuthorName)(None) - authorEmail <- json.getOrElse[Option[String]](Fields.AuthorEmail)(None) - editionProviders <- json.getOrElse[Seq[String]](Fields.EditionProviders)( - defaultEditionProviders - ) - } yield config.GlobalConfig( - defaultVersion = defaultVersion, - authorName = authorName, - authorEmail = authorEmail, - editionProviders = editionProviders - ) - } + implicit val yamlDecoder: YamlDecoder[GlobalConfig] = + new YamlDecoder[GlobalConfig] { + override def decode(node: Node) = node match { + case node: MappingNode => + val bindings = mappingKV(node) + val defaultVersionDecoder = + implicitly[YamlDecoder[DefaultVersion]] + val stringDecoder = implicitly[YamlDecoder[String]] + val seqStringDecoder = implicitly[YamlDecoder[Seq[String]]] - /** [[Encoder]] instance for [[GlobalConfig]]. - */ - implicit val encoder: Encoder[GlobalConfig] = { config => - val overrides = - Json.obj( - Fields.DefaultVersion -> config.defaultVersion.asJson, - Fields.AuthorName -> config.authorName.asJson, - Fields.AuthorEmail -> config.authorEmail.asJson, - Fields.EditionProviders -> config.editionProviders.asJson - ) - overrides.dropNullValues.asJson - } + val defaultVersionOpt = bindings.get("default") match { + case Some(versionNode: MappingNode) => + val versionBindings = mappingKV(versionNode) + versionBindings + .get("enso-version") + .toRight( + new YAMLException(s"missing '${Fields.DefaultVersion}' field") + ) + .flatMap(defaultVersionDecoder.decode) + case _ => + // Fallback + bindings + .get(Fields.DefaultVersion) + .map(defaultVersionDecoder.decode) + .getOrElse(Right(DefaultVersion.LatestInstalled)) + } + val (nameOpt, emailOpt) = bindings.get("author") match { + case Some(authorNode: MappingNode) => + val authorBindings = mappingKV(authorNode) + ( + authorBindings + .get("name") + .map(stringDecoder.decode) + .getOrElse(Right(None)) + .asInstanceOf[Either[Throwable, Option[String]]], + authorBindings + .get("email") + .map(stringDecoder.decode) + .getOrElse(Right(None)) + .asInstanceOf[Either[Throwable, Option[String]]] + ) + case _ => + // Fallback + ( + bindings + .get(Fields.AuthorName) + .map(stringDecoder.decode(_).map(Some(_))) + .getOrElse(Right(None)), + bindings + .get(Fields.AuthorEmail) + .map(stringDecoder.decode(_).map(Some(_))) + .getOrElse(Right(None)) + ) + } + val editionProviderOpt = bindings + .get(Fields.EditionProviders) + .map(seqStringDecoder.decode) + .getOrElse(Right(Seq.empty)) + for { + defaultVersion <- defaultVersionOpt + name <- nameOpt + email <- emailOpt + editionProvider <- editionProviderOpt + } yield GlobalConfig(defaultVersion, name, email, editionProvider) + } + } + + implicit val yamlEncoder: YamlEncoder[GlobalConfig] = + new YamlEncoder[GlobalConfig] { + override def encode(value: GlobalConfig): AnyRef = { + val defaultVersionEncoder = implicitly[YamlEncoder[DefaultVersion]] + val editionProviders = implicitly[YamlEncoder[Seq[String]]] + val elements = new util.ArrayList[(String, AnyRef)]() + elements.add( + ( + "default", + toMap( + "enso-version", + defaultVersionEncoder.encode(value.defaultVersion) + ) + ) + ) + if (value.authorName.nonEmpty || value.authorEmail.nonEmpty) { + val authorElements = new util.ArrayList[(String, AnyRef)]() + value.authorName.foreach(v => authorElements.add(("name", v))) + value.authorEmail.foreach(v => authorElements.add(("email", v))) + elements.add(("author", toMap(authorElements))) + } + if (value.editionProviders.nonEmpty) { + elements.add( + ( + Fields.EditionProviders, + editionProviders.encode(value.editionProviders) + ) + ) + } + toMap(elements) + } + } } diff --git a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfigurationManager.scala b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfigurationManager.scala index 353f1420c82b..6bb5dea91cfb 100644 --- a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfigurationManager.scala +++ b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfigurationManager.scala @@ -1,14 +1,16 @@ package org.enso.distribution.config import com.typesafe.scalalogging.Logger -import io.circe.syntax._ -import io.circe.yaml.Parser -import io.circe.{yaml, Json} import org.enso.distribution.DistributionManager import org.enso.distribution.FileSystem.PathSyntax +import org.enso.yaml.{YamlDecoder, YamlEncoder} +import org.yaml.snakeyaml.{DumperOptions, Yaml} +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.nodes.{MappingNode, Node, NodeTuple, ScalarNode, Tag} -import java.io.BufferedWriter +import java.io.{BufferedWriter, StringReader} import java.nio.file.{Files, NoSuchFileException, Path} +import scala.jdk.CollectionConverters.{CollectionHasAsScala, SeqHasAsJava} import scala.util.{Failure, Success, Try, Using} /** Manages the global configuration of the distribution. */ @@ -48,31 +50,159 @@ class GlobalConfigurationManager(distributionManager: DistributionManager) { * (because an invalid value has been set for a known field), the config is * not saved and an exception is thrown. */ - def updateConfigRaw(key: String, value: Json): Unit = { - val updated = GlobalConfig.encoder(getConfig).asObject.get.add(key, value) - GlobalConfigurationManager - .writeConfigRaw(configLocation, updated.asJson) - .recoverWith { case e: InvalidConfigError => - Failure( - InvalidConfigError( - s"Invalid value for key `$key`. Config changes were not saved.", - e - ) - ) - } + def updateConfigRaw(key: String, value: String): Unit = { + stringToYamlNode(value) + .flatMap(newValueNode => + updateYamlNode(key.split("\\.").toList, getConfig, newValueNode) + .flatMap { updatedNode => + GlobalConfigurationManager + .writeConfigRaw(configLocation, updatedNode) + .recoverWith { case e: InvalidConfigError => + Failure( + InvalidConfigError( + s"Invalid value for key `$key`. Config changes were not saved", + e + ) + ) + } + } + ) .get } + private def stringToYamlNode(value: String): Try[Node] = { + if (value == null) { + Success(null) + } else { + val snakeYaml = new org.yaml.snakeyaml.Yaml() + Try(snakeYaml.compose(new StringReader(value))) + } + } + + /** Updates GlobalConfig's YAML representation at the provided key-path */ + private def updateYamlNode( + keys: List[String], + config: GlobalConfig, + yamlNode: Node + ): Try[Node] = { + val encoder = implicitly[YamlEncoder[GlobalConfig]] + val snakeYaml = new org.yaml.snakeyaml.Yaml() + updateYamlNode(keys, snakeYaml.represent(encoder.encode(config)), yamlNode) + } + + /** Updates the given YAML node at the provided key-path. + * If the new `yamlNode` is null, the node at the provided key-path should be removed. + * + * @param keys list of keys representing the parent-child relation in YAML nodes + * @param original the currently traversed YAML node + * @param yamlNode the node to be placed at the end of the key-path + * @return the updated YAML node + */ + private def updateYamlNode( + keys: List[String], + original: Node, + yamlNode: Node + ): Try[Node] = { + keys match { + case Nil => + Success(yamlNode) + case head :: rest => + original match { + case mappingNode: MappingNode => + val tuples = mappingNode.getValue.asScala + val (failures, mappings) = tuples + .map { tuple => + tuple.getKeyNode match { + case s: ScalarNode => + Right((s.getValue, (tuple.getValueNode, s))) + case _ => + Left( + new YAMLException( + "Internal error: Unexpected key in the mapping node" + ) + ) + } + } + .span(_.isLeft) + if (failures.isEmpty) { + val m = mappings.map(_.toOption.get).toMap + m.get(head) match { + case Some((entryNode, scalarKeyNode)) => + val others = m.removed(head) + updateYamlNode(rest, entryNode, yamlNode) map { node => + createMappingNode( + others.toList.map { case (_, (valueNode, keyNode)) => + (keyNode, valueNode) + }, + scalarKeyNode, + node, + mappingNode + ) + } + case None if yamlNode == null => + // cannot remove node that is not present + Success(mappingNode) + case None => + val scalarKeyNode = new ScalarNode( + Tag.YAML, + keys.mkString("."), + null, + null, + DumperOptions.ScalarStyle.PLAIN + ) + Success( + createMappingNode( + m.toList.map { case (_, (valueNode, keyNode)) => + (keyNode, valueNode) + }, + scalarKeyNode, + yamlNode, + mappingNode + ) + ) + } + } else { + Failure(failures.head.left.toOption.get) + } + case _ => + Failure( + new YAMLException(s"Cannot replace `$head` in the non-map field") + ) + } + } + } + + private def createMappingNode( + existingTuples: List[(Node, Node)], + newKeyNode: Node, + newValueNode: Node, + existingMappingNode: MappingNode + ): Node = { + val allTuples = + if (newValueNode != null) + existingTuples ++ List((newKeyNode, newValueNode)) + else existingTuples + new MappingNode( + existingMappingNode.getTag, + true, + allTuples.map(v => new NodeTuple(v._1, v._2)).asJava, + existingMappingNode.getStartMark, + existingMappingNode.getEndMark, + existingMappingNode.getFlowStyle + ) + } + /** Removes the `key` from the config. * * If removing that setting would result in the config becoming unreadable, * the config is not saved and an exception is thrown. */ def removeFromConfig(key: String): Unit = { - val updated = GlobalConfig.encoder(getConfig).asObject.get.remove(key) - GlobalConfigurationManager.writeConfigRaw( - configLocation, - updated.asJson + updateYamlNode(key.split("\\.").toList, getConfig, null).map(updatedNode => + GlobalConfigurationManager.writeConfigRaw( + configLocation, + updatedNode + ) ) } } @@ -85,24 +215,31 @@ object GlobalConfigurationManager { /** Tries to read the global config from the given `path`. */ private def readConfig(path: Path): Try[GlobalConfig] = Using(Files.newBufferedReader(path)) { reader => - for { - json <- Parser.default.parse(reader) - config <- json.as[GlobalConfig] - } yield config - }.flatMap(_.toTry) + val snakeYaml = new Yaml() + Try(snakeYaml.compose(reader)).toEither + .flatMap(implicitly[YamlDecoder[GlobalConfig]].decode(_)) + .toTry + }.flatten /** Tries to write the provided `config` to the given `path`. */ - private def writeConfig(path: Path, config: GlobalConfig): Try[Unit] = - writeConfigRaw(path, GlobalConfig.encoder(config)) + private def writeConfig(path: Path, config: GlobalConfig): Try[Unit] = { + val snakeYaml = new org.yaml.snakeyaml.Yaml() + writeConfigRaw( + path, + snakeYaml.represent( + implicitly[YamlEncoder[GlobalConfig]].encode(config) + ) + ) + } /** Tries to write the config from a raw JSON value to the given `path`. * * The config will not be saved if it is invalid, instead an exception is * thrown. */ - private def writeConfigRaw(path: Path, rawConfig: Json): Try[Unit] = { - def verifyConfig: Try[Unit] = - rawConfig.as[GlobalConfig] match { + private def writeConfigRaw(path: Path, rawNode: Node): Try[Unit] = { + def verifyConfig: Try[Unit] = { + implicitly[YamlDecoder[GlobalConfig]].decode(rawNode) match { case Left(failure) => Failure( InvalidConfigError( @@ -112,16 +249,19 @@ object GlobalConfigurationManager { ) case Right(_) => Success(()) } + } + def bufferedWriter: BufferedWriter = { Files.createDirectories(path.getParent) Files.newBufferedWriter(path) } def writeConfig: Try[Unit] = Using(bufferedWriter) { writer => - val string = yaml.Printer.spaces2 - .copy(preserveOrder = true) - .pretty(rawConfig) - writer.write(string) + val dumperOptions = new DumperOptions() + dumperOptions.setIndent(2) + dumperOptions.setPrettyFlow(true) + val yaml = new Yaml(dumperOptions) + yaml.serialize(rawNode, writer) writer.newLine() } diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/EditionName.scala b/lib/scala/editions/src/main/scala/org/enso/editions/EditionName.scala index f781021140c1..fe6cc85c9e9b 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/EditionName.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/EditionName.scala @@ -1,7 +1,8 @@ package org.enso.editions -import io.circe.syntax.EncoderOps -import io.circe.{Decoder, DecodingFailure, Encoder, Json} +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.nodes.{Node, ScalarNode, Tag} +import org.enso.yaml.{YamlDecoder, YamlEncoder} /** A helper type to handle special parsing logic of edition names. * @@ -23,32 +24,29 @@ object EditionName { /** A helper method for constructing an [[EditionName]]. */ def apply(name: String): EditionName = new EditionName(name) - /** A [[Decoder]] instance for [[EditionName]] that accepts not only strings - * but also numbers as valid edition names. - */ - implicit val editionNameDecoder: Decoder[EditionName] = { json => - json - .as[String] - .fold[Either[DecodingFailure, Any]]( - _ => - if (json.value == Json.Null) - Left(DecodingFailure("edition cannot be empty", Nil)) - else - json.as[Int].orElse(json.as[Float]), - Right(_) - ) - .map(v => EditionName(v.toString)) - } + implicit val yamlDecoder: YamlDecoder[EditionName] = + new YamlDecoder[EditionName] { + override def decode(node: Node): Either[Throwable, EditionName] = + node match { + case scalarNode: ScalarNode => + scalarNode.getTag match { + case Tag.NULL => + Left(new YAMLException("edition cannot be empty")) + case _ => + val stringDecoder = implicitly[YamlDecoder[String]] + stringDecoder.decode(scalarNode).map(EditionName(_)) + } + case _ => + Left(new YAMLException("unexpected edition name")) + } + } - /** An [[Encoder]] instance for serializing [[EditionName]]. - * - * Regardless of the original representation, the edition name is always - * serialized as string as this is the most portable and precise format for - * this datatype. - */ - implicit val encoder: Encoder[EditionName] = { case EditionName(name) => - name.asJson - } + implicit val yamlEncoder: YamlEncoder[EditionName] = + new YamlEncoder[EditionName] { + override def encode(value: EditionName): Object = { + value.name + } + } /** The filename suffix that is used to create a filename corresponding to a * named edition. diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/EditionSerialization.scala b/lib/scala/editions/src/main/scala/org/enso/editions/EditionSerialization.scala index 58d281f449dc..5a9909fbce6d 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/EditionSerialization.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/EditionSerialization.scala @@ -1,17 +1,10 @@ package org.enso.editions -import cats.Show -import io.circe._ -import io.circe.syntax.EncoderOps -import io.circe.yaml.Parser -import org.enso.editions.Editions.{Raw, Repository} -import org.enso.semver.SemVerJson._ +import org.enso.editions.Editions.Raw import org.enso.yaml.YamlHelper -import java.io.FileReader import java.nio.file.Path -import scala.util.{Failure, Try, Using} -import org.enso.semver.SemVer +import scala.util.{Failure, Try} /** Gathers methods for decoding and encoding of Raw editions. */ object EditionSerialization { @@ -26,12 +19,8 @@ object EditionSerialization { /** Tries to load an edition definition from a YAML file. */ def loadEdition(path: Path): Try[Raw.Edition] = - Using(new FileReader(path.toFile)) { reader => - Parser.default - .parse(reader) - .flatMap(_.as[Raw.Edition]) - .toTry - }.flatten + YamlHelper + .load[Raw.Edition](path) .recoverWith { error => Failure( EditionResolutionError.wrapLoadingError( @@ -41,82 +30,6 @@ object EditionSerialization { ) } - /** A [[Decoder]] instance for [[Raw.Edition]]. - * - * It can be used to decode nested editions in other kinds of configurations - * files, for example in `package.yaml`. - */ - implicit val editionDecoder: Decoder[Raw.Edition] = { json => - for { - parent <- json.get[Option[EditionName]](Fields.parent) - engineVersion <- json.get[Option[SemVer]](Fields.engineVersion) - _ <- - if (parent.isEmpty && engineVersion.isEmpty) - Left( - DecodingFailure( - s"The edition must specify at least one of " + - s"${Fields.engineVersion} or ${Fields.parent}.", - json.history - ) - ) - else Right(()) - repositories <- - json.getOrElse[Seq[Repository]](Fields.repositories)(Seq()) - libraries <- json.getOrElse[Seq[Raw.Library]](Fields.libraries)(Seq()) - res <- { - val repositoryMap = Map.from(repositories.map(r => (r.name, r))) - val libraryMap = Map.from(libraries.map(l => (l.name, l))) - if (libraryMap.size != libraries.size) - Left( - DecodingFailure( - "Names of libraries defined within a single edition file must be unique.", - json.downField(Fields.libraries).history - ) - ) - else if (repositoryMap.size != repositories.size) - Left( - DecodingFailure( - "Names of repositories defined within a single edition file must be unique.", - json.downField(Fields.libraries).history - ) - ) - else - Right( - Raw.Edition( - parent = parent.map(_.name), - engineVersion = engineVersion, - repositories = repositoryMap, - libraries = libraryMap - ) - ) - } - } yield res - } - - /** An [[Encoder]] instance for [[Raw.Edition]]. */ - implicit val editionEncoder: Encoder[Raw.Edition] = { edition => - val parent = edition.parent.map { parent => Fields.parent -> parent.asJson } - val engineVersion = edition.engineVersion.map { version => - Fields.engineVersion -> version.asJson - } - - if (parent.isEmpty && engineVersion.isEmpty) { - throw new IllegalArgumentException( - "Internal error: An edition must specify at least the engine version or extends clause" - ) - } - - val repositories = - if (edition.repositories.isEmpty) None - else Some(Fields.repositories -> edition.repositories.values.asJson) - val libraries = - if (edition.libraries.isEmpty) None - else Some(Fields.libraries -> edition.libraries.values.asJson) - Json.obj( - parent.toSeq ++ engineVersion.toSeq ++ repositories.toSeq ++ libraries.toSeq: _* - ) - } - object Fields { val name = "name" val version = "version" @@ -136,99 +49,4 @@ object EditionSerialization { override def toString: String = message } - object EditionLoadingError { - - /** Creates a [[EditionLoadingError]] by wrapping another [[Throwable]]. - * - * Special logic is used for [[io.circe.Error]] to display the error - * summary in a human-readable way. - */ - def fromThrowable(throwable: Throwable): EditionLoadingError = - throwable match { - case decodingError: io.circe.Error => - val errorMessage = - implicitly[Show[io.circe.Error]].show(decodingError) - EditionLoadingError( - s"Could not parse the edition file: $errorMessage", - decodingError - ) - case other => - EditionLoadingError(s"Could not load the edition file: $other", other) - } - } - - implicit private val libraryDecoder: Decoder[Raw.Library] = { json => - def makeLibrary( - name: LibraryName, - repository: String, - version: Option[SemVer] - ) = - if (repository == Fields.localRepositoryName) - if (version.isDefined) - Left( - DecodingFailure( - "Version field must not be set for libraries associated with the local repository.", - json.history - ) - ) - else Right(Raw.LocalLibrary(name)) - else { - version match { - case Some(versionValue) => - Right(Raw.PublishedLibrary(name, versionValue, repository)) - case None => - Left( - DecodingFailure( - "Version field is mandatory for non-local libraries.", - json.history - ) - ) - } - } - for { - name <- json.get[LibraryName](Fields.name) - repository <- json.get[String](Fields.repository) - version <- json.get[Option[SemVer]](Fields.version) - res <- makeLibrary(name, repository, version) - } yield res - } - - implicit private val libraryEncoder: Encoder[Raw.Library] = { - case Raw.LocalLibrary(name) => - Json.obj( - Fields.name -> name.asJson, - Fields.repository -> Fields.localRepositoryName.asJson - ) - case Raw.PublishedLibrary(name, version, repository) => - Json.obj( - Fields.name -> name.asJson, - Fields.version -> version.asJson, - Fields.repository -> repository.asJson - ) - } - - implicit private val repositoryEncoder: Encoder[Repository] = { repo => - Json.obj( - Fields.name -> repo.name.asJson, - Fields.url -> repo.url.asJson - ) - } - - implicit private val repositoryDecoder: Decoder[Repository] = { json => - val nameField = json.downField(Fields.name) - for { - name <- nameField.as[String] - url <- json.get[String](Fields.url) - res <- - if (name == Fields.localRepositoryName) - Left( - DecodingFailure( - s"A defined repository cannot be called " + - s"`${Fields.localRepositoryName}` which is a reserved keyword.", - nameField.history - ) - ) - else Right(Repository(name, url)) - } yield res - } } diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/Editions.scala b/lib/scala/editions/src/main/scala/org/enso/editions/Editions.scala index 6c94467b7d5f..3493b64fe009 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/Editions.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/Editions.scala @@ -1,6 +1,12 @@ package org.enso.editions -import org.enso.semver.SemVer +import org.enso.semver.{SemVer, SemVerYaml} +import org.yaml.snakeyaml.nodes.{MappingNode, Node, ScalarNode} +import org.enso.yaml.{YamlDecoder, YamlEncoder} +import org.enso.yaml.YamlDecoder.MapKeyField +import org.yaml.snakeyaml.error.YAMLException + +import java.util /** Defines the general edition structure. * @@ -43,6 +49,136 @@ trait Editions { def name: LibraryName } + implicit def nestedEditionTypeDecoder: YamlDecoder[NestedEditionType] + implicit def libraryRepositoryTypeDecoder: YamlDecoder[LibraryRepositoryType] + + implicit def nestedEditionTypeEncoder: YamlEncoder[NestedEditionType] + implicit def libraryRepositoryTypeEncoder: YamlEncoder[LibraryRepositoryType] + + object Library { + + trait LibraryFields { + val Name = "name" + } + + object LocalLibraryFields extends LibraryFields + object PublishedLibraryFields extends LibraryFields { + val Version = "version" + val Repository = "repository" + } + + implicit val yamlDecoder: YamlDecoder[Library] = + new YamlDecoder[Library] { + override def decode(node: Node): Either[Throwable, Library] = + node match { + case mappingNode: MappingNode => + val bindings = mappingKV(mappingNode) + + bindings.get(LocalLibraryFields.Name) match { + case Some(node) => + val repoField = + bindings.get(PublishedLibraryFields.Repository) + + val isLocalRepo = repoField + .map(node => + node.isInstanceOf[ScalarNode] && node + .asInstanceOf[ScalarNode] + .getValue == "local" + ) + .getOrElse(false) + if (isLocalRepo) { + if (bindings.contains(PublishedLibraryFields.Version)) + Left( + new YAMLException( + s"'${PublishedLibraryFields.Version}' field must not be set for libraries associated with the local repository" + ) + ) + else + implicitly[YamlDecoder[LibraryName]] + .decode(node) + .map(LocalLibrary(_)) + } else { + val libraryNameDecoder = + implicitly[YamlDecoder[LibraryName]] + val versionDecoder = SemVerYaml.yamlSemverDecoder + val repositoryDecoder = + implicitly[YamlDecoder[LibraryRepositoryType]] + for { + name <- bindings + .get(PublishedLibraryFields.Name) + .toRight( + new YAMLException( + s"Missing '${PublishedLibraryFields.Name}' field" + ) + ) + .flatMap(libraryNameDecoder.decode) + version <- bindings + .get(PublishedLibraryFields.Version) + .toRight( + new YAMLException( + s"'${PublishedLibraryFields.Version}' field is mandatory for non-local libraries" + ) + ) + .flatMap(versionDecoder.decode) + repository <- bindings + .get(PublishedLibraryFields.Repository) + .toRight( + new YAMLException( + s"Missing '${PublishedLibraryFields.Repository}' field" + ) + ) + .flatMap(repositoryDecoder.decode) + } yield PublishedLibrary(name, version, repository) + } + case None => + Left( + new YAMLException( + s"Library requires `${LocalLibraryFields.Name}` field" + ) + ) + } + } + } + + implicit val yamlEncoder: YamlEncoder[Library] = + new YamlEncoder[Library] { + import SemVerYaml._ + override def encode(value: Library) = { + val libraryNamencoder = implicitly[YamlEncoder[LibraryName]] + val elements = new util.ArrayList[(String, Object)](1) + value match { + case local: LocalLibrary => + elements.add( + (LocalLibraryFields.Name, libraryNamencoder.encode(local.name)) + ) + case remote: PublishedLibrary => + val semverEncoder = implicitly[YamlEncoder[SemVer]] + val repoEncoder = + implicitly[YamlEncoder[LibraryRepositoryType]] + elements.add( + ( + PublishedLibraryFields.Name, + libraryNamencoder.encode(remote.name) + ) + ) + elements.add( + ( + PublishedLibraryFields.Version, + semverEncoder.encode(remote.version) + ) + ) + elements.add( + ( + PublishedLibraryFields.Repository, + repoEncoder.encode(remote.repository) + ) + ) + } + toMap(elements) + } + } + } + /** Represents a local library. */ case class LocalLibrary(override val name: LibraryName) extends Library @@ -111,6 +247,99 @@ trait Editions { libraries.map(l => (l.name, l)).toMap ) } + + object Fields { + val Parent = "extends" + val EngineVersion = "engine-version" + val Repositories = "repositories" + val Libraries = "libraries" + } + + implicit val yamlDecoder: YamlDecoder[Edition] = + new YamlDecoder[Edition] { + import org.enso.semver.SemVerYaml._ + override def decode(node: Node): Either[Throwable, Edition] = node match { + case mappingNode: MappingNode => + val clazzMap = mappingKV(mappingNode) + val parentDecoder = + implicitly[YamlDecoder[Option[NestedEditionType]]] + val semverDecoder = implicitly[YamlDecoder[Option[SemVer]]] + implicit val mapKey = MapKeyField.plainField("name") + val repositoriesDecoder = + implicitly[YamlDecoder[Map[String, Editions.Repository]]] + val librariesDecoder = + implicitly[YamlDecoder[Map[LibraryName, Library]]] + for { + parent <- clazzMap + .get(Fields.Parent) + .map(parentDecoder.decode) + .getOrElse(Right(None)) + engineVersion <- clazzMap + .get(Fields.EngineVersion) + .map(semverDecoder.decode) + .getOrElse(Right(None)) + _ <- + if (parent.isEmpty && engineVersion.isEmpty) + Left( + new YAMLException( + s"The edition must specify at least one of " + + s"`${Fields.EngineVersion}` or `${Fields.Parent}`" + ) + ) + else Right(()) + repositories <- clazzMap + .get(Fields.Repositories) + .map(repositoriesDecoder.decode) + .getOrElse(Right(Map.empty[String, Editions.Repository])) + libraries <- clazzMap + .get(Fields.Libraries) + .map(librariesDecoder.decode) + .getOrElse(Right(Map.empty[LibraryName, Library])) + + } yield Edition(parent, engineVersion, repositories, libraries) + } + } + + implicit val yamlEncoder: YamlEncoder[Edition] = + new YamlEncoder[Edition] { + import SemVerYaml._ + override def encode(value: Edition) = { + val parentEncoder = implicitly[YamlEncoder[NestedEditionType]] + val semverEncoder = implicitly[YamlEncoder[SemVer]] + val repositoriesEncoder = + implicitly[YamlEncoder[Seq[Editions.Repository]]] + val librariesEncoder = implicitly[YamlEncoder[Seq[Library]]] + + if (value.parent.isEmpty && value.engineVersion.isEmpty) + throw new YAMLException( + s"The edition must specify at least one of " + + s"`${Fields.EngineVersion}` or `${Fields.Parent}`" + ) + val elements = new util.ArrayList[(String, Object)]() + value.parent + .map(parentEncoder.encode) + .foreach(n => elements.add((Fields.Parent, n))) + value.engineVersion + .map(semverEncoder.encode) + .foreach(n => elements.add((Fields.EngineVersion, n))) + if (value.libraries.nonEmpty) + elements.add( + ( + Fields.Repositories, + repositoriesEncoder.encode(value.repositories.values.toSeq) + ) + ) + if (value.repositories.nonEmpty) + elements.add( + ( + Fields.Libraries, + librariesEncoder.encode(value.libraries.values.toSeq) + ) + ) + toMap(elements) + } + } + } object Editions { @@ -120,11 +349,53 @@ object Editions { object Repository { + object Fields { + val Name = "name" + val Url = "url" + } + /** An alternative constructor for unnamed repositories. * * The URL is used as the repository name. */ def apply(url: String): Repository = Repository(url, url) + + implicit val yamlDecoder: YamlDecoder[Repository] = { + new YamlDecoder[Repository] { + override def decode(node: Node): Either[Throwable, Repository] = + node match { + case mappingNode: MappingNode => + val stringDecoder = implicitly[YamlDecoder[String]] + val clazzMap = mappingKV(mappingNode) + for { + name <- clazzMap + .get(Fields.Name) + .toRight( + new YAMLException(s"Missing '${Fields.Name}' field") + ) + .flatMap(stringDecoder.decode) + url <- clazzMap + .get(Fields.Url) + .toRight( + new YAMLException(s"Missing '${Fields.Url}' field") + ) + .flatMap(stringDecoder.decode) + } yield Repository(name, url) + } + } + } + + implicit val yamlEncoder: YamlEncoder[Repository] = { + new YamlEncoder[Repository] { + override def encode(value: Repository) = { + val elements = new util.ArrayList[(String, Object)](2) + elements.add((Fields.Name, value.name)) + elements.add((Fields.Url, value.url)) + toMap(elements) + } + } + } + } /** Implements the Raw editions that can be directly parsed from a YAML @@ -136,6 +407,18 @@ object Editions { object Raw extends Editions { override type NestedEditionType = String override type LibraryRepositoryType = String + + implicit override def nestedEditionTypeDecoder + : YamlDecoder[NestedEditionType] = YamlDecoder.stringDecoderYaml + implicit override def libraryRepositoryTypeDecoder + : YamlDecoder[LibraryRepositoryType] = + YamlDecoder.stringDecoderYaml + + implicit override def nestedEditionTypeEncoder: YamlEncoder[String] = + YamlEncoder.stringEncoderYaml + + implicit override def libraryRepositoryTypeEncoder: YamlEncoder[String] = + YamlEncoder.stringEncoderYaml } /** Implements the Resolved editions which are obtained by analyzing the Raw @@ -144,6 +427,17 @@ object Editions { object Resolved extends Editions { override type NestedEditionType = this.Edition override type LibraryRepositoryType = Repository + + implicit override def nestedEditionTypeDecoder + : YamlDecoder[NestedEditionType] = yamlDecoder + implicit override def libraryRepositoryTypeDecoder + : YamlDecoder[Repository] = Repository.yamlDecoder + + implicit override def nestedEditionTypeEncoder + : YamlEncoder[NestedEditionType] = yamlEncoder + + implicit override def libraryRepositoryTypeEncoder + : YamlEncoder[Repository] = Repository.yamlEncoder } /** An alias for Raw editions. */ diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/EnsoVersion.scala b/lib/scala/editions/src/main/scala/org/enso/editions/EnsoVersion.scala index c45bdeea9adf..46c9f182abb9 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/EnsoVersion.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/EnsoVersion.scala @@ -1,6 +1,6 @@ package org.enso.editions -import io.circe.syntax._ +import io.circe.syntax.EncoderOps import io.circe.{Decoder, DecodingFailure, Encoder} import org.enso.semver.SemVer diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/LibraryName.scala b/lib/scala/editions/src/main/scala/org/enso/editions/LibraryName.scala index 9a1b11a16358..1e77808b03e1 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/LibraryName.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/LibraryName.scala @@ -2,6 +2,9 @@ package org.enso.editions import io.circe.syntax.EncoderOps import io.circe.{Decoder, DecodingFailure, Encoder} +import org.enso.yaml.{YamlDecoder, YamlEncoder} +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.nodes.{MappingNode, Node, ScalarNode} /** Represents a library name that should uniquely identify the library. * @@ -22,6 +25,47 @@ case class LibraryName(namespace: String, name: String) { object LibraryName { + object Fields { + val Namespace = "namespace" + val Email = "email" + } + + implicit val yamlDecoder: YamlDecoder[LibraryName] = + new YamlDecoder[LibraryName] { + override def decode(node: Node): Either[Throwable, LibraryName] = + node match { + case mappingNode: MappingNode => + val stringDecoder = implicitly[YamlDecoder[String]] + val clazzMap = mappingKV(mappingNode) + for { + namesapce <- clazzMap + .get(Fields.Namespace) + .toRight( + new YAMLException(s"Missing '${Fields.Namespace}' field") + ) + .flatMap(stringDecoder.decode) + email <- clazzMap + .get(Fields.Email) + .toRight( + new YAMLException(s"Missing '${Fields.Email}' field") + ) + .flatMap(stringDecoder.decode) + } yield LibraryName(namesapce, email) + case scalarNode: ScalarNode => + val v = scalarNode.getValue + fromModuleName(v).toRight( + new YAMLException(s"'$v' is not a valid library name") + ) + } + } + + implicit val yamlEncoder: YamlEncoder[LibraryName] = + new YamlEncoder[LibraryName] { + override def encode(value: LibraryName) = { + value.toString + } + } + /** A [[Decoder]] instance allowing to parse a [[LibraryName]]. */ implicit val decoder: Decoder[LibraryName] = { json => for { diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/repository/Manifest.scala b/lib/scala/editions/src/main/scala/org/enso/editions/repository/Manifest.scala index 6ed22c7f664c..5b1a597b8df7 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/repository/Manifest.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/repository/Manifest.scala @@ -1,8 +1,9 @@ package org.enso.editions.repository -import io.circe._ -import io.circe.syntax.EncoderOps import org.enso.editions.EditionName +import org.enso.yaml.{YamlDecoder, YamlEncoder} +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.nodes.{MappingNode, Node, ScalarNode, SequenceNode} /** The Edition Repository manifest, which lists all editions that the * repository provides. @@ -14,17 +15,35 @@ object Manifest { val editions = "editions" } - /** A [[Decoder]] instance for parsing [[Manifest]]. */ - implicit val decoder: Decoder[Manifest] = { json => - for { - editions <- json.get[Seq[EditionName]](Fields.editions) - } yield Manifest(editions) - } + implicit val yamlDecoder: YamlDecoder[Manifest] = + new YamlDecoder[Manifest] { + override def decode(node: Node): Either[Throwable, Manifest] = + node match { + case seqNode: SequenceNode => + val decoder = implicitly[YamlDecoder[Seq[EditionName]]] + decoder.decode(seqNode).map(Manifest(_)) + case mappingNode: MappingNode if mappingNode.getValue.size() == 1 => + val editionsNode = mappingNode.getValue.get(0) + (editionsNode.getKeyNode, editionsNode.getValueNode) match { + case (keyNode: ScalarNode, seqNode: SequenceNode) + if keyNode.getValue == Fields.editions => + val decoder = implicitly[YamlDecoder[Seq[EditionName]]] + decoder.decode(seqNode).map(Manifest(_)) + case _ => + Left(new YAMLException("Failed to decode editions")) + } + case _ => + Left(new YAMLException("Failed to decode editions")) + } + } - /** An [[Encoder]] instance for serializing [[Manifest]]. */ - implicit val encoder: Encoder[Manifest] = { manifest => - Json.obj(Fields.editions -> manifest.editions.asJson) - } + implicit val yamlEncoder: YamlEncoder[Manifest] = + new YamlEncoder[Manifest] { + override def encode(value: Manifest) = { + val editionsEncoder = implicitly[YamlEncoder[Seq[EditionName]]] + toMap(Fields.editions, editionsEncoder.encode(value.editions)) + } + } /** The name of the manifest file that should be present at the root of * editions repository. diff --git a/lib/scala/editions/src/main/scala/org/enso/yaml/ParseError.scala b/lib/scala/editions/src/main/scala/org/enso/yaml/ParseError.scala index 908500bee0f8..e10638d6625f 100644 --- a/lib/scala/editions/src/main/scala/org/enso/yaml/ParseError.scala +++ b/lib/scala/editions/src/main/scala/org/enso/yaml/ParseError.scala @@ -1,19 +1,15 @@ package org.enso.yaml -import cats.Show - /** Indicates a parse failure, usually meaning that the input data has * unexpected format (like missing fields or wrong field types). */ -case class ParseError(message: String, cause: io.circe.Error) +case class ParseError(message: String, cause: Throwable) extends RuntimeException(message, cause) object ParseError { - /** Wraps a [[io.circe.Error]] into a more user-friendly [[ParseError]]. */ - def apply(error: io.circe.Error): ParseError = { - val errorMessage = - implicitly[Show[io.circe.Error]].show(error) - ParseError(errorMessage, error) + /** Wraps a parser exception into a more user-friendly [[ParseError]]. */ + def apply(error: Throwable): ParseError = { + ParseError(error.getMessage, error) } } diff --git a/lib/scala/editions/src/main/scala/org/enso/yaml/YamlHelper.scala b/lib/scala/editions/src/main/scala/org/enso/yaml/YamlHelper.scala index aa537b698522..cd9e2fdca460 100644 --- a/lib/scala/editions/src/main/scala/org/enso/yaml/YamlHelper.scala +++ b/lib/scala/editions/src/main/scala/org/enso/yaml/YamlHelper.scala @@ -1,9 +1,9 @@ package org.enso.yaml -import io.circe.yaml.{Parser, Printer} -import io.circe.{yaml, Decoder, Encoder} +import org.yaml.snakeyaml.nodes.Tag +import org.yaml.snakeyaml.{DumperOptions, Yaml} -import java.io.FileReader +import java.io.{FileReader, StringReader} import java.nio.file.Path import scala.util.{Try, Using} @@ -13,23 +13,29 @@ object YamlHelper { /** Parses a string representation of a YAML configuration of type `R`. */ def parseString[R]( yamlString: String - )(implicit decoder: Decoder[R]): Either[ParseError, R] = - yaml.parser - .parse(yamlString) - .flatMap(_.as[R]) + )(implicit decoder: YamlDecoder[R]): Either[ParseError, R] = { + val snakeYaml = new org.yaml.snakeyaml.Yaml() + Try(snakeYaml.compose(new StringReader(yamlString))).toEither + .flatMap(decoder.decode(_)) .left .map(ParseError(_)) + } /** Tries to load and parse a YAML file at the provided path. */ - def load[R](path: Path)(implicit decoder: Decoder[R]): Try[R] = + def load[R](path: Path)(implicit decoder: YamlDecoder[R]): Try[R] = Using(new FileReader(path.toFile)) { reader => - Parser.default - .parse(reader) - .flatMap(_.as[R]) - .toTry + val snakeYaml = new org.yaml.snakeyaml.Yaml() + Try(snakeYaml.compose(reader)) + .flatMap(decoder.decode(_).toTry) }.flatten /** Saves a YAML representation of an object into a string. */ - def toYaml[A](obj: A)(implicit encoder: Encoder[A]): String = - Printer.spaces2.copy(preserveOrder = true).pretty(encoder(obj)) + def toYaml[A](obj: A)(implicit encoder: YamlEncoder[A]): String = { + val node = encoder.encode(obj) + val dumperOptions = new DumperOptions() + dumperOptions.setIndent(2) + dumperOptions.setPrettyFlow(true) + val yaml = new Yaml(dumperOptions) + yaml.dumpAs(node, Tag.MAP, DumperOptions.FlowStyle.BLOCK) + } } diff --git a/lib/scala/editions/src/test/scala/org/enso/editions/EditionSerializationSpec.scala b/lib/scala/editions/src/test/scala/org/enso/editions/EditionSerializationSpec.scala index c6e74157fdf2..d7a32fbcf5de 100644 --- a/lib/scala/editions/src/test/scala/org/enso/editions/EditionSerializationSpec.scala +++ b/lib/scala/editions/src/test/scala/org/enso/editions/EditionSerializationSpec.scala @@ -81,6 +81,23 @@ class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside { } } + "not allow non-unique libraries" in { + val parsed = EditionSerialization.parseYamlString( + """engine-version: 1.2.3-SNAPSHOT + |libraries: + |- name: Foo.local + | repository: local + |- name: Foo.local + | repository: local + |""".stripMargin + ) + inside(parsed) { case Failure(exception) => + exception.getMessage should include( + "YAML definition contains duplicate entries" + ) + } + } + "not allow invalid version combinations for libraries" in { val parsed = EditionSerialization.parseYamlString( """extends: foo @@ -91,7 +108,7 @@ class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside { |""".stripMargin ) inside(parsed) { case Failure(exception) => - exception.getMessage should include("Version field must not be set") + exception.getMessage should include("'version' field must not be set") } val parsed2 = EditionSerialization.parseYamlString( @@ -102,7 +119,9 @@ class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside { |""".stripMargin ) inside(parsed2) { case Failure(exception) => - exception.getMessage should include("Version field is mandatory") + exception.getMessage should include( + "'version' field is mandatory for non-local libraries" + ) } } } diff --git a/lib/scala/library-manager-test/src/main/scala/org/enso/librarymanager/published/repository/DummyRepository.scala b/lib/scala/library-manager-test/src/main/scala/org/enso/librarymanager/published/repository/DummyRepository.scala index 2f61671bcdc7..2564a55860de 100644 --- a/lib/scala/library-manager-test/src/main/scala/org/enso/librarymanager/published/repository/DummyRepository.scala +++ b/lib/scala/library-manager-test/src/main/scala/org/enso/librarymanager/published/repository/DummyRepository.scala @@ -4,7 +4,6 @@ import org.enso.semver.SemVer import org.enso.cli.OS import org.enso.distribution.FileSystem import org.enso.downloader.archive.TarGzWriter -import org.enso.editions.EditionSerialization.editionEncoder import org.enso.editions.Editions.RawEdition import org.enso.editions.{Editions, LibraryName} import org.enso.pkg.{Package, PackageManager} diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/repository/LibraryManifest.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/repository/LibraryManifest.scala index 600372901f35..7fcc01dc740a 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/repository/LibraryManifest.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/repository/LibraryManifest.scala @@ -1,10 +1,12 @@ package org.enso.librarymanager.published.repository -import io.circe.{Decoder, Encoder, Json} -import io.circe.syntax.EncoderOps -import io.circe.yaml import org.enso.editions.LibraryName +import org.enso.yaml.{YamlDecoder, YamlEncoder} +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.nodes.{MappingNode, Node} +import java.io.StringReader +import java.util import scala.util.Try /** The manifest file containing metadata related to a published library. @@ -41,40 +43,73 @@ object LibraryManifest { val description = "description" } - /** A [[Decoder]] instance for parsing [[LibraryManifest]]. */ - implicit val decoder: Decoder[LibraryManifest] = { json => - for { - archives <- json.get[Seq[String]](Fields.archives) - dependencies <- json.getOrElse[Seq[LibraryName]](Fields.dependencies)( - Seq() - ) - tagLine <- json.get[Option[String]](Fields.tagLine) - description <- json.get[Option[String]](Fields.description) - } yield LibraryManifest( - archives = archives, - dependencies = dependencies, - tagLine = tagLine, - description = description - ) - } - - /** An [[Encoder]] instance for parsing [[LibraryManifest]]. */ - implicit val encoder: Encoder[LibraryManifest] = { manifest => - val baseFields = Seq( - Fields.archives -> manifest.archives.asJson, - Fields.dependencies -> manifest.dependencies.asJson - ) + implicit val yamlDecoder: YamlDecoder[LibraryManifest] = + new YamlDecoder[LibraryManifest] { + override def decode(node: Node): Either[Throwable, LibraryManifest] = + node match { + case mappingNode: MappingNode => + val archivesDecoder = implicitly[YamlDecoder[Seq[String]]] + val dependenciesDecoder = + implicitly[YamlDecoder[Seq[LibraryName]]] + val optStringDecoder = implicitly[YamlDecoder[Option[String]]] + val kv = mappingKV(mappingNode) + for { + archives <- kv + .get(Fields.archives) + .map(archivesDecoder.decode(_)) + .getOrElse(Right(Seq.empty)) + dependencies <- kv + .get(Fields.dependencies) + .map(dependenciesDecoder.decode(_)) + .getOrElse(Right(Seq.empty)) + tagLine <- kv + .get(Fields.tagLine) + .map(optStringDecoder.decode(_)) + .getOrElse(Right(None)) + description <- kv + .get(Fields.description) + .map(optStringDecoder.decode(_)) + .getOrElse(Right(None)) + } yield LibraryManifest( + archives, + dependencies, + tagLine, + description + ) + case _ => + Left(new YAMLException("Unexpected library manifest definition")) + } + } - val allFields = baseFields ++ - manifest.tagLine.map(Fields.tagLine -> _.asJson).toSeq ++ - manifest.description.map(Fields.description -> _.asJson).toSeq - - Json.obj(allFields: _*) - } + implicit val yamlEncoder: YamlEncoder[LibraryManifest] = + new YamlEncoder[LibraryManifest] { + override def encode(value: LibraryManifest): AnyRef = { + val archivesEncoder = implicitly[YamlEncoder[Seq[String]]] + val dependenciesEncoder = implicitly[YamlEncoder[Seq[LibraryName]]] + val elements = new util.ArrayList[(String, Object)]() + if (value.archives.nonEmpty) + elements.add( + (Fields.archives, archivesEncoder.encode(value.archives)) + ) + if (value.dependencies.nonEmpty) + elements.add( + ( + Fields.dependencies, + dependenciesEncoder.encode(value.dependencies) + ) + ) + value.tagLine.foreach(v => elements.add((Fields.tagLine, v))) + value.description.foreach(v => elements.add((Fields.description, v))) + toMap(elements) + } + } /** Parser the provided string and returns a LibraryManifest, if valid */ def fromYaml(yamlString: String): Try[LibraryManifest] = { - yaml.parser.parse(yamlString).flatMap(_.as[LibraryManifest]).toTry + val snakeYaml = new org.yaml.snakeyaml.Yaml() + Try(snakeYaml.compose(new StringReader(yamlString))).toEither + .flatMap(implicitly[YamlDecoder[LibraryManifest]].decode(_)) + .toTry } /** The name of the manifest file as included in the directory associated with diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/ComponentGroup.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/ComponentGroup.scala index 647b8e6b52c9..823429d0dc0b 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/ComponentGroup.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/ComponentGroup.scala @@ -3,6 +3,11 @@ package org.enso.pkg import io.circe._ import io.circe.syntax._ import org.enso.editions.LibraryName +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.nodes.{MappingNode, Node, ScalarNode, SequenceNode} +import org.enso.yaml.{YamlDecoder, YamlEncoder} + +import java.util /** The description of component groups provided by the package. * @@ -45,6 +50,54 @@ object ComponentGroups { json.getOrElse[List[ExtendedComponentGroup]](Fields.Extends)(List()) } yield ComponentGroups(newGroups, extendsGroups) } + + implicit val yamlDecoder: YamlDecoder[ComponentGroups] = + new YamlDecoder[ComponentGroups] { + override def decode(node: Node): Either[Throwable, ComponentGroups] = + node match { + case mappingNode: MappingNode => + val clazzMap = mappingKV(mappingNode) + val newGroupsDecoder = + implicitly[YamlDecoder[List[ComponentGroup]]] + val extendedGroupsDecoder = + implicitly[YamlDecoder[List[ExtendedComponentGroup]]] + for { + newGroups <- clazzMap + .get(Fields.New) + .map(newGroupsDecoder.decode) + .getOrElse(Right(Nil)) + extendedGroups <- clazzMap + .get(Fields.Extends) + .map(extendedGroupsDecoder.decode) + .getOrElse(Right(Nil)) + } yield ComponentGroups(newGroups, extendedGroups) + } + } + + implicit val yamlEncoder: YamlEncoder[ComponentGroups] = + new YamlEncoder[ComponentGroups] { + override def encode(value: ComponentGroups) = { + val componentGroupEncoder = + implicitly[YamlEncoder[List[ComponentGroup]]] + val extendedComponentGoupEncoder = + implicitly[YamlEncoder[List[ExtendedComponentGroup]]] + val elements = new util.ArrayList[(String, Object)](0) + if (value.newGroups.nonEmpty) { + elements.add( + (Fields.New, componentGroupEncoder.encode(value.newGroups)) + ) + } + if (value.extendedGroups.nonEmpty) { + elements.add( + ( + Fields.Extends, + extendedComponentGoupEncoder.encode(value.extendedGroups) + ) + ) + } + toMap(elements) + } + } } /** The definition of a single component group. @@ -64,11 +117,88 @@ object ComponentGroup { /** Fields for use when serializing the [[ComponentGroup]]. */ private object Fields { + val Group = "group" val Color = "color" val Icon = "icon" val Exports = "exports" } + implicit val yamlDecoder: YamlDecoder[ComponentGroup] = + new YamlDecoder[ComponentGroup] { + override def decode(node: Node): Either[Throwable, ComponentGroup] = + node match { + case mappingNode: MappingNode => + if (mappingNode.getValue.size() == 1) { + val groupNode = mappingNode.getValue.get(0) + (groupNode.getKeyNode, groupNode.getValueNode) match { + case (scalarNode: ScalarNode, mappingNode: MappingNode) => + val clazzMap = mappingKV(mappingNode) + val groupDecoder = implicitly[YamlDecoder[GroupName]] + val colorDecoder = + implicitly[YamlDecoder[Option[String]]] + val iconDecoder = + implicitly[YamlDecoder[Option[String]]] + val exportDecoder = + implicitly[YamlDecoder[Seq[Component]]] + + for { + group <- groupDecoder.decode(scalarNode) + color <- clazzMap + .get(Fields.Color) + .map(colorDecoder.decode) + .getOrElse(Right(None)) + icon <- clazzMap + .get(Fields.Icon) + .map(iconDecoder.decode) + .getOrElse(Right(None)) + exports <- clazzMap + .get(Fields.Exports) + .map(exportDecoder.decode) + .getOrElse(Right(Seq.empty)) + } yield ComponentGroup(group, color, icon, exports) + case (_: ScalarNode, value: ScalarNode) => + Left( + new YAMLException( + "Failed to decode component group. Expected a map field, got a value:" + value.getValue + ) + ) + case (_: ScalarNode, _: SequenceNode) => + Left( + new YAMLException( + "Failed to decode component group. Expected a mapping, got a sequence" + ) + ) + case _ => + Left( + new YAMLException( + "Failed to decode component group" + ) + ) + } + } else { + Left( + new YAMLException("Failed to decode component group") + ) + } + } + } + + implicit val yamlEncoder: YamlEncoder[ComponentGroup] = + new YamlEncoder[ComponentGroup] { + override def encode(value: ComponentGroup) = { + val fields = new util.ArrayList[(String, Object)](3) + val seqEncoder = implicitly[YamlEncoder[Seq[Component]]] + value.color.foreach(v => fields.add((Fields.Color, v))) + value.icon.foreach(v => fields.add((Fields.Icon, v))) + if (value.exports.nonEmpty) { + val exportsNode = seqEncoder.encode(value.exports) + fields.add((Fields.Exports, exportsNode)) + } + val componentElementsGroupNode = toMap(fields) + toMap(value.group.name, componentElementsGroupNode) + } + } + /** [[Encoder]] instance for the [[ComponentGroup]]. */ implicit val encoder: Encoder[ComponentGroup] = { componentGroup => val color = componentGroup.color.map(Fields.Color -> _.asJson) @@ -131,9 +261,94 @@ object ExtendedComponentGroup { /** Fields for use when serializing the [[ExtendedComponentGroup]]. */ private object Fields { + val Group = "group" val Exports = "exports" } + implicit val yamlDecoder: YamlDecoder[ExtendedComponentGroup] = + new YamlDecoder[ExtendedComponentGroup] { + override def decode( + node: Node + ): Either[Throwable, ExtendedComponentGroup] = node match { + case mappingNode: MappingNode => + if (mappingNode.getValue.size() == 1) { + val groupDecoder = implicitly[YamlDecoder[GroupReference]] + val exportsDecoder = implicitly[YamlDecoder[Seq[Component]]] + val groupNode = mappingNode.getValue.get(0) + (groupNode.getKeyNode, groupNode.getValueNode) match { + case (scalarNode: ScalarNode, seqNode: SequenceNode) => + for { + group <- groupDecoder.decode(scalarNode) + exports <- exportsDecoder.decode(seqNode) + } yield ExtendedComponentGroup(group, exports) + case (groupNode: ScalarNode, componentExportsNode: MappingNode) => + val values = componentExportsNode.getValue + val valuesCount = values.size() + if (valuesCount == 0) { + groupDecoder + .decode(groupNode) + .map(ExtendedComponentGroup(_, Seq.empty)) + } else if (valuesCount == 1) { + val exportsNode = values.get(0) + (exportsNode.getKeyNode, exportsNode.getValueNode) match { + case (exportsKeyNode: ScalarNode, seqNode: SequenceNode) + if exportsKeyNode.getValue == Fields.Exports => + for { + group <- groupDecoder.decode(groupNode) + exports <- exportsDecoder.decode(seqNode) + } yield ExtendedComponentGroup(group, exports) + case _ => + Left( + new YAMLException( + "Failed to decode Extended ComponentGroup" + ) + ) + } + } else { + Left( + new YAMLException( + "Failed to decode Extended Component Group" + ) + ) + } + case _ => + Left( + new YAMLException( + "Failed to decode Component Group's name in " + groupNode + ) + ) + } + } else { + Left( + new YAMLException("Failed to decode Component Group's name") + ) + } + case scalarNode: ScalarNode => + val groupDecoder = implicitly[YamlDecoder[GroupReference]] + groupDecoder + .decode(scalarNode) + .map(ExtendedComponentGroup(_, Seq.empty)) + } + } + + implicit val yamlEncoder: YamlEncoder[ExtendedComponentGroup] = + new YamlEncoder[ExtendedComponentGroup] { + override def encode(value: ExtendedComponentGroup): Object = { + val groupReferenceEncoder = implicitly[YamlEncoder[GroupReference]] + val componentsEncoder = implicitly[YamlEncoder[Seq[Component]]] + + val groupReferenceNode = + groupReferenceEncoder.encode(value.group).asInstanceOf[String] + if (value.exports.nonEmpty) + toMap( + groupReferenceNode, + toMap("exports", componentsEncoder.encode(value.exports)) + ) + else + groupReferenceNode + } + } + /** [[Encoder]] instance for the [[ExtendedComponentGroup]]. */ implicit val encoder: Encoder[ExtendedComponentGroup] = { extendedComponentGroup => @@ -200,9 +415,63 @@ case class Component(name: String, shortcut: Option[Shortcut]) object Component { object Fields { + val Name = "name" val Shortcut = "shortcut" } + implicit val yamlDecoder: YamlDecoder[Component] = + new YamlDecoder[Component] { + override def decode(node: Node): Either[Throwable, Component] = + node match { + case mappingNode: MappingNode => + if (mappingNode.getValue.size() == 1) { + val componentNode = mappingNode.getValue.get(0) + (componentNode.getKeyNode, componentNode.getValueNode) match { + case (scalarNode: ScalarNode, mappingNode: MappingNode) => + val stringDecoder = implicitly[YamlDecoder[String]] + val shortcutDecoder = + implicitly[YamlDecoder[Option[Shortcut]]] + for { + name <- stringDecoder.decode(scalarNode) + shortcut <- shortcutDecoder + .decode(mappingNode) + .map(_.filter(_.key.nonEmpty)) + } yield Component(name, shortcut) + case (keyNode: ScalarNode, _: ScalarNode) => + Left( + new YAMLException( + "Failed to decode exported component '" + keyNode.getValue + "'" + ) + ) + case _ => + Left( + new YAMLException( + "Failed to decode Component" + ) + ) + } + } else { + Left(new YAMLException("Failed to decode Component")) + } + case scalarNode: ScalarNode => + val stringDecoder = implicitly[YamlDecoder[String]] + stringDecoder.decode(scalarNode).map(Component(_, None)) + } + } + + implicit val yamlEncoder: YamlEncoder[Component] = + new YamlEncoder[Component] { + override def encode(value: Component) = { + if (value.shortcut.isEmpty) { + value.name + } else { + val shortcutEncoder = implicitly[YamlEncoder[Shortcut]] + val shortcutNode = value.shortcut.map(shortcutEncoder.encode(_)).get + toMap(value.name, shortcutNode) + } + } + } + /** [[Encoder]] instance for the [[Component]]. */ implicit val encoder: Encoder[Component] = { component => component.shortcut match { @@ -256,6 +525,44 @@ object Component { case class Shortcut(key: String) object Shortcut { + object Fields { + val Key = "shortcut" + } + + implicit val yamlDecoder: YamlDecoder[Shortcut] = + new YamlDecoder[Shortcut] { + override def decode(node: Node): Either[Throwable, Shortcut] = + node match { + case mappingNode: MappingNode => + val stringDecoder = implicitly[YamlDecoder[String]] + val shortcutNode = mappingNode.getValue.get(0) + (shortcutNode.getKeyNode, shortcutNode.getValueNode) match { + case (key: ScalarNode, valueNode) if key.getValue == Fields.Key => + valueNode match { + case valueNode: ScalarNode => + stringDecoder.decode(valueNode).map(Shortcut(_)) + case _: SequenceNode => + Left( + new YAMLException( + "Failed to decode shortcut. Expected a string value, got a sequence" + ) + ) + case _ => + Left(new YAMLException("Failed to decode Shortcut")) + } + case _ => + Left(new YAMLException("Failed to decode Shortcut")) + } + } + } + + implicit val yamlEncoder: YamlEncoder[Shortcut] = + new YamlEncoder[Shortcut] { + override def encode(value: Shortcut) = { + toMap(Fields.Key, value.key) + } + } + /** [[Encoder]] instance for the [[Shortcut]]. */ implicit val encoder: Encoder[Shortcut] = { shortcut => shortcut.key.asJson @@ -307,6 +614,31 @@ object GroupReference { case _ => None } + + object Fields { + val LibraryName = "library-name" + val GroupName = "group-name" + } + + implicit val yamlDecoder: YamlDecoder[GroupReference] = + new YamlDecoder[GroupReference] { + override def decode(node: Node): Either[Throwable, GroupReference] = + node match { + case scalarNode: ScalarNode => + fromModuleName(scalarNode.getValue).toRight( + new YAMLException( + s"Failed to decode '${scalarNode.getValue}' as a module reference" + ) + ) + } + } + + implicit val yamlEncoder: YamlEncoder[GroupReference] = + new YamlEncoder[GroupReference] { + override def encode(value: GroupReference) = { + value.libraryName.qualifiedName + LibraryName.separator + value.groupName.name + } + } } /** The module name. @@ -316,6 +648,20 @@ object GroupReference { case class GroupName(name: String) object GroupName { + object Fields { + val Name = "name" + } + + implicit val yamlDecoder: YamlDecoder[GroupName] = + new YamlDecoder[GroupName] { + override def decode(node: Node): Either[Throwable, GroupName] = + node match { + case scalarNode: ScalarNode => + val stringDecoder = implicitly[YamlDecoder[String]] + stringDecoder.decode(scalarNode).map(GroupName(_)) + } + } + /** Create a [[GroupName]] from its components. */ def fromComponents(item: String, items: List[String]): GroupName = GroupName((item :: items).mkString(LibraryName.separator.toString)) diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala index 8a0b530f7d61..9ad83acdb404 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala @@ -1,20 +1,16 @@ package org.enso.pkg -import io.circe._ -import io.circe.syntax._ -import io.circe.yaml.{Parser, Printer} +import org.yaml.snakeyaml.nodes.Tag import org.enso.semver.SemVer -import org.enso.editions.EditionSerialization._ -import org.enso.editions.{ - DefaultEnsoVersion, - EditionName, - Editions, - EnsoVersion, - SemVerEnsoVersion -} +import org.enso.editions.{EditionName, Editions} import org.enso.pkg.validation.NameValidation +import org.enso.yaml.{YamlDecoder, YamlEncoder} +import org.yaml.snakeyaml.{DumperOptions, Yaml} +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.nodes.{MappingNode, Node} -import java.io.Reader +import java.io.{Reader, StringReader} +import java.util import scala.util.Try /** Contact information to a user. @@ -46,35 +42,38 @@ object Contact { val Email = "email" } - /** [[Encoder]] instance for the [[Contact]]. */ - implicit val encoder: Encoder[Contact] = { contact => - val name = contact.name.map(Fields.Name -> _.asJson) - val email = contact.email.map(Fields.Email -> _.asJson) - Json.obj((name.toSeq ++ email.toSeq): _*) - } - - /** [[Decoder]] instance for the [[Contact]]. - */ - implicit val decoder: Decoder[Contact] = { json => - def verifyAtLeastOneDefined( - name: Option[String], - email: Option[String] - ): Either[DecodingFailure, Unit] = - if (name.isEmpty && email.isEmpty) - Left( - DecodingFailure( - "At least one of the fields `name`, `email` must be defined.", - json.history - ) - ) - else Right(()) + implicit val decoderSnake: YamlDecoder[Contact] = + new YamlDecoder[Contact] { + override def decode(node: Node): Either[Throwable, Contact] = node match { + case mappingNode: MappingNode => + val optString = implicitly[YamlDecoder[Option[String]]] + val bindings = mappingKV(mappingNode) + for { + name <- bindings + .get(Fields.Name) + .map(optString.decode) + .getOrElse(Right(None)) + email <- bindings + .get(Fields.Email) + .map(optString.decode) + .getOrElse(Right(None)) + } yield Contact(name, email) + } + } - for { - name <- json.getOrElse[Option[String]](Fields.Name)(None) - email <- json.getOrElse[Option[String]](Fields.Email)(None) - _ <- verifyAtLeastOneDefined(name, email) - } yield Contact(name, email) - } + implicit val encoderSnake: YamlEncoder[Contact] = + new YamlEncoder[Contact] { + override def encode(value: Contact) = { + val elements = new util.ArrayList[(String, Object)]() + value.name + .map((Fields.Name, _)) + .foreach(elements.add) + value.email + .map((Fields.Email, _)) + .foreach(elements.add) + toMap(elements) + } + } } /** Represents a package configuration stored in the `package.yaml` file. @@ -114,8 +113,14 @@ case class Config( ) { /** Converts the configuration into a YAML representation. */ - def toYaml: String = - Printer.spaces2.copy(preserveOrder = true).pretty(Config.encoder(this)) + def toYaml: String = { + val node = implicitly[YamlEncoder[Config]].encode(this) + val dumperOptions = new DumperOptions() + dumperOptions.setIndent(2) + dumperOptions.setPrettyFlow(true) + val yaml = new Yaml(dumperOptions) + yaml.dumpAs(node, Tag.MAP, DumperOptions.FlowStyle.BLOCK) + } /** @return the module of name. */ def moduleName: String = @@ -125,121 +130,182 @@ case class Config( object Config { - val defaultNamespace: String = "local" + val DefaultNamespace: String = "local" + val DefaultVersion: String = "dev" + val DefaultLicense: String = "" + val DefaultPreferLocalLibraries = false private object JsonFields { - val name: String = "name" - val normalizedName: String = "normalized-name" - val version: String = "version" - val ensoVersion: String = "enso-version" - val license: String = "license" - val author: String = "authors" - val namespace: String = "namespace" - val maintainer: String = "maintainers" - val edition: String = "edition" - val preferLocalLibraries = "prefer-local-libraries" - val componentGroups = "component-groups" + val Name: String = "name" + val NormalizedName: String = "normalized-name" + val Version: String = "version" + val EnsoVersion: String = "enso-version" + val License: String = "license" + val Author: String = "authors" + val Namespace: String = "namespace" + val Maintainer: String = "maintainers" + val Edition: String = "edition" + val PreferLocalLibraries = "prefer-local-libraries" + val ComponentGroups = "component-groups" } - implicit val decoder: Decoder[Config] = { json => - for { - name <- json.get[String](JsonFields.name) - normalizedName <- json.get[Option[String]](JsonFields.normalizedName) - namespace <- json.getOrElse[String](JsonFields.namespace)( - defaultNamespace - ) - version <- json.getOrElse[String](JsonFields.version)("dev") - ensoVersion <- json.get[Option[EnsoVersion]](JsonFields.ensoVersion) - rawEdition <- json - .get[EditionName](JsonFields.edition) - .map(x => Left(x.name)) - .orElse( - json - .get[Option[Editions.RawEdition]](JsonFields.edition) - .map(Right(_)) - ) - edition = rawEdition.fold( - editionName => Some(Editions.Raw.Edition(parent = Some(editionName))), - identity - ) - license <- json.getOrElse(JsonFields.license)("") - author <- json.getOrElse[List[Contact]](JsonFields.author)(List()) - maintainer <- json.getOrElse[List[Contact]](JsonFields.maintainer)(List()) - preferLocal <- - json.getOrElse[Boolean](JsonFields.preferLocalLibraries)(false) - finalEdition <- - editionOrVersionBackwardsCompatibility(edition, ensoVersion).left.map { - error => DecodingFailure(error, json.history) - } - componentGroups <- json.getOrElse[Option[ComponentGroups]]( - JsonFields.componentGroups - )(None) - } yield { - - Config( - name = name, - normalizedName = normalizedName, - namespace = namespace, - version = version, - license = license, - authors = author, - maintainers = maintainer, - edition = finalEdition, - preferLocalLibraries = preferLocal, - componentGroups = componentGroups - ) - } - } - - implicit val encoder: Encoder[Config] = { config => - val edition = config.edition - .map { edition => - if (edition.isDerivingWithoutOverrides) edition.parent.get.asJson - else edition.asJson + implicit val yamlDecoder: YamlDecoder[Config] = + new YamlDecoder[Config] { + override def decode(node: Node): Either[Throwable, Config] = node match { + case mappingNode: MappingNode => + val clazzMap = mappingKV(mappingNode) + val stringDecoder = implicitly[YamlDecoder[String]] + val normalizedNameDecoder = + implicitly[YamlDecoder[Option[String]]] + val contactDecoder = implicitly[YamlDecoder[List[Contact]]] + val editionNameDecoder = implicitly[YamlDecoder[EditionName]] + val editionDecoder = + implicitly[YamlDecoder[Option[Editions.RawEdition]]] + val booleanDecoder = implicitly[YamlDecoder[Boolean]] + val componentGroups = + implicitly[YamlDecoder[Option[ComponentGroups]]] + for { + name <- clazzMap + .get(JsonFields.Name) + .toRight( + new YAMLException(s"Missing '${JsonFields.Name}' field") + ) + .flatMap(stringDecoder.decode) + normalizedName <- clazzMap + .get(JsonFields.NormalizedName) + .map(normalizedNameDecoder.decode) + .getOrElse(Right(None)) + namespace <- clazzMap + .get(JsonFields.Namespace) + .map(stringDecoder.decode) + .getOrElse(Right(DefaultNamespace)) + version <- clazzMap + .get(JsonFields.Version) + .map(stringDecoder.decode) + .getOrElse(Right(DefaultVersion)) + license <- clazzMap + .get(JsonFields.License) + .map(stringDecoder.decode) + .getOrElse(Right(DefaultLicense)) + authors <- clazzMap + .get(JsonFields.Author) + .map(contactDecoder.decode) + .getOrElse(Right(Nil)) + maintainers <- clazzMap + .get(JsonFields.Maintainer) + .map(contactDecoder.decode) + .getOrElse(Right(Nil)) + rawEdition = clazzMap + .get(JsonFields.Edition) + .flatMap(x => editionNameDecoder.decode(x).toOption.map(Left(_))) + .getOrElse( + clazzMap + .get(JsonFields.Edition) + .map(editionDecoder.decode) + .getOrElse(Right(None)) + ) + .asInstanceOf[Either[EditionName, Option[Editions.RawEdition]]] + edition <- rawEdition.fold( + editionName => + Right( + Some(Editions.Raw.Edition(parent = Some(editionName.name))) + ), + r => Right(r) + ) + preferLocalLibraries <- clazzMap + .get(JsonFields.PreferLocalLibraries) + .map(booleanDecoder.decode) + .getOrElse(Right(DefaultPreferLocalLibraries)) + componentGroups <- clazzMap + .get(JsonFields.ComponentGroups) + .map(componentGroups.decode) + .getOrElse(Right(None)) + } yield Config( + name, + normalizedName, + namespace, + version, + license, + authors, + maintainers, + edition, + preferLocalLibraries, + componentGroups + ) } - .map(JsonFields.edition -> _) + } - val componentGroups = - Option.unless( - config.componentGroups.isEmpty - )( - JsonFields.componentGroups -> config.componentGroups.asJson - ) + implicit val encoderSnake: YamlEncoder[Config] = + new YamlEncoder[Config] { + override def encode(value: Config) = { + val contactsEncoder = implicitly[YamlEncoder[List[Contact]]] + val editionEncoder = implicitly[YamlEncoder[Editions.RawEdition]] + val booleanEncoder = implicitly[YamlEncoder[Boolean]] + val componentGroupsEncoder = + implicitly[YamlEncoder[ComponentGroups]] - val normalizedName = config.normalizedName.map(value => - JsonFields.normalizedName -> value.asJson - ) + val elements = new util.ArrayList[(String, Object)]() + elements.add((JsonFields.Name, value.name)) + value.normalizedName.foreach(v => + elements.add((JsonFields.NormalizedName, v)) + ) + if (value.namespace != DefaultNamespace) + elements.add((JsonFields.Namespace, value.namespace)) + if (value.version != DefaultVersion) + elements.add( + (JsonFields.Version, value.version) + ) + if (value.license != DefaultLicense) + elements.add( + (JsonFields.License, value.license) + ) + if (value.authors.nonEmpty) { + elements.add( + (JsonFields.Author, contactsEncoder.encode(value.authors)) + ) + } + if (value.maintainers.nonEmpty) { + elements.add( + (JsonFields.Maintainer, contactsEncoder.encode(value.maintainers)) + ) + } - val overrides = - Seq(JsonFields.name -> config.name.asJson) ++ - normalizedName.toSeq ++ - Seq( - JsonFields.namespace -> config.namespace.asJson, - JsonFields.version -> config.version.asJson, - JsonFields.license -> config.license.asJson, - JsonFields.author -> config.authors.asJson, - JsonFields.maintainer -> config.maintainers.asJson - ) ++ edition.toSeq ++ componentGroups.toSeq + value.edition.foreach { edition => + if (edition.isDerivingWithoutOverrides) + elements.add((JsonFields.Edition, edition.parent.get)) + else + elements.add((JsonFields.Edition, editionEncoder.encode(edition))) + } + if (value.preferLocalLibraries != DefaultPreferLocalLibraries) + elements.add( + ( + JsonFields.PreferLocalLibraries, + booleanEncoder.encode(value.preferLocalLibraries) + ) + ) + value.componentGroups.foreach(v => + elements.add( + (JsonFields.ComponentGroups, componentGroupsEncoder.encode(v)) + ) + ) - val preferLocalOverride = - if (config.preferLocalLibraries) - Seq(JsonFields.preferLocalLibraries -> true.asJson) - else Seq() - val overridesObject = JsonObject( - overrides ++ preferLocalOverride: _* - ) + toMap(elements) + } + } - overridesObject.asJson + /** Tries to parse the [[Config]] directly from the Reader */ + def fromYaml(reader: Reader): Try[Config] = { + val snakeYaml = new org.yaml.snakeyaml.Yaml() + Try(snakeYaml.compose(reader)).toEither + .flatMap(implicitly[YamlDecoder[Config]].decode(_)) + .toTry } - /** Tries to parse the [[Config]] from a YAML string. */ def fromYaml(yamlString: String): Try[Config] = { - yaml.parser.parse(yamlString).flatMap(_.as[Config]).toTry - } - - /** Tries to parse the [[Config]] directly from the Reader */ - def fromYaml(reader: Reader): Try[Config] = { - Parser.default.parse(reader).flatMap(_.as[Config]).toTry + val snakeYaml = new org.yaml.snakeyaml.Yaml() + Try(snakeYaml.compose(new StringReader(yamlString))).toEither + .flatMap(implicitly[YamlDecoder[Config]].decode(_)) + .toTry } /** Creates a simple edition that just defines the provided engine version. @@ -262,37 +328,4 @@ object Config { repositories = Map(), libraries = Map() ) - - /** A helper method that reconciles the old and new fields of the config - * related to the edition. - * - * If an edition is present, it is just returned as-is. If the engine version - * is specified, a special edition is created that specifies this particular - * engine version and nothing else. - * - * If both fields are defined, an error is raised as the configuration may be - * inconsistent - the `engine-version` field should only be present in old - * configs and after migration to the edition format it should be removed. - */ - private def editionOrVersionBackwardsCompatibility( - edition: Option[Editions.RawEdition], - ensoVersion: Option[EnsoVersion] - ): Either[String, Option[Editions.RawEdition]] = - (edition, ensoVersion) match { - case (Some(_), Some(_)) => - Left( - s"The deprecated `${JsonFields.ensoVersion}` should not be defined " + - s"if the `${JsonFields.edition}` that replaces it is already defined." - ) - case (Some(edition), _) => - Right(Some(edition)) - case (_, Some(SemVerEnsoVersion(version))) => - Right(Some(makeCompatibilityEditionFromVersion(version))) - case (_, Some(DefaultEnsoVersion)) => - // If the `default` version is specified, we return None, so that later - // on, it will fallback to the default edition. - Right(None) - case (None, None) => - Right(None) - } } diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala index d880f9c020ed..a4ff24d07c81 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala @@ -1,6 +1,5 @@ package org.enso.pkg -import cats.Show import org.enso.editions.{Editions, LibraryName} import org.enso.filesystem.FileSystem import org.enso.pkg.validation.NameValidation @@ -350,15 +349,6 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) { result.recoverWith { case packageLoadingException: PackageManager.PackageLoadingException => Failure(packageLoadingException) - case decodingError: io.circe.Error => - val errorMessage = - implicitly[Show[io.circe.Error]].show(decodingError) - Failure( - PackageManager.PackageLoadingFailure( - s"Cannot decode the package config: $errorMessage", - decodingError - ) - ) case otherError => Failure( PackageManager.PackageLoadingFailure( diff --git a/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala b/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala index cdd784248676..d813d7fc0912 100644 --- a/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala +++ b/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala @@ -1,12 +1,11 @@ package org.enso.pkg -import cats.Show -import io.circe.{DecodingFailure, Json} import org.enso.semver.SemVer import org.enso.editions.LibraryName import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatest.{Inside, OptionValues} +import org.yaml.snakeyaml.error.YAMLException import scala.util.Failure @@ -17,22 +16,6 @@ class ConfigSpec with OptionValues { "Config" should { - "preserve unknown keys when deserialized and serialized again" ignore { - val original = Json.obj( - "name" -> Json.fromString("name"), - "unknown-key" -> Json.fromString("value") - ) - - inside(original.as[Config]) { case Right(config) => - val serialized = Config.encoder(config) - serialized.asObject - .value("unknown-key") - .value - .asString - .value shouldEqual "value" - } - } - "deserialize the serialized representation to the original value" in { val config = Config( name = "placeholder", @@ -194,8 +177,8 @@ class ConfigSpec val parsed = Config.fromYaml(config) parsed match { - case Failure(f: DecodingFailure) => - Show[DecodingFailure].show(f) should include( + case Failure(failure: YAMLException) => + failure.getMessage should include( "Failed to decode 'Group 1' as a module reference" ) case unexpected => @@ -254,9 +237,9 @@ class ConfigSpec |""".stripMargin val parsed = Config.fromYaml(config) parsed match { - case Failure(f: DecodingFailure) => - Show[DecodingFailure].show(f) should include( - "Failed to decode shortcut" + case Failure(failure: YAMLException) => + failure.getMessage should equal( + "Failed to decode shortcut. Expected a string value, got a sequence" ) case unexpected => fail(s"Unexpected result: $unexpected") @@ -273,9 +256,9 @@ class ConfigSpec |""".stripMargin val parsed = Config.fromYaml(config) parsed match { - case Failure(f: DecodingFailure) => - Show[DecodingFailure].show(f) should include( - "Failed to decode component group" + case Failure(failure: YAMLException) => + failure.getMessage should equal( + "Failed to decode component group. Expected a mapping, got a sequence" ) case unexpected => fail(s"Unexpected result: $unexpected") @@ -293,9 +276,9 @@ class ConfigSpec |""".stripMargin val parsed = Config.fromYaml(config) parsed match { - case Failure(f: DecodingFailure) => - Show[DecodingFailure].show(f) should include( - "Failed to decode exported component" + case Failure(failure: YAMLException) => + failure.getMessage should equal( + "Failed to decode exported component 'one'" ) case unexpected => fail(s"Unexpected result: $unexpected") diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala index b4acc7874eed..8491e866a364 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala @@ -108,7 +108,7 @@ class ProjectService[ id = projectId, name = name, module = moduleName, - namespace = Config.defaultNamespace, + namespace = Config.DefaultNamespace, kind = UserProject, created = creationTime, edition = None, diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/config/GlobalConfigService.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/config/GlobalConfigService.scala index 203ee2d4ae80..4817fb6a5eba 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/config/GlobalConfigService.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/config/GlobalConfigService.scala @@ -1,6 +1,5 @@ package org.enso.projectmanager.service.config -import io.circe.Json import org.enso.semver.SemVer import org.enso.editions.{DefaultEnsoVersion, EnsoVersion, SemVerEnsoVersion} import org.enso.projectmanager.control.core.CovariantFlatMap @@ -36,7 +35,7 @@ class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap]( key: String, value: String ): F[GlobalConfigServiceFailure, Unit] = Sync[F].blockingOp { - configurationManager.updateConfigRaw(key, Json.fromString(value)) + configurationManager.updateConfigRaw(key, value) }.recoverAccessErrors /** @inheritdoc */ diff --git a/lib/scala/runtime-version-manager-test/src/test/scala/org/enso/runtimeversionmanager/config/GlobalConfigurationManagerSpec.scala b/lib/scala/runtime-version-manager-test/src/test/scala/org/enso/runtimeversionmanager/config/GlobalConfigurationManagerSpec.scala index b500dfcbe06e..5b9bec8a47ab 100644 --- a/lib/scala/runtime-version-manager-test/src/test/scala/org/enso/runtimeversionmanager/config/GlobalConfigurationManagerSpec.scala +++ b/lib/scala/runtime-version-manager-test/src/test/scala/org/enso/runtimeversionmanager/config/GlobalConfigurationManagerSpec.scala @@ -1,6 +1,5 @@ package org.enso.runtimeversionmanager.config -import io.circe.Json import org.enso.semver.SemVer import org.enso.distribution.DistributionManager import org.enso.distribution.config.InvalidConfigError @@ -27,16 +26,16 @@ class GlobalConfigurationManagerSpec "GlobalConfigurationManager" should { "allow to edit and remove known keys" in { val configurationManager = makeConfigManager() - val value = Json.fromInt(42) + val value = 42.toString configurationManager.updateConfigRaw("unknown-key", value) configurationManager.getConfig .findByKey("unknown-key") should not be defined - val newEmail = Json.fromString("foo@bar.com") + val newEmail = "foo@bar.com" configurationManager.getConfig .findByKey("author.email") should not be defined configurationManager.updateConfigRaw("author.email", newEmail) configurationManager.getConfig - .findByKey("author.email") shouldEqual newEmail.asString + .findByKey("author.email") shouldEqual Some(newEmail) } "not allow saving an invalid config" in { @@ -44,7 +43,7 @@ class GlobalConfigurationManagerSpec intercept[InvalidConfigError] { configurationManager.updateConfigRaw( "default.enso-version", - Json.fromString("invalid-version") + "invalid-version" ) } } diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/Manifest.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/Manifest.scala index d280f21f473d..13abe895ac5c 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/Manifest.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/Manifest.scala @@ -1,18 +1,19 @@ package org.enso.runtimeversionmanager.components -import java.io.FileReader +import java.io.{FileReader, StringReader} import java.nio.file.Path -import cats.Show -import io.circe.yaml.Parser -import io.circe.{yaml, Decoder} +import org.enso import org.enso.semver.SemVer import org.enso.cli.OS -import org.enso.semver.SemVerJson._ +import org.enso.semver.SemVerYaml._ import org.enso.runtimeversionmanager.components.Manifest.{ JVMOption, RequiredInstallerVersions } import org.enso.runtimeversionmanager.components +import org.enso.yaml.{ParseError, YamlDecoder} +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.nodes.{MappingNode, Node} import scala.util.{Failure, Try, Using} @@ -70,6 +71,39 @@ object Manifest { */ case class RequiredInstallerVersions(launcher: SemVer, projectManager: SemVer) + object RequiredInstallerVersions { + implicit val yamlDecoder: YamlDecoder[RequiredInstallerVersions] = + new YamlDecoder[RequiredInstallerVersions] { + override def decode( + node: Node + ): Either[Throwable, RequiredInstallerVersions] = { + node match { + case mappingNode: MappingNode => + val semverDecoder = implicitly[YamlDecoder[SemVer]] + val bindings = mappingKV(mappingNode) + for { + launcher <- bindings + .get(Manifest.Fields.MinimumLauncherVersion) + .toRight( + new YAMLException( + s"Missing `${Manifest.Fields.MinimumLauncherVersion}` field" + ) + ) + .flatMap(semverDecoder.decode) + projectManager <- bindings + .get(Manifest.Fields.MinimumProjectManagerVersion) + .toRight( + new YAMLException( + s"Missing `${Manifest.Fields.MinimumProjectManagerVersion}` field" + ) + ) + .flatMap(semverDecoder.decode) + } yield RequiredInstallerVersions(launcher, projectManager) + } + } + } + } + /** Defines the name under which the manifest is included in the releases. */ val DEFAULT_MANIFEST_NAME = "manifest.yaml" @@ -109,24 +143,31 @@ object Manifest { object JVMOption { private object Fields { - val os = "os" - val value = "value" + val Os = "os" + val Value = "value" } - /** [[Decoder]] instance that allows to parse the [[JVMOption]] from the - * YAML manifest. - */ - implicit val decoder: Decoder[JVMOption] = { json => - val hasOSKey = json.keys.exists { keyList: Iterable[String] => - keyList.toSeq.contains(Fields.os) + implicit val yamlDecoder: YamlDecoder[JVMOption] = + new YamlDecoder[JVMOption] { + override def decode(node: Node): Either[Throwable, JVMOption] = { + node match { + case node: MappingNode => + val bindings = mappingKV(node) + val stringDecoder = implicitly[YamlDecoder[String]] + val OSdecoder = implicitly[YamlDecoder[OS]] + for { + value <- bindings + .get(Fields.Value) + .toRight(new YAMLException(s"missing `${Fields.Value} field")) + .flatMap(stringDecoder.decode) + osRestriction <- bindings + .get(Fields.Os) + .map(OSdecoder.decode(_).map(Some(_))) + .getOrElse(Right(None)) + } yield JVMOption(value, osRestriction) + } + } } - - for { - value <- json.get[String](Fields.value) - osRestriction <- - if (hasOSKey) json.get[OS](Fields.os).map(Some(_)) else Right(None) - } yield JVMOption(value, osRestriction) - } } /** Tries to load the manifest at the given path. @@ -135,10 +176,11 @@ object Manifest { */ def load(path: Path): Try[Manifest] = Using(new FileReader(path.toFile)) { reader => - Parser.default - .parse(reader) - .flatMap(_.as[Manifest]) - .toTry + val snakeYaml = new org.yaml.snakeyaml.Yaml() + Try(snakeYaml.compose(reader)) + .flatMap( + implicitly[enso.yaml.YamlDecoder[Manifest]].decode(_).toTry + ) }.flatten.recoverWith { error => Failure(ManifestLoadingError.fromThrowable(error)) } @@ -148,9 +190,11 @@ object Manifest { * Returns None if the definition cannot be parsed. */ def fromYaml(yamlString: String): Try[Manifest] = { - yaml.parser - .parse(yamlString) - .flatMap(_.as[Manifest]) + val snakeYaml = new org.yaml.snakeyaml.Yaml() + Try(snakeYaml.compose(new StringReader(yamlString))).toEither + .flatMap(implicitly[enso.yaml.YamlDecoder[Manifest]].decode(_)) + .left + .map(ParseError(_)) .toTry .recoverWith { error => Failure(ManifestLoadingError.fromThrowable(error)) @@ -176,49 +220,68 @@ object Manifest { */ def fromThrowable(throwable: Throwable): ManifestLoadingError = throwable match { - case decodingError: io.circe.Error => - val errorMessage = - implicitly[Show[io.circe.Error]].show(decodingError) - ManifestLoadingError( - s"Could not parse the manifest: $errorMessage", - decodingError - ) case other => ManifestLoadingError(s"Could not load the manifest: $other", other) } } object Fields { - val minimumLauncherVersion = "minimum-launcher-version" - val minimumProjectManagerVersion = "minimum-project-manager-version" - val jvmOptions = "jvm-options" - val graalVMVersion = "graal-vm-version" - val graalJavaVersion = "graal-java-version" - val brokenMark = "broken" + val MinimumLauncherVersion = "minimum-launcher-version" + val MinimumProjectManagerVersion = "minimum-project-manager-version" + val JvmOptions = "jvm-options" + val GraalVMVersion = "graal-vm-version" + val GraalJavaVersion = "graal-java-version" + val NrokenMark = "broken" } - implicit private val decoder: Decoder[Manifest] = { json => - for { - minimumLauncherVersion <- json.get[SemVer](Fields.minimumLauncherVersion) - minimumProjectManagerVersion <- json.get[SemVer]( - Fields.minimumProjectManagerVersion - ) - graalVMVersion <- json.get[String](Fields.graalVMVersion) - graalJavaVersion <- - json - .get[String](Fields.graalJavaVersion) - .orElse(json.get[Int](Fields.graalJavaVersion).map(_.toString)) - jvmOptions <- json.getOrElse[Seq[JVMOption]](Fields.jvmOptions)(Seq()) - broken <- json.getOrElse[Boolean](Fields.brokenMark)(false) - } yield Manifest( - requiredInstallerVersions = RequiredInstallerVersions( - launcher = minimumLauncherVersion, - projectManager = minimumProjectManagerVersion - ), - graalVMVersion = graalVMVersion, - graalJavaVersion = graalJavaVersion, - jvmOptions = jvmOptions, - brokenMark = broken - ) - } + implicit val yamlDecoder: YamlDecoder[Manifest] = + new YamlDecoder[Manifest] { + override def decode(node: Node): Either[Throwable, Manifest] = { + node match { + case node: MappingNode => + val bindings = mappingKV(node) + val requiredInstallerVersionsDecoder = + implicitly[YamlDecoder[RequiredInstallerVersions]] + val stringDecoder = implicitly[YamlDecoder[String]] + val seqJVMOptionsDecoder = + implicitly[YamlDecoder[Seq[JVMOption]]] + val booleanDecoder = implicitly[YamlDecoder[Boolean]] + + for { + requiredInstallerVersions <- requiredInstallerVersionsDecoder + .decode(node) + graalVMVersion <- bindings + .get(Fields.GraalVMVersion) + .toRight( + new YAMLException( + s"Required `${Fields.GraalVMVersion}`field is missing" + ) + ) + .flatMap(stringDecoder.decode) + graalJavaVersion <- bindings + .get(Fields.GraalJavaVersion) + .toRight( + new YAMLException( + s"Required `${Fields.GraalJavaVersion}` field is missing" + ) + ) + .flatMap(stringDecoder.decode) + jvmOptions <- bindings + .get(Fields.JvmOptions) + .map(seqJVMOptionsDecoder.decode) + .getOrElse(Right(Seq.empty)) + brokenMark <- bindings + .get(Fields.NrokenMark) + .map(booleanDecoder.decode) + .getOrElse(Right(false)) + } yield Manifest( + requiredInstallerVersions, + graalVMVersion, + graalJavaVersion, + jvmOptions, + brokenMark + ) + } + } + } } diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/RuntimeVersionManager.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/RuntimeVersionManager.scala index 93c681b897cc..1eb1d8dc93cb 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/RuntimeVersionManager.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/RuntimeVersionManager.scala @@ -524,7 +524,7 @@ class RuntimeVersionManager( ) ) { writer => writer.newLine() - writer.write(s"${Manifest.Fields.brokenMark}: true\n") + writer.write(s"${Manifest.Fields.NrokenMark}: true\n") }.get } catch { case ex: Exception => @@ -733,7 +733,8 @@ class RuntimeVersionManager( private def loadAndCheckEngineManifest( path: Path ): Try[Manifest] = { - Manifest.load(path / Manifest.DEFAULT_MANIFEST_NAME).flatMap { manifest => + val manifestPath = path / Manifest.DEFAULT_MANIFEST_NAME + Manifest.load(manifestPath).flatMap { manifest => if (!isEngineVersionCompatibleWithThisInstaller(manifest)) { Failure( UpgradeRequiredError( diff --git a/lib/scala/semver/src/main/scala/org/enso/semver/SemVerJson.scala b/lib/scala/semver/src/main/scala/org/enso/semver/SemVerJson.scala index 25849798eaec..88a74318e638 100644 --- a/lib/scala/semver/src/main/scala/org/enso/semver/SemVerJson.scala +++ b/lib/scala/semver/src/main/scala/org/enso/semver/SemVerJson.scala @@ -26,7 +26,7 @@ object SemVerJson { Left( DecodingFailure( s"`$version` is not a valid semantic versioning string.", - json.history + if (json != null) json.history else Nil ) ) } @@ -35,4 +35,5 @@ object SemVerJson { /** [[Encoder]] instance allowing to serialize semantic versioning strings. */ implicit val semverEncoder: Encoder[SemVer] = _.toString.asJson + } diff --git a/lib/scala/semver/src/main/scala/org/enso/semver/SemVerYaml.scala b/lib/scala/semver/src/main/scala/org/enso/semver/SemVerYaml.scala new file mode 100644 index 000000000000..8db803815873 --- /dev/null +++ b/lib/scala/semver/src/main/scala/org/enso/semver/SemVerYaml.scala @@ -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 + } + } +} diff --git a/lib/scala/yaml/src/main/scala/org/enso/yaml/YamlDecoder.scala b/lib/scala/yaml/src/main/scala/org/enso/yaml/YamlDecoder.scala new file mode 100644 index 000000000000..1a5624abe6fe --- /dev/null +++ b/lib/scala/yaml/src/main/scala/org/enso/yaml/YamlDecoder.scala @@ -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()) + } + } + +} diff --git a/lib/scala/yaml/src/main/scala/org/enso/yaml/YamlEncoder.scala b/lib/scala/yaml/src/main/scala/org/enso/yaml/YamlEncoder.scala new file mode 100644 index 000000000000..cadc9f662515 --- /dev/null +++ b/lib/scala/yaml/src/main/scala/org/enso/yaml/YamlEncoder.scala @@ -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 + } + } +} diff --git a/package-lock.json b/package-lock.json index 930a45eb9d55..37e872ccd97a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -302,8 +302,7 @@ "@stripe/stripe-js": "^2.1.10", "esbuild-plugin-inline-image": "^0.0.9", "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "is-hidden-file": "^1.1.2" + "eslint-plugin-react-hooks": "^4.6.0" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^6.7.2", @@ -13781,21 +13780,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-hidden-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-hidden-file/-/is-hidden-file-1.1.2.tgz", - "integrity": "sha512-WS2Y+gFNWlK8IPAvcvsqa4rwf4kZUqGz3VTpHVhAu4Zvrbk+XPbve/RKacyyVNYxHQulubZshXXlzmfCR7G+WQ==", - "hasInstallScript": true, - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32" - ], - "dependencies": { - "cross-spawn": "^7.0.3" - } - }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", diff --git a/tools/legal-review/engine/io.circe.circe-yaml-common_2.13-0.15.1/copyright-keep-context b/tools/legal-review/engine/io.circe.circe-yaml-common_2.13-0.15.1/copyright-keep-context deleted file mode 100644 index d2ad096e1b58..000000000000 --- a/tools/legal-review/engine/io.circe.circe-yaml-common_2.13-0.15.1/copyright-keep-context +++ /dev/null @@ -1 +0,0 @@ -Copyright 2016 circe diff --git a/tools/legal-review/engine/io.circe.circe-yaml_2.13-0.15.1/copyright-keep-context b/tools/legal-review/engine/io.circe.circe-yaml_2.13-0.15.1/copyright-keep-context deleted file mode 100644 index d2ad096e1b58..000000000000 --- a/tools/legal-review/engine/io.circe.circe-yaml_2.13-0.15.1/copyright-keep-context +++ /dev/null @@ -1 +0,0 @@ -Copyright 2016 circe diff --git a/tools/legal-review/engine/io.circe.circe-yaml_2.13-0.15.1/files-ignore b/tools/legal-review/engine/io.circe.circe-yaml_2.13-0.15.1/files-ignore deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/tools/legal-review/engine/io.circe.circe-yaml_2.13-0.15.1/files-keep b/tools/legal-review/engine/io.circe.circe-yaml_2.13-0.15.1/files-keep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/tools/legal-review/engine/report-state b/tools/legal-review/engine/report-state index 806b091cf6bf..1d4993e9ac90 100644 --- a/tools/legal-review/engine/report-state +++ b/tools/legal-review/engine/report-state @@ -1,3 +1,3 @@ -EDA5F924FB902EA37C9FD0A8C8657C4B55E804E33F8E4F2A59802D657D448F43 -69A14ED5CFDD2E721976A524D46407779F7851C84B8ED9481CC6177B9E3C99A1 +8DE3C509911C2AB8D430D76BDB6FD401A8262BC700DA927E97A6CC9055B331C9 +1CCB55F023131497A0E6A16BB5B2D63E5D842572D8638017816EF1D5474B0169 0 diff --git a/tools/legal-review/launcher/io.circe.circe-yaml-common_2.13-0.15.1/copyright-keep-context b/tools/legal-review/launcher/io.circe.circe-yaml-common_2.13-0.15.1/copyright-keep-context deleted file mode 100644 index d2ad096e1b58..000000000000 --- a/tools/legal-review/launcher/io.circe.circe-yaml-common_2.13-0.15.1/copyright-keep-context +++ /dev/null @@ -1 +0,0 @@ -Copyright 2016 circe diff --git a/tools/legal-review/launcher/io.circe.circe-yaml_2.13-0.15.1/copyright-keep-context b/tools/legal-review/launcher/io.circe.circe-yaml_2.13-0.15.1/copyright-keep-context deleted file mode 100644 index d2ad096e1b58..000000000000 --- a/tools/legal-review/launcher/io.circe.circe-yaml_2.13-0.15.1/copyright-keep-context +++ /dev/null @@ -1 +0,0 @@ -Copyright 2016 circe diff --git a/tools/legal-review/launcher/io.circe.circe-yaml_2.13-0.15.1/files-ignore b/tools/legal-review/launcher/io.circe.circe-yaml_2.13-0.15.1/files-ignore deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/tools/legal-review/launcher/io.circe.circe-yaml_2.13-0.15.1/files-keep b/tools/legal-review/launcher/io.circe.circe-yaml_2.13-0.15.1/files-keep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/tools/legal-review/launcher/report-state b/tools/legal-review/launcher/report-state index 2935f81c5b0f..288ec5466753 100644 --- a/tools/legal-review/launcher/report-state +++ b/tools/legal-review/launcher/report-state @@ -1,3 +1,3 @@ -FD4B2B697E262E316088253505039F532B194D2DA6849BC7D4A6C7911DD6E404 -2DAD16605A78EDE6A4592684F8634387B4906BCA6932D33620CB2E23C7832215 +61FA814CA4FC0688FB059CC530561D4B5E329B33919A6DBEAD9CBD9C19D49337 +F356D9CC4CE4F118B02747EBA189642FCFB2EA96121262374402C3BA3B6B5ECD 0 diff --git a/tools/legal-review/project-manager/io.circe.circe-yaml-common_2.13-0.15.1/copyright-keep-context b/tools/legal-review/project-manager/io.circe.circe-yaml-common_2.13-0.15.1/copyright-keep-context deleted file mode 100644 index d2ad096e1b58..000000000000 --- a/tools/legal-review/project-manager/io.circe.circe-yaml-common_2.13-0.15.1/copyright-keep-context +++ /dev/null @@ -1 +0,0 @@ -Copyright 2016 circe diff --git a/tools/legal-review/project-manager/io.circe.circe-yaml_2.13-0.15.1/copyright-keep-context b/tools/legal-review/project-manager/io.circe.circe-yaml_2.13-0.15.1/copyright-keep-context deleted file mode 100644 index d2ad096e1b58..000000000000 --- a/tools/legal-review/project-manager/io.circe.circe-yaml_2.13-0.15.1/copyright-keep-context +++ /dev/null @@ -1 +0,0 @@ -Copyright 2016 circe diff --git a/tools/legal-review/project-manager/io.circe.circe-yaml_2.13-0.15.1/files-ignore b/tools/legal-review/project-manager/io.circe.circe-yaml_2.13-0.15.1/files-ignore deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/tools/legal-review/project-manager/io.circe.circe-yaml_2.13-0.15.1/files-keep b/tools/legal-review/project-manager/io.circe.circe-yaml_2.13-0.15.1/files-keep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/tools/legal-review/project-manager/report-state b/tools/legal-review/project-manager/report-state index dd5abc47add8..29bcd8219a41 100644 --- a/tools/legal-review/project-manager/report-state +++ b/tools/legal-review/project-manager/report-state @@ -1,3 +1,3 @@ -6A91D4302F22E9404B2BF4D85EFEDF4D7F363633A12499A5A3C7C244E9CB4E4E -D4E2379AA0DB83E264E6E580E39ABA46F3D0322CC79234EECAE2A79D39E46E74 +69E9EEFDB627E4C31E3C7184A7CF645047519F5D162D8BBA2715CB494C26E4FF +FC47A03D984C60193E6785C0EC0B681C0F8903F5AF4106AEB319F26F9B3A9CBB 0