From 1c3ad13df4f9d5648d2cfa115906ab79c5e253b4 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 11 Jul 2024 13:15:28 +0200 Subject: [PATCH] Crosscompile for JS and Native with scala-yaml as the backend (#431) * Crosscompile for JS and Native with scala-yaml as the backend This is rudimentary support for YAML on JS and Native. We could add much more tests etc., and maybe there are some bugs for edge cases, but this should be fine for the beginning. * tlVersionIntroduced 0.15.2 * tlVersionIntroduced 0.15.2 * Drop 2.12 in circe-yaml-scalayaml * headerCreate * Test/headerCreate * scalafixAll * import org.virtuslab.yaml.NodeOps * fix tests * circe-yaml-common tlVersionIntroduced 0.15.2 * Bring back 2.12 * scala-native 0.4.17 * Explicit crossScalaVersions * Scala.js 1.16.0 * Drop 2.12 * Simplify, rely on scala-yaml more * scala-yaml 0.1.0 * Update circe * fix * fix * fix * fix --- .github/workflows/ci.yml | 60 +++++- build.sbt | 37 ++-- .../scala/io/circe/yaml/common/Parser.scala | 0 .../scala/io/circe/yaml/common/Printer.scala | 0 .../io/circe/yaml/scalayaml/Parser.scala | 112 ++++++++++ .../io/circe/yaml/scalayaml/Printer.scala | 38 ++++ .../circe/yaml/scalayaml/parser/package.scala | 44 ++++ .../yaml/scalayaml/printer/package.scala | 28 +++ .../io/circe/yaml/scalayaml/ParserTests.scala | 194 ++++++++++++++++++ .../circe/yaml/scalayaml/PrinterTests.scala | 101 +++++++++ project/plugins.sbt | 3 + 11 files changed, 594 insertions(+), 23 deletions(-) rename circe-yaml-common/{ => shared}/src/main/scala/io/circe/yaml/common/Parser.scala (100%) rename circe-yaml-common/{ => shared}/src/main/scala/io/circe/yaml/common/Printer.scala (100%) create mode 100644 circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/Parser.scala create mode 100644 circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/Printer.scala create mode 100644 circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/parser/package.scala create mode 100644 circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/printer/package.scala create mode 100644 circe-yaml-scalayaml/shared/src/test/scala/io/circe/yaml/scalayaml/ParserTests.scala create mode 100644 circe-yaml-scalayaml/shared/src/test/scala/io/circe/yaml/scalayaml/PrinterTests.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58053117..bf51aec1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,14 +29,16 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - scala: [2.12, 2.13, 3] + scala: [2.13, 3] java: [temurin@11, temurin@17] - project: [rootJVM] + project: [rootJS, rootJVM, rootNative] exclude: - - scala: 2.12 - java: temurin@17 - scala: 3 java: temurin@17 + - project: rootJS + java: temurin@17 + - project: rootNative + java: temurin@17 runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: @@ -82,6 +84,14 @@ jobs: if: matrix.java == 'temurin@11' && matrix.os == 'ubuntu-latest' run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' 'scalafixAll --check' + - name: scalaJSLink + if: matrix.project == 'rootJS' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/scalaJSLinkerResult + + - name: nativeLink + if: matrix.project == 'rootNative' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/nativeLink + - name: Test run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' test @@ -95,11 +105,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p circe-yaml-v12/target circe-yaml-common/target circe-yaml/target project/target + run: mkdir -p circe-yaml-v12/target circe-yaml-common/jvm/target circe-yaml-common/native/target circe-yaml-common/js/target circe-yaml/target circe-yaml-scalayaml/jvm/target circe-yaml-scalayaml/js/target circe-yaml-scalayaml/native/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar circe-yaml-v12/target circe-yaml-common/target circe-yaml/target project/target + run: tar cf targets.tar circe-yaml-v12/target circe-yaml-common/jvm/target circe-yaml-common/native/target circe-yaml-common/js/target circe-yaml/target circe-yaml-scalayaml/jvm/target circe-yaml-scalayaml/js/target circe-yaml-scalayaml/native/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') @@ -149,12 +159,12 @@ jobs: if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' run: sbt +update - - name: Download target directories (2.12, rootJVM) + - name: Download target directories (2.13, rootJS) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJS - - name: Inflate target directories (2.12, rootJVM) + - name: Inflate target directories (2.13, rootJS) run: | tar xf targets.tar rm targets.tar @@ -169,6 +179,26 @@ jobs: tar xf targets.tar rm targets.tar + - name: Download target directories (2.13, rootNative) + uses: actions/download-artifact@v4 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootNative + + - name: Inflate target directories (2.13, rootNative) + run: | + tar xf targets.tar + rm targets.tar + + - name: Download target directories (3, rootJS) + uses: actions/download-artifact@v4 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJS + + - name: Inflate target directories (3, rootJS) + run: | + tar xf targets.tar + rm targets.tar + - name: Download target directories (3, rootJVM) uses: actions/download-artifact@v4 with: @@ -179,6 +209,16 @@ jobs: tar xf targets.tar rm targets.tar + - name: Download target directories (3, rootNative) + uses: actions/download-artifact@v4 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootNative + + - name: Inflate target directories (3, rootNative) + run: | + tar xf targets.tar + rm targets.tar + - name: Import signing key if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' env: @@ -246,5 +286,5 @@ jobs: - name: Submit Dependencies uses: scalacenter/sbt-dependency-submission@v2 with: - modules-ignore: rootjs_2.12 rootjs_2.13 rootjs_3 rootjvm_2.12 rootjvm_2.13 rootjvm_3 rootnative_2.12 rootnative_2.13 rootnative_3 + modules-ignore: rootjs_2.13 rootjs_3 rootjvm_2.13 rootjvm_3 rootnative_2.13 rootnative_3 configs-ignore: test scala-tool scala-doc-tool test-internal diff --git a/build.sbt b/build.sbt index 901db29c..c123b0f7 100644 --- a/build.sbt +++ b/build.sbt @@ -1,25 +1,23 @@ ThisBuild / tlBaseVersion := "0.15" ThisBuild / circeRootOfCodeCoverage := None ThisBuild / startYear := Some(2016) -ThisBuild / scalafixScalaBinaryVersion := "2.12" ThisBuild / tlFatalWarnings := false //TODO: ... fix this someday ThisBuild / githubWorkflowBuildMatrixFailFast := Some(false) val Versions = new { - val circe = "0.14.7" + val circe = "0.14.9" val discipline = "1.7.0" val scalaCheck = "1.18.0" - val scalaTest = "3.2.19" + val scalaTest = "3.2.18" val scalaTestPlus = "3.2.18.0" val snakeYaml = "2.2" val snakeYamlEngine = "2.7" val previousCirceYamls = Set("0.14.0", "0.14.1", "0.14.2") - val scala212 = "2.12.19" val scala213 = "2.13.14" val scala3 = "3.3.3" - val scalaVersions = Seq(scala212, scala213, scala3) + val scalaVersions = Seq(scala213, scala3) } ThisBuild / scalaVersion := Versions.scala213 @@ -28,22 +26,23 @@ ThisBuild / crossScalaVersions := Versions.scalaVersions val root = tlCrossRootProject.aggregate( `circe-yaml-common`, `circe-yaml`, - `circe-yaml-v12` + `circe-yaml-v12`, + `circe-yaml-scalayaml` ) -lazy val `circe-yaml-common` = project +lazy val `circe-yaml-common` = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("circe-yaml-common")) .settings( description := "Library for converting between SnakeYAML's AST (YAML 2.0) and circe's AST", libraryDependencies ++= Seq( - "io.circe" %% "circe-core" % Versions.circe + "io.circe" %%% "circe-core" % Versions.circe ), - tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.14.3").toMap + tlVersionIntroduced := List("2.13", "3").map(_ -> "0.15.3").toMap ) lazy val `circe-yaml` = project .in(file("circe-yaml")) - .dependsOn(`circe-yaml-common`) + .dependsOn(`circe-yaml-common`.jvm) .settings( description := "Library for converting between SnakeYAML's AST (YAML 2.0) and circe's AST", libraryDependencies ++= Seq( @@ -54,12 +53,13 @@ lazy val `circe-yaml` = project "org.scalacheck" %% "scalacheck" % Versions.scalaCheck % Test, "org.scalatest" %% "scalatest" % Versions.scalaTest % Test, "org.scalatestplus" %% "scalacheck-1-17" % Versions.scalaTestPlus % Test - ) + ), + tlVersionIntroduced := List("2.13", "3").map(_ -> "0.14.3").toMap ) lazy val `circe-yaml-v12` = project .in(file("circe-yaml-v12")) - .dependsOn(`circe-yaml-common`) + .dependsOn(`circe-yaml-common`.jvm) .settings( description := "Library for converting between snakeyaml-engine's AST (YAML 2.0) and circe's AST", libraryDependencies ++= Seq( @@ -71,7 +71,18 @@ lazy val `circe-yaml-v12` = project "org.scalatest" %% "scalatest" % Versions.scalaTest % Test, "org.scalatestplus" %% "scalacheck-1-17" % Versions.scalaTestPlus % Test ), - tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.14.3").toMap + tlVersionIntroduced := List("2.13", "3").map(_ -> "0.14.3").toMap + ) + +lazy val `circe-yaml-scalayaml` = crossProject(JSPlatform, JVMPlatform, NativePlatform) + .dependsOn(`circe-yaml-common`) + .settings( + description := "Library for converting between scala-yaml AST and circe's AST", + libraryDependencies ++= Seq( + "org.virtuslab" %%% "scala-yaml" % "0.1.0", + "org.scalatest" %%% "scalatest" % Versions.scalaTest % Test + ), + tlVersionIntroduced := List("2.13", "3").map(_ -> "0.15.3").toMap ) ThisBuild / developers := List( diff --git a/circe-yaml-common/src/main/scala/io/circe/yaml/common/Parser.scala b/circe-yaml-common/shared/src/main/scala/io/circe/yaml/common/Parser.scala similarity index 100% rename from circe-yaml-common/src/main/scala/io/circe/yaml/common/Parser.scala rename to circe-yaml-common/shared/src/main/scala/io/circe/yaml/common/Parser.scala diff --git a/circe-yaml-common/src/main/scala/io/circe/yaml/common/Printer.scala b/circe-yaml-common/shared/src/main/scala/io/circe/yaml/common/Printer.scala similarity index 100% rename from circe-yaml-common/src/main/scala/io/circe/yaml/common/Printer.scala rename to circe-yaml-common/shared/src/main/scala/io/circe/yaml/common/Printer.scala diff --git a/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/Parser.scala b/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/Parser.scala new file mode 100644 index 00000000..fcceb69c --- /dev/null +++ b/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/Parser.scala @@ -0,0 +1,112 @@ +/* + * 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. + */ + +package io.circe.yaml.scalayaml + +import cats.data.ValidatedNel +import cats.syntax.all._ +import io.circe +import io.circe.Decoder +import io.circe.Json +import io.circe.ParsingFailure +import org.virtuslab.yaml._ + +import java.io.Reader +import scala.collection.mutable + +object Parser extends io.circe.yaml.common.Parser { + + private def readerToString(yaml: Reader): String = { + val buffer = new Array[Char](4 * 1024) + val builder = new mutable.StringBuilder(4 * 1024) + var readBytes = -1 + while ({ readBytes = yaml.read(buffer); readBytes } > 0) + builder.appendAll(buffer, 0, readBytes) + builder.result() + } + + override def parse(yaml: Reader): Either[ParsingFailure, Json] = { + val string = readerToString(yaml) + parse(string) + } + + override def parseDocuments(yaml: Reader): Stream[Either[ParsingFailure, Json]] = { + val string = readerToString(yaml) + val parsed = parse(string) + Stream(parsed) + } + + override def parseDocuments(yaml: String): Stream[Either[ParsingFailure, Json]] = { + val parsed = parse(yaml) + Stream(parsed) + } + + override def decode[A: Decoder](input: Reader): Either[circe.Error, A] = { + val string = readerToString(input) + val parsed = parse(string) + parsed.flatMap(_.as[A]) + } + + override def decodeAccumulating[A: Decoder](input: Reader): ValidatedNel[circe.Error, A] = + decode(input).toValidatedNel + + override def parse(input: String): Either[ParsingFailure, Json] = { + val node = input.asNode + node match { + case Right(node) => nodeToJson(node) + case Left(error) => Left(errorToFailure(error)) + } + } + + private def scalarNodeToJson(node: Node.ScalarNode): Either[ParsingFailure, Json] = { + val parsed = YamlDecoder.forAny.construct(node).left.map(errorToFailure) + parsed.flatMap { + case null | None => Json.Null.asRight + case value: String => Json.fromString(value).asRight + case value: Int => Json.fromInt(value).asRight + case double: Double => + Json.fromDouble(double).toRight(ParsingFailure(s"${node.value} cannot be represented as a JSON number.", null)) + case value: Boolean => Json.fromBoolean(value).asRight + case value: Long => Json.fromLong(value).asRight + case float: Float => + Json.fromFloat(float).toRight(ParsingFailure(s"${node.value} cannot be represented as a JSON number.", null)) + case value: BigDecimal => Json.fromBigDecimal(value).asRight + case value: BigInt => Json.fromBigInt(value).asRight + case value: Byte => Json.fromInt(value.toInt).asRight + case value: Short => Json.fromInt(value.toInt).asRight + case value => ParsingFailure(s"Can't map ${value.getClass.getSimpleName} (${node.value}) to JSON.", null).asLeft + } + } + + private def nodeToJson(node: Node): Either[ParsingFailure, Json] = node match { + case node: Node.ScalarNode => + scalarNodeToJson(node) + case Node.SequenceNode(nodes, _) => + val values = nodes.traverse(nodeToJson) + values.map(Json.fromValues) + case Node.MappingNode(mappings, _) => + val fields = mappings.toList.traverse { + case (Node.ScalarNode(key, _), value) => + nodeToJson(value).map(key -> _) + case (node, _) => + Left(ParsingFailure(s"Unexpected ${node.getClass.getSimpleName} type, expected ScalarNode.", null)) + } + fields.map(Json.fromFields) + } + + private def errorToFailure(error: YamlError): ParsingFailure = + ParsingFailure(s"Parsing failed: ${error.msg}", error) +} diff --git a/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/Printer.scala b/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/Printer.scala new file mode 100644 index 00000000..fdb7d73f --- /dev/null +++ b/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/Printer.scala @@ -0,0 +1,38 @@ +/* + * 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. + */ + +package io.circe.yaml.scalayaml + +import io.circe.Json +import org.virtuslab.yaml.Node +import org.virtuslab.yaml.NodeOps + +object Printer extends io.circe.yaml.common.Printer { + + override def pretty(json: Json): String = { + val node = jsonToNode(json) + node.asYaml + } + + private def jsonToNode(json: Json): Node = json match { + case Json.JArray(value) => + Node.SequenceNode(value.map(jsonToNode): _*) + case Json.JObject(value) => + Node.MappingNode(value.toMap.map { case (key, value) => (Node.ScalarNode(key): Node) -> jsonToNode(value) }) + case json => + Node.ScalarNode(json.toString) + } +} diff --git a/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/parser/package.scala b/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/parser/package.scala new file mode 100644 index 00000000..f2a70b97 --- /dev/null +++ b/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/parser/package.scala @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package io.circe.yaml.scalayaml + +import cats.data.ValidatedNel +import io.circe.Decoder +import io.circe.Error +import io.circe.Json +import io.circe.ParsingFailure + +import java.io.Reader + +package object parser extends io.circe.yaml.common.Parser { + + /** + * Parse YAML from the given [[Reader]], returning either [[ParsingFailure]] or [[Json]] + * @param yaml + * @return + */ + def parse(yaml: Reader): Either[ParsingFailure, Json] = Parser.parse(yaml) + + def parse(yaml: String): Either[ParsingFailure, Json] = Parser.parse(yaml) + + def parseDocuments(yaml: Reader): Stream[Either[ParsingFailure, Json]] = Parser.parseDocuments(yaml) + def parseDocuments(yaml: String): Stream[Either[ParsingFailure, Json]] = Parser.parseDocuments(yaml) + + final def decode[A: Decoder](input: Reader): Either[Error, A] = Parser.decode[A](input) + final def decodeAccumulating[A: Decoder](input: Reader): ValidatedNel[Error, A] = + Parser.decodeAccumulating[A](input) +} diff --git a/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/printer/package.scala b/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/printer/package.scala new file mode 100644 index 00000000..a19a4345 --- /dev/null +++ b/circe-yaml-scalayaml/shared/src/main/scala/io/circe/yaml/scalayaml/printer/package.scala @@ -0,0 +1,28 @@ +/* + * 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. + */ + +package io.circe.yaml.scalayaml + +import io.circe.Json + +package object printer extends io.circe.yaml.common.Printer { + + /** + * A default printer implementation using Snake YAML. + */ + def print(tree: Json): String = pretty(tree) + def pretty(tree: Json): String = Printer.pretty(tree) +} diff --git a/circe-yaml-scalayaml/shared/src/test/scala/io/circe/yaml/scalayaml/ParserTests.scala b/circe-yaml-scalayaml/shared/src/test/scala/io/circe/yaml/scalayaml/ParserTests.scala new file mode 100644 index 00000000..be7dd289 --- /dev/null +++ b/circe-yaml-scalayaml/shared/src/test/scala/io/circe/yaml/scalayaml/ParserTests.scala @@ -0,0 +1,194 @@ +/* + * 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. + */ + +package io.circe.yaml.scalayaml + +import io.circe.Json +import io.circe.syntax._ +import org.scalatest.EitherValues +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import java.io.StringReader + +class ParserTests extends AnyFlatSpec with Matchers with EitherValues { + // the laws should do a pretty good job of surfacing errors; these are mainly to ensure test coverage + + "Parser.parse" should "fail on invalid tagged numbers" in { + assert(parser.parse("!!int 12foo").isLeft) + } + + it should "fail to parse complex keys" in { + assert( + parser + .parse(""" + |? - foo + | - bar + |: 1 + """.stripMargin) + .isLeft + ) + } + + it should "fail to parse invalid YAML" in { + assert( + parser + .parse( + """foo: - bar""" + ) + .isLeft + ) + } + + it should "parse yes as true" in { + assert( + parser + .parse( + """foo: yes""" + ) + .isRight + ) + } + + it should "parse hexadecimal" in { + assert( + parser + .parse( + """[0xFF, 0xff, 0xabcd]""" + ) + .contains(Seq(0xff, 0xff, 0xabcd).asJson) + ) + } + + it should "parse decimal with underscore breaks" in { + assert( + parser + .parse( + """foo: !!int 1_000_000""" + ) + .contains(Map("foo" -> 1000000).asJson) + ) + } + + it should "parse aliases" in { + assert( + Parser + .parse( + """ + | aliases: + | - &alias1 + | foo: + | bar + | baz: + | - *alias1 + | - *alias1 + |""".stripMargin + ) + .isRight + ) + } + + "Parser.parseDocuments" should "fail on invalid tagged numbers" in { + val result = parser.parseDocuments(new StringReader("!!int 12foo")).toList + assert(result.size == 1) + assert(result.head.isLeft) + } + + it should "fail to parse complex keys" in { + val result = parser + .parseDocuments(new StringReader(""" + |? - foo + | - bar + |: 1""".stripMargin)) + .toList + assert(result.size == 1) + assert(result.head.isLeft) + } + + it should "fail to parse invalid YAML" in { + val result = parser.parseDocuments(new StringReader("""foo: - bar""")).toList + assert(result.size == 1) + assert(result.head.isLeft) + assert(result.head.isInstanceOf[Either[io.circe.ParsingFailure, Json]]) + } + + it should "parse yes as true" in { + val result = parser.parseDocuments(new StringReader("""foo: yes""")).toList + assert(result.size == 1) + assert(result.head.isRight) + } + + it should "parse hexadecimal" in { + val result = parser.parseDocuments(new StringReader("""[0xFF, 0xff, 0xabcd]""")).toList + assert(result.size == 1) + assert(result.head.contains(Seq(0xff, 0xff, 0xabcd).asJson)) + } + + it should "parse decimal with underscore breaks" in { + val result = parser.parseDocuments(new StringReader("""foo: !!int 1_000_000""")).toList + assert(result.size == 1) + assert(result.head.contains(Map("foo" -> 1000000).asJson)) + } + + it should "parse aliases" in { + val result = parser + .parseDocuments( + new StringReader( + """ + | aliases: + | - &alias1 + | foo: + | bar + | baz: + | - *alias1 + | - *alias1 + |""".stripMargin + ) + ) + .toList + assert(result.size == 1) + assert(result.head.isRight) + } + + it should "parse when within depth limits" in { + assert( + Parser + .parse( + """ + | foo: + | bar: + | baz + |""".stripMargin + ) + .isRight + ) + } + + it should "parse when within code point limit" in { + assert( + Parser // 1MB + .parse( + """ + | foo: + | bar: + | baz + |""".stripMargin + ) + .isRight + ) + } + +} diff --git a/circe-yaml-scalayaml/shared/src/test/scala/io/circe/yaml/scalayaml/PrinterTests.scala b/circe-yaml-scalayaml/shared/src/test/scala/io/circe/yaml/scalayaml/PrinterTests.scala new file mode 100644 index 00000000..ccbd0279 --- /dev/null +++ b/circe-yaml-scalayaml/shared/src/test/scala/io/circe/yaml/scalayaml/PrinterTests.scala @@ -0,0 +1,101 @@ +/* + * 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. + */ + +package io.circe.yaml.scalayaml + +import io.circe.Json +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should.Matchers + +class PrinterTests extends AnyFreeSpec with Matchers { + + "Flow style" - { + val json = Json.obj("foo" -> Json.arr((0 until 3).map(_.toString).map(Json.fromString): _*)) + + "Block" in { + val printer = Printer + printer.pretty(json) shouldEqual + """foo: + | - "0" + | - "1" + | - "2" + |""".stripMargin + } + + } + + "Preserves order" - { + val kvPairs = Seq("d" -> 4, "a" -> 1, "b" -> 2, "c" -> 3) + val json = Json.obj(kvPairs.map { case (k, v) => k -> Json.fromInt(v) }: _*) + "true" in { + val printer = Printer + printer.pretty(json) shouldEqual + """d: 4 + |a: 1 + |b: 2 + |c: 3 + |""".stripMargin + } + } + + "Scalar style" - { + val foos = Seq.fill(40)("foo") + val foosPlain = foos.mkString(" ") + val foosFolded = Seq(foos.take(20), foos.slice(20, 40)).map(_.mkString(" ")).mkString("\n ") + val json = Json.obj("foo" -> Json.fromString(foosPlain)) + + "Plain" in { + val printer = Printer + printer.pretty(json) shouldEqual + s"""foo: "$foosPlain" + |""".stripMargin + } + + } + + "Plain with newlines" in { + val json = Json.obj("foo" -> Json.fromString("abc\nxyz\n")) + val printer = Printer + printer.pretty(json) shouldEqual + "foo: \"abc\\nxyz\\n\"\n" + } + + "Drop null keys" in { + val json = Json.obj("nullField" -> Json.Null, "nonNullField" -> Json.fromString("foo")) + Printer.pretty(json) shouldEqual "nullField: null\nnonNullField: \"foo\"\n" + } + + "Root integer" in { + val json = Json.fromInt(10) + Printer.pretty(json) shouldEqual "10\n" + } + + "Root float" in { + val json = Json.fromDoubleOrNull(22.22) + Printer.pretty(json) shouldEqual "22.22\n" + } + + "Line break" - { + val json = Json.arr(Json.fromString("foo"), Json.fromString("bar")) + + "Unix" in { + Printer.pretty(json) shouldEqual + "- \"foo\"\n- \"bar\"\n" + } + + } + +} diff --git a/project/plugins.sbt b/project/plugins.sbt index d8b09df4..6bc74113 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1,4 @@ addSbtPlugin("io.circe" % "sbt-circe-org" % "0.4.1") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1")